From fdbd8d9d2b2e04cd68fd800882309b40c05aba2c Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Tue, 1 Nov 2022 22:45:15 +0100 Subject: [PATCH 001/152] refactor: remove old updater Signed-off-by: Sefa Eyeoglu --- launcher/Application.cpp | 68 +-- launcher/Application.h | 12 +- launcher/CMakeLists.txt | 11 - launcher/UpdateController.cpp | 443 ------------------ launcher/UpdateController.h | 44 -- .../minecraft/launch/LauncherPartLaunch.cpp | 1 + launcher/net/MetaCacheSink.cpp | 1 + launcher/net/PasteUpload.cpp | 2 + launcher/ui/GuiUtil.cpp | 1 + launcher/ui/MainWindow.cpp | 90 +--- launcher/ui/MainWindow.h | 10 - launcher/ui/dialogs/BlockedModsDialog.cpp | 12 +- launcher/ui/dialogs/ExportInstanceDialog.cpp | 1 + launcher/ui/dialogs/UpdateDialog.cpp | 217 --------- launcher/ui/dialogs/UpdateDialog.h | 67 --- launcher/ui/dialogs/UpdateDialog.ui | 91 ---- launcher/ui/pages/global/LauncherPage.cpp | 119 +---- launcher/ui/pages/global/LauncherPage.h | 12 - launcher/ui/pages/global/LauncherPage.ui | 28 -- launcher/ui/pages/instance/LogPage.cpp | 2 +- launcher/ui/pages/instance/ScreenshotsPage.h | 1 + launcher/ui/pages/instance/ServersPage.cpp | 1 + launcher/ui/pages/instance/WorldListPage.cpp | 2 +- .../modplatform/legacy_ftb/ListModel.cpp | 2 + .../modplatform/modrinth/ModrinthModel.h | 1 + launcher/updater/DownloadTask.cpp | 177 ------- launcher/updater/DownloadTask.h | 100 ---- launcher/updater/GoUpdate.cpp | 198 -------- launcher/updater/GoUpdate.h | 125 ----- launcher/updater/MacSparkleUpdater.h | 2 - launcher/updater/MacSparkleUpdater.mm | 12 - launcher/updater/UpdateChecker.cpp | 296 ------------ launcher/updater/UpdateChecker.h | 140 ------ 33 files changed, 59 insertions(+), 2230 deletions(-) delete mode 100644 launcher/UpdateController.cpp delete mode 100644 launcher/UpdateController.h delete mode 100644 launcher/ui/dialogs/UpdateDialog.cpp delete mode 100644 launcher/ui/dialogs/UpdateDialog.h delete mode 100644 launcher/ui/dialogs/UpdateDialog.ui delete mode 100644 launcher/updater/DownloadTask.cpp delete mode 100644 launcher/updater/DownloadTask.h delete mode 100644 launcher/updater/GoUpdate.cpp delete mode 100644 launcher/updater/GoUpdate.h delete mode 100644 launcher/updater/UpdateChecker.cpp delete mode 100644 launcher/updater/UpdateChecker.h diff --git a/launcher/Application.cpp b/launcher/Application.cpp index ea8d2326..8fe5a8bf 100644 --- a/launcher/Application.cpp +++ b/launcher/Application.cpp @@ -104,7 +104,7 @@ #include "java/JavaUtils.h" -#include "updater/UpdateChecker.h" +#include "updater/ExternalUpdater.h" #include "tools/JProfiler.h" #include "tools/JVisualVM.h" @@ -127,6 +127,10 @@ #include "gamemode_client.h" #endif +#ifdef Q_OS_MAC +#include "updater/MacSparkleUpdater.h" +#endif + #if defined Q_OS_WIN32 #ifndef WIN32_LEAN_AND_MEAN @@ -162,45 +166,6 @@ void appDebugOutput(QtMsgType type, const QMessageLogContext &context, const QSt fflush(stderr); } -QString getIdealPlatform(QString currentPlatform) { - auto info = Sys::getKernelInfo(); - switch(info.kernelType) { - case Sys::KernelType::Darwin: { - if(info.kernelMajor >= 17) { - // macOS 10.13 or newer - return "osx64-5.15.2"; - } - else { - // macOS 10.12 or older - return "osx64"; - } - } - case Sys::KernelType::Windows: { - // FIXME: 5.15.2 is not stable on Windows, due to a large number of completely unpredictable and hard to reproduce issues - break; -/* - if(info.kernelMajor == 6 && info.kernelMinor >= 1) { - // Windows 7 - return "win32-5.15.2"; - } - else if (info.kernelMajor > 6) { - // Above Windows 7 - return "win32-5.15.2"; - } - else { - // Below Windows 7 - return "win32"; - } -*/ - } - case Sys::KernelType::Undetermined: - case Sys::KernelType::Linux: { - break; - } - } - return currentPlatform; -} - } Application::Application(int &argc, char **argv) : QApplication(argc, argv) @@ -490,10 +455,6 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv) { // Provide a fallback for migration from PolyMC m_settings.reset(new INISettingsObject({ BuildConfig.LAUNCHER_CONFIGFILE, "polymc.cfg", "multimc.cfg" }, this)); - // Updates - // Multiple channels are separated by spaces - m_settings->registerSetting("UpdateChannel", BuildConfig.VERSION_CHANNEL); - m_settings->registerSetting("AutoUpdate", true); // Theming m_settings->registerSetting("IconTheme", QString("pe_colored")); @@ -724,10 +685,10 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv) // initialize the updater if(BuildConfig.UPDATER_ENABLED) { - auto platform = getIdealPlatform(BuildConfig.BUILD_PLATFORM); - auto channelUrl = BuildConfig.UPDATER_BASE + platform + "/channels.json"; - qDebug() << "Initializing updater with platform: " << platform << " -- " << channelUrl; - m_updateChecker.reset(new UpdateChecker(m_network, channelUrl, BuildConfig.VERSION_CHANNEL)); + qDebug() << "Initializing updater"; +#ifdef Q_OS_MAC + m_updater.reset(new MacSparkleUpdater()); +#endif qDebug() << "<> Updater started."; } @@ -1690,3 +1651,14 @@ bool Application::handleDataMigration(const QString& currentData, } return true; } + +void Application::triggerUpdateCheck() +{ + if (m_updater) { + qDebug() << "Checking for updates."; + m_updater->setBetaAllowed(false); // There are no other channels than stable + m_updater->checkForUpdates(); + } else { + qDebug() << "Updater not available."; + } +} diff --git a/launcher/Application.h b/launcher/Application.h index 7884227a..23c70e4c 100644 --- a/launcher/Application.h +++ b/launcher/Application.h @@ -43,7 +43,6 @@ #include #include #include -#include #include @@ -63,7 +62,7 @@ class AccountList; class IconList; class QNetworkAccessManager; class JavaInstallList; -class UpdateChecker; +class ExternalUpdater; class BaseProfilerFactory; class BaseDetachedToolFactory; class TranslationsModel; @@ -124,10 +123,12 @@ public: void setApplicationTheme(const QString& name, bool initial); - shared_qobject_ptr updateChecker() { - return m_updateChecker; + shared_qobject_ptr updater() { + return m_updater; } + void triggerUpdateCheck(); + std::shared_ptr translations(); std::shared_ptr javalist(); @@ -248,7 +249,7 @@ private: shared_qobject_ptr m_network; - shared_qobject_ptr m_updateChecker; + shared_qobject_ptr m_updater; shared_qobject_ptr m_accounts; shared_qobject_ptr m_metacache; @@ -307,4 +308,3 @@ public: QString m_instanceIdToShowWindowOf; std::unique_ptr logFile; }; - diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index e8afa6b8..528c7990 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -150,12 +150,6 @@ set(LAUNCH_SOURCES # Old update system set(UPDATE_SOURCES - updater/GoUpdate.h - updater/GoUpdate.cpp - updater/UpdateChecker.h - updater/UpdateChecker.cpp - updater/DownloadTask.h - updater/DownloadTask.cpp updater/ExternalUpdater.h ) @@ -578,8 +572,6 @@ SET(LAUNCHER_SOURCES Application.cpp DataMigrationTask.h DataMigrationTask.cpp - UpdateController.cpp - UpdateController.h ApplicationMessage.h ApplicationMessage.cpp @@ -814,8 +806,6 @@ SET(LAUNCHER_SOURCES ui/dialogs/ProgressDialog.h ui/dialogs/ReviewMessageBox.cpp ui/dialogs/ReviewMessageBox.h - ui/dialogs/UpdateDialog.cpp - ui/dialogs/UpdateDialog.h ui/dialogs/VersionSelectDialog.cpp ui/dialogs/VersionSelectDialog.h ui/dialogs/SkinUploadDialog.cpp @@ -937,7 +927,6 @@ qt_wrap_ui(LAUNCHER_UI ui/dialogs/ProfileSetupDialog.ui ui/dialogs/ProgressDialog.ui ui/dialogs/NewInstanceDialog.ui - ui/dialogs/UpdateDialog.ui ui/dialogs/NewComponentDialog.ui ui/dialogs/NewsDialog.ui ui/dialogs/ProfileSelectDialog.ui diff --git a/launcher/UpdateController.cpp b/launcher/UpdateController.cpp deleted file mode 100644 index 9ff44854..00000000 --- a/launcher/UpdateController.cpp +++ /dev/null @@ -1,443 +0,0 @@ -#include -#include -#include -#include -#include "UpdateController.h" -#include -#include -#include -#include - -#include "BuildConfig.h" - - -// from -#ifndef S_IRUSR -#define __S_IREAD 0400 /* Read by owner. */ -#define __S_IWRITE 0200 /* Write by owner. */ -#define __S_IEXEC 0100 /* Execute by owner. */ -#define S_IRUSR __S_IREAD /* Read by owner. */ -#define S_IWUSR __S_IWRITE /* Write by owner. */ -#define S_IXUSR __S_IEXEC /* Execute by owner. */ - -#define S_IRGRP (S_IRUSR >> 3) /* Read by group. */ -#define S_IWGRP (S_IWUSR >> 3) /* Write by group. */ -#define S_IXGRP (S_IXUSR >> 3) /* Execute by group. */ - -#define S_IROTH (S_IRGRP >> 3) /* Read by others. */ -#define S_IWOTH (S_IWGRP >> 3) /* Write by others. */ -#define S_IXOTH (S_IXGRP >> 3) /* Execute by others. */ -#endif -static QFile::Permissions unixModeToPermissions(const int mode) -{ - QFile::Permissions perms; - - if (mode & S_IRUSR) - { - perms |= QFile::ReadUser; - } - if (mode & S_IWUSR) - { - perms |= QFile::WriteUser; - } - if (mode & S_IXUSR) - { - perms |= QFile::ExeUser; - } - - if (mode & S_IRGRP) - { - perms |= QFile::ReadGroup; - } - if (mode & S_IWGRP) - { - perms |= QFile::WriteGroup; - } - if (mode & S_IXGRP) - { - perms |= QFile::ExeGroup; - } - - if (mode & S_IROTH) - { - perms |= QFile::ReadOther; - } - if (mode & S_IWOTH) - { - perms |= QFile::WriteOther; - } - if (mode & S_IXOTH) - { - perms |= QFile::ExeOther; - } - return perms; -} - -static const QLatin1String liveCheckFile("live.check"); - -UpdateController::UpdateController(QWidget * parent, const QString& root, const QString updateFilesDir, GoUpdate::OperationList operations) -{ - m_parent = parent; - m_root = root; - m_updateFilesDir = updateFilesDir; - m_operations = operations; -} - - -void UpdateController::installUpdates() -{ - qint64 pid = -1; - QStringList args; - bool started = false; - - qDebug() << "Installing updates."; -#ifdef Q_OS_WIN - QString finishCmd = QApplication::applicationFilePath(); -#elif defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) || defined (Q_OS_OPENBSD) - QString finishCmd = FS::PathCombine(m_root, BuildConfig.LAUNCHER_NAME); -#elif defined Q_OS_MAC - QString finishCmd = QApplication::applicationFilePath(); -#else -#error Unsupported operating system. -#endif - - QString backupPath = FS::PathCombine(m_root, "update", "backup"); - QDir origin(m_root); - - // clean up the backup folder. it should be empty before we start - if(!FS::deletePath(backupPath)) - { - qWarning() << "couldn't remove previous backup folder" << backupPath; - } - // and it should exist. - if(!FS::ensureFolderPathExists(backupPath)) - { - qWarning() << "couldn't create folder" << backupPath; - return; - } - - bool useXPHack = false; - QString exePath; - QString exeOrigin; - QString exeBackup; - - // perform the update operations - for(auto op: m_operations) - { - switch(op.type) - { - // replace = move original out to backup, if it exists, move the new file in its place - case GoUpdate::Operation::OP_REPLACE: - { -#ifdef Q_OS_WIN32 - QString windowsExeName = BuildConfig.LAUNCHER_NAME + ".exe"; - // hack for people renaming the .exe because ... reasons :) - if(op.destination == windowsExeName) - { - op.destination = QFileInfo(QApplication::applicationFilePath()).fileName(); - } -#endif - QFileInfo destination (FS::PathCombine(m_root, op.destination)); - if(destination.exists()) - { - QString backupName = op.destination; - backupName.replace('/', '_'); - QString backupFilePath = FS::PathCombine(backupPath, backupName); - if(!QFile::rename(destination.absoluteFilePath(), backupFilePath)) - { - qWarning() << "Couldn't move:" << destination.absoluteFilePath() << "to" << backupFilePath; - m_failedOperationType = Replace; - m_failedFile = op.destination; - fail(); - return; - } - BackupEntry be; - be.original = destination.absoluteFilePath(); - be.backup = backupFilePath; - be.update = op.source; - m_replace_backups.append(be); - } - // make sure the folder we are putting this into exists - if(!FS::ensureFilePathExists(destination.absoluteFilePath())) - { - qWarning() << "REPLACE: Couldn't create folder:" << destination.absoluteFilePath(); - m_failedOperationType = Replace; - m_failedFile = op.destination; - fail(); - return; - } - // now move the new file in - if(!QFile::rename(op.source, destination.absoluteFilePath())) - { - qWarning() << "REPLACE: Couldn't move:" << op.source << "to" << destination.absoluteFilePath(); - m_failedOperationType = Replace; - m_failedFile = op.destination; - fail(); - return; - } - QFile::setPermissions(destination.absoluteFilePath(), unixModeToPermissions(op.destinationMode)); - } - break; - // delete = move original to backup - case GoUpdate::Operation::OP_DELETE: - { - QString destFilePath = FS::PathCombine(m_root, op.destination); - if(QFile::exists(destFilePath)) - { - QString backupName = op.destination; - backupName.replace('/', '_'); - QString trashFilePath = FS::PathCombine(backupPath, backupName); - - if(!QFile::rename(destFilePath, trashFilePath)) - { - qWarning() << "DELETE: Couldn't move:" << op.destination << "to" << trashFilePath; - m_failedFile = op.destination; - m_failedOperationType = Delete; - fail(); - return; - } - BackupEntry be; - be.original = destFilePath; - be.backup = trashFilePath; - m_delete_backups.append(be); - } - } - break; - } - } - - // try to start the new binary - args = qApp->arguments(); - args.removeFirst(); - - // on old Windows, do insane things... no error checking here, this is just to have something. - if(useXPHack) - { - QString script; - auto nativePath = QDir::toNativeSeparators(exePath); - auto nativeOriginPath = QDir::toNativeSeparators(exeOrigin); - auto nativeBackupPath = QDir::toNativeSeparators(exeBackup); - - // so we write this vbscript thing... - QTextStream out(&script); - out << "WScript.Sleep 1000\n"; - out << "Set fso=CreateObject(\"Scripting.FileSystemObject\")\n"; - out << "Set shell=CreateObject(\"WScript.Shell\")\n"; - out << "fso.MoveFile \"" << nativePath << "\", \"" << nativeBackupPath << "\"\n"; - out << "fso.MoveFile \"" << nativeOriginPath << "\", \"" << nativePath << "\"\n"; - out << "shell.Run \"" << nativePath << "\"\n"; - - QString scriptPath = FS::PathCombine(m_root, "update", "update.vbs"); - - // we save it - QFile scriptFile(scriptPath); - scriptFile.open(QIODevice::WriteOnly); - scriptFile.write(script.toLocal8Bit().replace("\n", "\r\n")); - scriptFile.close(); - - // we run it - started = QProcess::startDetached("wscript", {scriptPath}, m_root); - - // and we quit. conscious thought. - qApp->quit(); - return; - } - bool doLiveCheck = true; - bool startFailed = false; - - // remove live check file, if any - if(QFile::exists(liveCheckFile)) - { - if(!QFile::remove(liveCheckFile)) - { - qWarning() << "Couldn't remove the" << liveCheckFile << "file! We will proceed without :("; - doLiveCheck = false; - } - } - - if(doLiveCheck) - { - if(!args.contains("--alive")) - { - args.append("--alive"); - } - } - - // FIXME: reparse args and construct a safe variant from scratch. This is a workaround for GH-1874: - QStringList realargs; - int skip = 0; - for(auto & arg: args) - { - if(skip) - { - skip--; - continue; - } - if(arg == "-l") - { - skip = 1; - continue; - } - realargs.append(arg); - } - - // start the updated application - started = QProcess::startDetached(finishCmd, realargs, QDir::currentPath(), &pid); - // much dumber check - just find out if the call - if(!started || pid == -1) - { - qWarning() << "Couldn't start new process properly!"; - startFailed = true; - } - if(!startFailed && doLiveCheck) - { - int attempts = 0; - while(attempts < 10) - { - attempts++; - QString key; - std::this_thread::sleep_for(std::chrono::milliseconds(250)); - if(!QFile::exists(liveCheckFile)) - { - qWarning() << "Couldn't find the" << liveCheckFile << "file!"; - startFailed = true; - continue; - } - try - { - key = QString::fromUtf8(FS::read(liveCheckFile)); - auto id = ApplicationId::fromRawString(key); - LocalPeer peer(nullptr, id); - if(peer.isClient()) - { - startFailed = false; - qDebug() << "Found process started with key " << key; - break; - } - else - { - startFailed = true; - qDebug() << "Process started with key " << key << "apparently died or is not reponding..."; - break; - } - } - catch (const Exception &e) - { - qWarning() << "Couldn't read the" << liveCheckFile << "file!"; - startFailed = true; - continue; - } - } - } - if(startFailed) - { - m_failedOperationType = Start; - fail(); - return; - } - else - { - origin.rmdir(m_updateFilesDir); - qApp->quit(); - return; - } -} - -void UpdateController::fail() -{ - qWarning() << "Update failed!"; - - QString msg; - bool doRollback = false; - QString failTitle = QObject::tr("Update failed!"); - QString rollFailTitle = QObject::tr("Rollback failed!"); - switch (m_failedOperationType) - { - case Replace: - { - msg = QObject::tr( - "Couldn't replace file %1. Changes will be reverted.\n" - "See the %2 log file for details." - ).arg(m_failedFile, BuildConfig.LAUNCHER_DISPLAYNAME); - doRollback = true; - QMessageBox::critical(m_parent, failTitle, msg); - break; - } - case Delete: - { - msg = QObject::tr( - "Couldn't remove file %1. Changes will be reverted.\n" - "See the %2 log file for details." - ).arg(m_failedFile, BuildConfig.LAUNCHER_DISPLAYNAME); - doRollback = true; - QMessageBox::critical(m_parent, failTitle, msg); - break; - } - case Start: - { - msg = QObject::tr("The new version didn't start or is too old and doesn't respond to startup checks.\n" - "\n" - "Roll back to previous version?"); - auto result = QMessageBox::critical( - m_parent, - failTitle, - msg, - QMessageBox::Yes | QMessageBox::No, - QMessageBox::Yes - ); - doRollback = (result == QMessageBox::Yes); - break; - } - case Nothing: - default: - return; - } - if(doRollback) - { - auto rollbackOK = rollback(); - if(!rollbackOK) - { - msg = QObject::tr("The rollback failed too.\n" - "You will have to repair %1 manually.\n" - "Please let us know why and how this happened.").arg(BuildConfig.LAUNCHER_DISPLAYNAME); - QMessageBox::critical(m_parent, rollFailTitle, msg); - qApp->quit(); - } - } - else - { - qApp->quit(); - } -} - -bool UpdateController::rollback() -{ - bool revertOK = true; - // if the above failed, roll back changes - for(auto backup:m_replace_backups) - { - qWarning() << "restoring" << backup.original << "from" << backup.backup; - if(!QFile::rename(backup.original, backup.update)) - { - revertOK = false; - qWarning() << "moving new" << backup.original << "back to" << backup.update << "failed!"; - continue; - } - - if(!QFile::rename(backup.backup, backup.original)) - { - revertOK = false; - qWarning() << "restoring" << backup.original << "failed!"; - } - } - for(auto backup:m_delete_backups) - { - qWarning() << "restoring" << backup.original << "from" << backup.backup; - if(!QFile::rename(backup.backup, backup.original)) - { - revertOK = false; - qWarning() << "restoring" << backup.original << "failed!"; - } - } - return revertOK; -} diff --git a/launcher/UpdateController.h b/launcher/UpdateController.h deleted file mode 100644 index 715554e5..00000000 --- a/launcher/UpdateController.h +++ /dev/null @@ -1,44 +0,0 @@ -#pragma once - -#include -#include -#include - -class QWidget; - -class UpdateController -{ -public: - UpdateController(QWidget * parent, const QString &root, const QString updateFilesDir, GoUpdate::OperationList operations); - void installUpdates(); - -private: - void fail(); - bool rollback(); - -private: - QString m_root; - QString m_updateFilesDir; - GoUpdate::OperationList m_operations; - QWidget * m_parent; - - struct BackupEntry - { - // path where we got the new file from - QString update; - // path of what is being actually updated - QString original; - // path where the backup of the updated file was placed - QString backup; - }; - QList m_replace_backups; - QList m_delete_backups; - enum Failure - { - Replace, - Delete, - Start, - Nothing - } m_failedOperationType = Nothing; - QString m_failedFile; -}; diff --git a/launcher/minecraft/launch/LauncherPartLaunch.cpp b/launcher/minecraft/launch/LauncherPartLaunch.cpp index 1d8d7083..8ecf715d 100644 --- a/launcher/minecraft/launch/LauncherPartLaunch.cpp +++ b/launcher/minecraft/launch/LauncherPartLaunch.cpp @@ -36,6 +36,7 @@ #include "LauncherPartLaunch.h" #include +#include #include "launch/LaunchTask.h" #include "minecraft/MinecraftInstance.h" diff --git a/launcher/net/MetaCacheSink.cpp b/launcher/net/MetaCacheSink.cpp index 5ae53c1c..c730fdbf 100644 --- a/launcher/net/MetaCacheSink.cpp +++ b/launcher/net/MetaCacheSink.cpp @@ -36,6 +36,7 @@ #include "MetaCacheSink.h" #include #include +#include #include "Application.h" namespace Net { diff --git a/launcher/net/PasteUpload.cpp b/launcher/net/PasteUpload.cpp index 76b86743..d9e785c5 100644 --- a/launcher/net/PasteUpload.cpp +++ b/launcher/net/PasteUpload.cpp @@ -41,9 +41,11 @@ #include #include +#include #include #include #include +#include std::array PasteUpload::PasteTypes = { {{"0x0.st", "https://0x0.st", ""}, diff --git a/launcher/ui/GuiUtil.cpp b/launcher/ui/GuiUtil.cpp index 5a62e4d0..b1ea5ee9 100644 --- a/launcher/ui/GuiUtil.cpp +++ b/launcher/ui/GuiUtil.cpp @@ -39,6 +39,7 @@ #include #include #include +#include #include "ui/dialogs/ProgressDialog.h" #include "ui/dialogs/CustomMessageBox.h" diff --git a/launcher/ui/MainWindow.cpp b/launcher/ui/MainWindow.cpp index 929f2a85..0595634f 100644 --- a/launcher/ui/MainWindow.cpp +++ b/launcher/ui/MainWindow.cpp @@ -83,8 +83,7 @@ #include #include #include -#include -#include +#include #include #include "InstanceWindow.h" #include "InstancePageProvider.h" @@ -99,16 +98,13 @@ #include "ui/dialogs/NewsDialog.h" #include "ui/dialogs/ProgressDialog.h" #include "ui/dialogs/AboutDialog.h" -#include "ui/dialogs/VersionSelectDialog.h" #include "ui/dialogs/CustomMessageBox.h" #include "ui/dialogs/IconPickerDialog.h" #include "ui/dialogs/CopyInstanceDialog.h" -#include "ui/dialogs/UpdateDialog.h" #include "ui/dialogs/EditAccountDialog.h" #include "ui/dialogs/ExportInstanceDialog.h" #include "ui/themes/ITheme.h" -#include "UpdateController.h" #include "KonamiCode.h" #include "InstanceImportTask.h" @@ -1039,9 +1035,7 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new MainWindow updateNewsLabel(); } - - if(BuildConfig.UPDATER_ENABLED) - { + if (BuildConfig.UPDATER_ENABLED) { bool updatesAllowed = APPLICATION->updatesAreAllowed(); updatesAllowedChanged(updatesAllowed); @@ -1049,21 +1043,10 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new MainWindow connect(ui->actionCheckUpdate.operator->(), &QAction::triggered, this, &MainWindow::checkForUpdates); // set up the updater object. - auto updater = APPLICATION->updateChecker(); - connect(updater.get(), &UpdateChecker::updateAvailable, this, &MainWindow::updateAvailable); - connect(updater.get(), &UpdateChecker::noUpdateFound, this, &MainWindow::updateNotAvailable); - // if automatic update checks are allowed, start one. - if (APPLICATION->settings()->get("AutoUpdate").toBool() && updatesAllowed) - { - updater->checkForUpdate(APPLICATION->settings()->get("UpdateChannel").toString(), false); - } + auto updater = APPLICATION->updater(); - if (APPLICATION->updateChecker()->getExternalUpdater()) - { - connect(APPLICATION->updateChecker()->getExternalUpdater(), - &ExternalUpdater::canCheckForUpdatesChanged, - this, - &MainWindow::updatesAllowedChanged); + if (updater) { + connect(updater.get(), &ExternalUpdater::canCheckForUpdatesChanged, this, &MainWindow::updatesAllowedChanged); } } @@ -1541,32 +1524,6 @@ void MainWindow::updateNewsLabel() } } -void MainWindow::updateAvailable(GoUpdate::Status status) -{ - if(!APPLICATION->updatesAreAllowed()) - { - updateNotAvailable(); - return; - } - UpdateDialog dlg(true, this); - UpdateAction action = (UpdateAction)dlg.exec(); - switch (action) - { - case UPDATE_LATER: - qDebug() << "Update will be installed later."; - break; - case UPDATE_NOW: - downloadUpdates(status); - break; - } -} - -void MainWindow::updateNotAvailable() -{ - UpdateDialog dlg(false, this); - dlg.exec(); -} - QList stringToIntList(const QString &string) { #if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) @@ -1591,40 +1548,6 @@ QString intListToString(const QList &list) return slist.join(','); } -void MainWindow::downloadUpdates(GoUpdate::Status status) -{ - if(!APPLICATION->updatesAreAllowed()) - { - return; - } - qDebug() << "Downloading updates."; - ProgressDialog updateDlg(this); - status.rootPath = APPLICATION->root(); - - auto dlPath = FS::PathCombine(APPLICATION->root(), "update", "XXXXXX"); - if (!FS::ensureFilePathExists(dlPath)) - { - CustomMessageBox::selectable(this, tr("Error"), tr("Couldn't create folder for update downloads:\n%1").arg(dlPath), QMessageBox::Warning)->show(); - } - GoUpdate::DownloadTask updateTask(APPLICATION->network(), status, dlPath, &updateDlg); - // If the task succeeds, install the updates. - if (updateDlg.execWithTask(&updateTask)) - { - /** - * NOTE: This disables launching instances until the update either succeeds (and this process exits) - * or the update fails (and the control leaves this scope). - */ - APPLICATION->updateIsRunning(true); - UpdateController update(this, APPLICATION->root(), updateTask.updateFilesDir(), updateTask.operations()); - update.installUpdates(); - APPLICATION->updateIsRunning(false); - } - else - { - CustomMessageBox::selectable(this, tr("Error"), updateTask.failReason(), QMessageBox::Warning)->show(); - } -} - void MainWindow::onCatToggled(bool state) { setCatBackground(state); @@ -1941,8 +1864,7 @@ void MainWindow::checkForUpdates() { if(BuildConfig.UPDATER_ENABLED) { - auto updater = APPLICATION->updateChecker(); - updater->checkForUpdate(APPLICATION->settings()->get("UpdateChannel").toString(), true); + APPLICATION->triggerUpdateCheck(); } else { diff --git a/launcher/ui/MainWindow.h b/launcher/ui/MainWindow.h index 0aa01ee2..53db4919 100644 --- a/launcher/ui/MainWindow.h +++ b/launcher/ui/MainWindow.h @@ -48,7 +48,6 @@ #include "BaseInstance.h" #include "minecraft/auth/MinecraftAccount.h" #include "net/NetJob.h" -#include "updater/GoUpdate.h" class LaunchController; class NewsChecker; @@ -188,10 +187,6 @@ private slots: void startTask(Task *task); - void updateAvailable(GoUpdate::Status status); - - void updateNotAvailable(); - void defaultAccountChanged(); void changeActiveAccount(); @@ -200,10 +195,6 @@ private slots: void updateNewsLabel(); - /*! - * Runs the DownloadTask and installs updates. - */ - void downloadUpdates(GoUpdate::Status status); void konamiTriggered(); @@ -252,4 +243,3 @@ private: // managed by the application object Task *m_versionLoadTask = nullptr; }; - diff --git a/launcher/ui/dialogs/BlockedModsDialog.cpp b/launcher/ui/dialogs/BlockedModsDialog.cpp index edb4ff7d..eeeeb709 100644 --- a/launcher/ui/dialogs/BlockedModsDialog.cpp +++ b/launcher/ui/dialogs/BlockedModsDialog.cpp @@ -1,14 +1,18 @@ #include "BlockedModsDialog.h" -#include -#include -#include -#include "Application.h" #include "ui_BlockedModsDialog.h" +#include "Application.h" + #include +#include +#include +#include +#include #include #include #include +#include +#include #include BlockedModsDialog::BlockedModsDialog(QWidget* parent, const QString& title, const QString& text, QList& mods) diff --git a/launcher/ui/dialogs/ExportInstanceDialog.cpp b/launcher/ui/dialogs/ExportInstanceDialog.cpp index 88552b23..f13e36e8 100644 --- a/launcher/ui/dialogs/ExportInstanceDialog.cpp +++ b/launcher/ui/dialogs/ExportInstanceDialog.cpp @@ -44,6 +44,7 @@ #include #include #include +#include #include "StringUtils.h" #include "SeparatorPrefixTree.h" #include "Application.h" diff --git a/launcher/ui/dialogs/UpdateDialog.cpp b/launcher/ui/dialogs/UpdateDialog.cpp deleted file mode 100644 index 9e82531a..00000000 --- a/launcher/ui/dialogs/UpdateDialog.cpp +++ /dev/null @@ -1,217 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-only -/* - * PolyMC - 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 "UpdateDialog.h" -#include "ui_UpdateDialog.h" -#include -#include "Application.h" -#include -#include - -#include "BuildConfig.h" -#include "HoeDown.h" - -UpdateDialog::UpdateDialog(bool hasUpdate, QWidget *parent) : QDialog(parent), ui(new Ui::UpdateDialog) -{ - ui->setupUi(this); - auto channel = APPLICATION->settings()->get("UpdateChannel").toString(); - if(hasUpdate) - { - ui->label->setText(tr("A new %1 update is available!").arg(channel)); - } - else - { - ui->label->setText(tr("No %1 updates found. You are running the latest version.").arg(channel)); - ui->btnUpdateNow->setHidden(true); - ui->btnUpdateLater->setText(tr("Close")); - } - ui->changelogBrowser->setHtml(tr("

Loading changelog...

")); - loadChangelog(); - restoreGeometry(QByteArray::fromBase64(APPLICATION->settings()->get("UpdateDialogGeometry").toByteArray())); -} - -UpdateDialog::~UpdateDialog() -{ -} - -void UpdateDialog::loadChangelog() -{ - auto channel = APPLICATION->settings()->get("UpdateChannel").toString(); - dljob = new NetJob("Changelog", APPLICATION->network()); - QString url; - if(channel == "stable") - { - url = QString("https://raw.githubusercontent.com/PrismLauncher/PrismLauncher/%1/changelog.md").arg(channel); - m_changelogType = CHANGELOG_MARKDOWN; - } - else - { - url = QString("https://api.github.com/repos/PrismLauncher/PrismLauncher/compare/%1...%2").arg(BuildConfig.GIT_COMMIT, channel); - m_changelogType = CHANGELOG_COMMITS; - } - dljob->addNetAction(Net::Download::makeByteArray(QUrl(url), &changelogData)); - connect(dljob.get(), &NetJob::succeeded, this, &UpdateDialog::changelogLoaded); - connect(dljob.get(), &NetJob::failed, this, &UpdateDialog::changelogFailed); - dljob->start(); -} - -QString reprocessMarkdown(QByteArray markdown) -{ - HoeDown hoedown; - QString output = hoedown.process(markdown); - - // HACK: easier than customizing hoedown - output.replace(QRegularExpression("GH-([0-9]+)"), "GH-\\1"); - qDebug() << output; - return output; -} - -QString reprocessCommits(QByteArray json) -{ - auto channel = APPLICATION->settings()->get("UpdateChannel").toString(); - try - { - QString result; - auto document = Json::requireDocument(json); - auto rootobject = Json::requireObject(document); - auto status = Json::requireString(rootobject, "status"); - auto diff_url = Json::requireString(rootobject, "html_url"); - - auto print_commits = [&]() - { - result += ""; - auto commitarray = Json::requireArray(rootobject, "commits"); - for(int i = commitarray.size() - 1; i >= 0; i--) - { - const auto & commitval = commitarray[i]; - auto commitobj = Json::requireObject(commitval); - auto parents_info = Json::ensureArray(commitobj, "parents"); - // NOTE: this ignores merge commits, because they have more than one parent - if(parents_info.size() > 1) - { - continue; - } - auto commit_url = Json::requireString(commitobj, "html_url"); - auto commit_info = Json::requireObject(commitobj, "commit"); - auto commit_message = Json::requireString(commit_info, "message"); - auto lines = commit_message.split('\n'); - QRegularExpression regexp("(?(GH-(?[0-9]+))|(NOISSUE)|(SCRATCH))? *(?.*) *"); - auto match = regexp.match(lines.takeFirst(), 0, QRegularExpression::NormalMatch); - auto issuenr = match.captured("issuenr"); - auto prefix = match.captured("prefix"); - auto rest = match.captured("rest"); - result += ""; - lines.prepend(rest); - result += ""; - } - result += "
"; - if(issuenr.length()) - { - result += QString("GH-%2").arg(issuenr, issuenr); - } - else if(prefix.length()) - { - result += QString("%2").arg(commit_url, prefix); - } - else - { - result += QString("NOISSUE").arg(commit_url); - } - result += "

" + lines.join("
") + "

"; - }; - - if(status == "identical") - { - return QObject::tr("

There are no code changes between your current version and latest %1.

").arg(channel); - } - else if(status == "ahead") - { - result += QObject::tr("

Following commits were added since last update:

"); - print_commits(); - } - else if(status == "diverged") - { - auto commit_ahead = Json::requireInteger(rootobject, "ahead_by"); - auto commit_behind = Json::requireInteger(rootobject, "behind_by"); - result += QObject::tr("

The update removes %1 commits and adds the following %2:

").arg(commit_behind).arg(commit_ahead); - print_commits(); - } - result += QObject::tr("

You can look at the changes on github.

").arg(diff_url); - return result; - } - catch (const JSONValidationError &e) - { - qWarning() << "Got an unparseable commit log from github:" << e.what(); - qDebug() << json; - } - return QString(); -} - -void UpdateDialog::changelogLoaded() -{ - QString result; - switch(m_changelogType) - { - case CHANGELOG_COMMITS: - result = reprocessCommits(changelogData); - break; - case CHANGELOG_MARKDOWN: - result = reprocessMarkdown(changelogData); - break; - } - changelogData.clear(); - ui->changelogBrowser->setHtml(result); -} - -void UpdateDialog::changelogFailed(QString reason) -{ - ui->changelogBrowser->setHtml(tr("

Failed to fetch changelog... Error: %1

").arg(reason)); -} - -void UpdateDialog::on_btnUpdateLater_clicked() -{ - reject(); -} - -void UpdateDialog::on_btnUpdateNow_clicked() -{ - done(UPDATE_NOW); -} - -void UpdateDialog::closeEvent(QCloseEvent* evt) -{ - APPLICATION->settings()->set("UpdateDialogGeometry", saveGeometry().toBase64()); - QDialog::closeEvent(evt); -} diff --git a/launcher/ui/dialogs/UpdateDialog.h b/launcher/ui/dialogs/UpdateDialog.h deleted file mode 100644 index 07cbe09f..00000000 --- a/launcher/ui/dialogs/UpdateDialog.h +++ /dev/null @@ -1,67 +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 -#include "net/NetJob.h" - -namespace Ui -{ -class UpdateDialog; -} - -enum UpdateAction -{ - UPDATE_LATER = QDialog::Rejected, - UPDATE_NOW = QDialog::Accepted, -}; - -enum ChangelogType -{ - CHANGELOG_MARKDOWN, - CHANGELOG_COMMITS -}; - -class UpdateDialog : public QDialog -{ - Q_OBJECT - -public: - explicit UpdateDialog(bool hasUpdate = true, QWidget *parent = 0); - ~UpdateDialog(); - -public slots: - void on_btnUpdateNow_clicked(); - void on_btnUpdateLater_clicked(); - - /// Starts loading the changelog - void loadChangelog(); - - /// Slot for when the chengelog loads successfully. - void changelogLoaded(); - - /// Slot for when the chengelog fails to load... - void changelogFailed(QString reason); - -protected: - void closeEvent(QCloseEvent * ) override; - -private: - Ui::UpdateDialog *ui; - QByteArray changelogData; - NetJob::Ptr dljob; - ChangelogType m_changelogType = CHANGELOG_MARKDOWN; -}; diff --git a/launcher/ui/dialogs/UpdateDialog.ui b/launcher/ui/dialogs/UpdateDialog.ui deleted file mode 100644 index 5eb9d88a..00000000 --- a/launcher/ui/dialogs/UpdateDialog.ui +++ /dev/null @@ -1,91 +0,0 @@ - - - UpdateDialog - - - - 0 - 0 - 657 - 673 - - - - Launcher Update - - - - :/icons/toolbar/checkupdate:/icons/toolbar/checkupdate - - - - - - - - - 14 - - - - - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - changelogBrowser - - - - - - - - - true - - - - - - - - - - 0 - 0 - - - - Update now - - - - - - - - 0 - 0 - - - - Don't update yet - - - - - - - - - changelogBrowser - btnUpdateNow - btnUpdateLater - - - - - - diff --git a/launcher/ui/pages/global/LauncherPage.cpp b/launcher/ui/pages/global/LauncherPage.cpp index cae0635f..a4c2755c 100644 --- a/launcher/ui/pages/global/LauncherPage.cpp +++ b/launcher/ui/pages/global/LauncherPage.cpp @@ -44,14 +44,13 @@ #include #include -#include "updater/UpdateChecker.h" - #include "settings/SettingsObject.h" #include #include "Application.h" #include "BuildConfig.h" #include "DesktopServices.h" #include "ui/themes/ITheme.h" +#include "updater/ExternalUpdater.h" #include #include @@ -80,30 +79,8 @@ LauncherPage::LauncherPage(QWidget *parent) : QWidget(parent), ui(new Ui::Launch m_languageModel = APPLICATION->translations(); loadSettings(); - if(BuildConfig.UPDATER_ENABLED) - { - QObject::connect(APPLICATION->updateChecker().get(), &UpdateChecker::channelListLoaded, this, &LauncherPage::refreshUpdateChannelList); + ui->updateSettingsBox->setHidden(!APPLICATION->updater()); - if (APPLICATION->updateChecker()->hasChannels()) - { - refreshUpdateChannelList(); - } - else - { - APPLICATION->updateChecker()->updateChanList(false); - } - - if (APPLICATION->updateChecker()->getExternalUpdater()) - { - ui->updateChannelComboBox->setVisible(false); - ui->updateChannelDescLabel->setVisible(false); - ui->updateChannelLabel->setVisible(false); - } - } - else - { - ui->updateSettingsBox->setHidden(true); - } connect(ui->fontSizeBox, SIGNAL(valueChanged(int)), SLOT(refreshFontPreview())); connect(ui->consoleFont, SIGNAL(currentFontChanged(QFont)), SLOT(refreshFontPreview())); } @@ -198,94 +175,16 @@ void LauncherPage::on_metadataDisableBtn_clicked() ui->metadataWarningLabel->setHidden(!ui->metadataDisableBtn->isChecked()); } -void LauncherPage::refreshUpdateChannelList() -{ - // Stop listening for selection changes. It's going to change a lot while we update it and - // we don't need to update the - // description label constantly. - QObject::disconnect(ui->updateChannelComboBox, SIGNAL(currentIndexChanged(int)), this, - SLOT(updateChannelSelectionChanged(int))); - - QList channelList = APPLICATION->updateChecker()->getChannelList(); - ui->updateChannelComboBox->clear(); - int selection = -1; - for (int i = 0; i < channelList.count(); i++) - { - UpdateChecker::ChannelListEntry entry = channelList.at(i); - - // When it comes to selection, we'll rely on the indexes of a channel entry being the - // same in the - // combo box as it is in the update checker's channel list. - // This probably isn't very safe, but the channel list doesn't change often enough (or - // at all) for - // this to be a big deal. Hope it doesn't break... - ui->updateChannelComboBox->addItem(entry.name); - - // If the update channel we just added was the selected one, set the current index in - // the combo box to it. - if (entry.id == m_currentUpdateChannel) - { - qDebug() << "Selected index" << i << "channel id" << m_currentUpdateChannel; - selection = i; - } - } - - ui->updateChannelComboBox->setCurrentIndex(selection); - - // Start listening for selection changes again and update the description label. - QObject::connect(ui->updateChannelComboBox, SIGNAL(currentIndexChanged(int)), this, - SLOT(updateChannelSelectionChanged(int))); - refreshUpdateChannelDesc(); - - // Now that we've updated the channel list, we can enable the combo box. - // It starts off disabled so that if the channel list hasn't been loaded, it will be - // disabled. - ui->updateChannelComboBox->setEnabled(true); -} - -void LauncherPage::updateChannelSelectionChanged(int index) -{ - refreshUpdateChannelDesc(); -} - -void LauncherPage::refreshUpdateChannelDesc() -{ - // Get the channel list. - QList channelList = APPLICATION->updateChecker()->getChannelList(); - int selectedIndex = ui->updateChannelComboBox->currentIndex(); - if (selectedIndex < 0) - { - return; - } - if (selectedIndex < channelList.count()) - { - // Find the channel list entry with the given index. - UpdateChecker::ChannelListEntry selected = channelList.at(selectedIndex); - - // Set the description text. - ui->updateChannelDescLabel->setText(selected.description); - - // Set the currently selected channel ID. - m_currentUpdateChannel = selected.id; - } -} - void LauncherPage::applySettings() { auto s = APPLICATION->settings(); // Updates - if (BuildConfig.UPDATER_ENABLED && APPLICATION->updateChecker()->getExternalUpdater()) + if (APPLICATION->updater()) { - APPLICATION->updateChecker()->getExternalUpdater()->setAutomaticallyChecksForUpdates( - ui->autoUpdateCheckBox->isChecked()); - } - else - { - s->set("AutoUpdate", ui->autoUpdateCheckBox->isChecked()); + APPLICATION->updater()->setAutomaticallyChecksForUpdates(ui->autoUpdateCheckBox->isChecked()); } - s->set("UpdateChannel", m_currentUpdateChannel); auto original = s->get("IconTheme").toString(); //FIXME: make generic switch (ui->themeComboBox->currentIndex()) @@ -390,17 +289,11 @@ void LauncherPage::loadSettings() { auto s = APPLICATION->settings(); // Updates - if (BuildConfig.UPDATER_ENABLED && APPLICATION->updateChecker()->getExternalUpdater()) + if (APPLICATION->updater()) { - ui->autoUpdateCheckBox->setChecked( - APPLICATION->updateChecker()->getExternalUpdater()->getAutomaticallyChecksForUpdates()); - } - else - { - ui->autoUpdateCheckBox->setChecked(s->get("AutoUpdate").toBool()); + ui->autoUpdateCheckBox->setChecked(APPLICATION->updater()->getAutomaticallyChecksForUpdates()); } - m_currentUpdateChannel = s->get("UpdateChannel").toString(); //FIXME: make generic auto theme = s->get("IconTheme").toString(); QStringList iconThemeOptions{"pe_colored", diff --git a/launcher/ui/pages/global/LauncherPage.h b/launcher/ui/pages/global/LauncherPage.h index f38c922e..c60156c2 100644 --- a/launcher/ui/pages/global/LauncherPage.h +++ b/launcher/ui/pages/global/LauncherPage.h @@ -90,23 +90,11 @@ slots: void on_iconsDirBrowseBtn_clicked(); void on_metadataDisableBtn_clicked(); - /*! - * Updates the list of update channels in the combo box. - */ - void refreshUpdateChannelList(); - - /*! - * Updates the channel description label. - */ - void refreshUpdateChannelDesc(); - /*! * Updates the font preview */ void refreshFontPreview(); - void updateChannelSelectionChanged(int index); - private: Ui::LauncherPage *ui; diff --git a/launcher/ui/pages/global/LauncherPage.ui b/launcher/ui/pages/global/LauncherPage.ui index c44718a1..fb36608d 100644 --- a/launcher/ui/pages/global/LauncherPage.ui +++ b/launcher/ui/pages/global/LauncherPage.ui @@ -58,33 +58,6 @@ - - - - Up&date Channel: - - - updateChannelComboBox - - - - - - - false - - - - - - - No channel selected. - - - true - - - @@ -573,7 +546,6 @@ tabWidget autoUpdateCheckBox - updateChannelComboBox instDirTextBox instDirBrowseBtn modsDirTextBox diff --git a/launcher/ui/pages/instance/LogPage.cpp b/launcher/ui/pages/instance/LogPage.cpp index 31c3e925..9985f426 100644 --- a/launcher/ui/pages/instance/LogPage.cpp +++ b/launcher/ui/pages/instance/LogPage.cpp @@ -39,7 +39,7 @@ #include "Application.h" -#include +#include #include #include diff --git a/launcher/ui/pages/instance/ScreenshotsPage.h b/launcher/ui/pages/instance/ScreenshotsPage.h index c22706af..cdd53cc9 100644 --- a/launcher/ui/pages/instance/ScreenshotsPage.h +++ b/launcher/ui/pages/instance/ScreenshotsPage.h @@ -42,6 +42,7 @@ class QFileSystemModel; class QIdentityProxyModel; +class QItemSelection; namespace Ui { class ScreenshotsPage; diff --git a/launcher/ui/pages/instance/ServersPage.cpp b/launcher/ui/pages/instance/ServersPage.cpp index d64bcb76..a4f9f330 100644 --- a/launcher/ui/pages/instance/ServersPage.cpp +++ b/launcher/ui/pages/instance/ServersPage.cpp @@ -48,6 +48,7 @@ #include #include +#include static const int COLUMN_COUNT = 2; // 3 , TBD: latency and other nice things. diff --git a/launcher/ui/pages/instance/WorldListPage.cpp b/launcher/ui/pages/instance/WorldListPage.cpp index 85cc01ff..7819d077 100644 --- a/launcher/ui/pages/instance/WorldListPage.cpp +++ b/launcher/ui/pages/instance/WorldListPage.cpp @@ -43,9 +43,9 @@ #include #include #include +#include #include #include -#include #include #include "tools/MCEditTool.h" diff --git a/launcher/ui/pages/modplatform/legacy_ftb/ListModel.cpp b/launcher/ui/pages/modplatform/legacy_ftb/ListModel.cpp index 6b1f6b89..2343b79f 100644 --- a/launcher/ui/pages/modplatform/legacy_ftb/ListModel.cpp +++ b/launcher/ui/pages/modplatform/legacy_ftb/ListModel.cpp @@ -35,6 +35,8 @@ #include "ListModel.h" #include "Application.h" +#include "net/HttpMetaCache.h" +#include "net/NetJob.h" #include "StringUtils.h" #include diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h index 3be377a1..6e6be4b9 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h @@ -38,6 +38,7 @@ #include #include "modplatform/modrinth/ModrinthPackManifest.h" +#include "net/NetJob.h" #include "ui/pages/modplatform/modrinth/ModrinthPage.h" class ModPage; diff --git a/launcher/updater/DownloadTask.cpp b/launcher/updater/DownloadTask.cpp deleted file mode 100644 index 48fe767a..00000000 --- a/launcher/updater/DownloadTask.cpp +++ /dev/null @@ -1,177 +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 "DownloadTask.h" - -#include "updater/UpdateChecker.h" -#include "GoUpdate.h" -#include "net/NetJob.h" - -#include -#include -#include - -namespace GoUpdate -{ - -DownloadTask::DownloadTask( - shared_qobject_ptr network, - Status status, - QString target, - QObject *parent -) : Task(parent), m_updateFilesDir(target), m_network(network) -{ - m_status = status; - - m_updateFilesDir.setAutoRemove(false); -} - -void DownloadTask::executeTask() -{ - loadVersionInfo(); -} - -void DownloadTask::loadVersionInfo() -{ - setStatus(tr("Loading version information...")); - - NetJob *netJob = new NetJob("Version Info", m_network); - - // Find the index URL. - QUrl newIndexUrl = QUrl(m_status.newRepoUrl).resolved(QString::number(m_status.newVersionId) + ".json"); - qDebug() << m_status.newRepoUrl << " turns into " << newIndexUrl; - - netJob->addNetAction(m_newVersionFileListDownload = Net::Download::makeByteArray(newIndexUrl, &newVersionFileListData)); - - // If we have a current version URL, get that one too. - if (!m_status.currentRepoUrl.isEmpty()) - { - QUrl cIndexUrl = QUrl(m_status.currentRepoUrl).resolved(QString::number(m_status.currentVersionId) + ".json"); - netJob->addNetAction(m_currentVersionFileListDownload = Net::Download::makeByteArray(cIndexUrl, ¤tVersionFileListData)); - qDebug() << m_status.currentRepoUrl << " turns into " << cIndexUrl; - } - - // connect signals and start the job - connect(netJob, &NetJob::succeeded, this, &DownloadTask::processDownloadedVersionInfo); - connect(netJob, &NetJob::failed, this, &DownloadTask::vinfoDownloadFailed); - m_vinfoNetJob.reset(netJob); - netJob->start(); -} - -void DownloadTask::vinfoDownloadFailed() -{ - // Something failed. We really need the second download (current version info), so parse - // downloads anyways as long as the first one succeeded. - if (m_newVersionFileListDownload->wasSuccessful()) - { - processDownloadedVersionInfo(); - return; - } - - // TODO: Give a more detailed error message. - qCritical() << "Failed to download version info files."; - emitFailed(tr("Failed to download version info files.")); -} - -void DownloadTask::processDownloadedVersionInfo() -{ - VersionFileList m_currentVersionFileList; - VersionFileList m_newVersionFileList; - - setStatus(tr("Reading file list for new version...")); - qDebug() << "Reading file list for new version..."; - QString error; - if (!parseVersionInfo(newVersionFileListData, m_newVersionFileList, error)) - { - qCritical() << error; - emitFailed(error); - return; - } - - // if we have the current version info, use it. - if (m_currentVersionFileListDownload && m_currentVersionFileListDownload->wasSuccessful()) - { - setStatus(tr("Reading file list for current version...")); - qDebug() << "Reading file list for current version..."; - // if this fails, it's not a complete loss. - QString error; - if(!parseVersionInfo( currentVersionFileListData, m_currentVersionFileList, error)) - { - qDebug() << error << "This is not a fatal error."; - } - } - - // We don't need this any more. - m_currentVersionFileListDownload.reset(); - m_newVersionFileListDownload.reset(); - m_vinfoNetJob.reset(); - - setStatus(tr("Processing file lists - figuring out how to install the update...")); - - // make a new netjob for the actual update files - NetJob::Ptr netJob = new NetJob("Update Files", m_network); - - // fill netJob and operationList - if (!processFileLists(m_currentVersionFileList, m_newVersionFileList, m_status.rootPath, m_updateFilesDir.path(), netJob, m_operations)) - { - emitFailed(tr("Failed to process update lists...")); - return; - } - - // Now start the download. - QObject::connect(netJob.get(), &NetJob::succeeded, this, &DownloadTask::fileDownloadFinished); - QObject::connect(netJob.get(), &NetJob::progress, this, &DownloadTask::fileDownloadProgressChanged); - QObject::connect(netJob.get(), &NetJob::failed, this, &DownloadTask::fileDownloadFailed); - - if(netJob->size() == 1) // Translation issues... see https://github.com/MultiMC/Launcher/issues/1701 - { - setStatus(tr("Downloading one update file.")); - } - else - { - setStatus(tr("Downloading %1 update files.").arg(QString::number(netJob->size()))); - } - qDebug() << "Begin downloading update files to" << m_updateFilesDir.path(); - m_filesNetJob = netJob; - m_filesNetJob->start(); -} - -void DownloadTask::fileDownloadFinished() -{ - emitSucceeded(); -} - -void DownloadTask::fileDownloadFailed(QString reason) -{ - qCritical() << "Failed to download update files:" << reason; - emitFailed(tr("Failed to download update files: %1").arg(reason)); -} - -void DownloadTask::fileDownloadProgressChanged(qint64 current, qint64 total) -{ - setProgress(current, total); -} - -QString DownloadTask::updateFilesDir() -{ - return m_updateFilesDir.path(); -} - -OperationList DownloadTask::operations() -{ - return m_operations; -} - -} diff --git a/launcher/updater/DownloadTask.h b/launcher/updater/DownloadTask.h deleted file mode 100644 index 19a6265c..00000000 --- a/launcher/updater/DownloadTask.h +++ /dev/null @@ -1,100 +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 "tasks/Task.h" -#include "net/NetJob.h" -#include "GoUpdate.h" - -namespace GoUpdate -{ -/*! - * The DownloadTask is a task that takes a given version ID and repository URL, - * downloads that version's files from the repository, and prepares to install them. - */ -class DownloadTask : public Task -{ - Q_OBJECT - -public: - /** - * Create a download task - * - * target is a template - XXXXXX at the end will be replaced with a random generated string, ensuring uniqueness - */ - explicit DownloadTask(shared_qobject_ptr network, Status status, QString target, QObject* parent = 0); - virtual ~DownloadTask() {}; - - /// Get the directory that will contain the update files. - QString updateFilesDir(); - - /// Get the list of operations that should be done - OperationList operations(); - - /// set updater download behavior - void setUseLocalUpdater(bool useLocal); - -protected: - //! Entry point for tasks. - virtual void executeTask() override; - - /*! - * Downloads the version info files from the repository. - * The files for both the current build, and the build that we're updating to need to be downloaded. - * If the current version's info file can't be found, Prism Launcher will not delete files that - * were removed between versions. It will still replace files that have changed, however. - * Note that although the repository URL for the current version is not given to the update task, - * the task will attempt to look it up in the UpdateChecker's channel list. - * If an error occurs here, the function will call emitFailed and return false. - */ - void loadVersionInfo(); - - NetJob::Ptr m_vinfoNetJob; - QByteArray currentVersionFileListData; - QByteArray newVersionFileListData; - Net::Download::Ptr m_currentVersionFileListDownload; - Net::Download::Ptr m_newVersionFileListDownload; - - NetJob::Ptr m_filesNetJob; - - Status m_status; - - OperationList m_operations; - - /*! - * Temporary directory to store update files in. - * This will be set to not auto delete. Task will fail if this fails to be created. - */ - QTemporaryDir m_updateFilesDir; - -protected slots: - /*! - * This function is called when version information is finished downloading - * and at least the new file list download succeeded - */ - void processDownloadedVersionInfo(); - void vinfoDownloadFailed(); - - void fileDownloadFinished(); - void fileDownloadFailed(QString reason); - void fileDownloadProgressChanged(qint64 current, qint64 total); - -private: - shared_qobject_ptr m_network; -}; - -} - diff --git a/launcher/updater/GoUpdate.cpp b/launcher/updater/GoUpdate.cpp deleted file mode 100644 index 4bc7dfa9..00000000 --- a/launcher/updater/GoUpdate.cpp +++ /dev/null @@ -1,198 +0,0 @@ -#include "GoUpdate.h" -#include -#include -#include -#include - -#include "net/Download.h" -#include "net/ChecksumValidator.h" - -namespace GoUpdate -{ - -bool parseVersionInfo(const QByteArray &data, VersionFileList &list, QString &error) -{ - QJsonParseError jsonError; - QJsonDocument jsonDoc = QJsonDocument::fromJson(data, &jsonError); - if (jsonError.error != QJsonParseError::NoError) - { - error = QString("Failed to parse version info JSON: %1 at %2") - .arg(jsonError.errorString()) - .arg(jsonError.offset); - qCritical() << error; - return false; - } - - QJsonObject json = jsonDoc.object(); - - qDebug() << data; - qDebug() << "Loading version info from JSON."; - QJsonArray filesArray = json.value("Files").toArray(); - for (QJsonValue fileValue : filesArray) - { - QJsonObject fileObj = fileValue.toObject(); - - QString file_path = fileObj.value("Path").toString(); - - VersionFileEntry file{file_path, fileObj.value("Perms").toVariant().toInt(), - FileSourceList(), fileObj.value("MD5").toString(), }; - qDebug() << "File" << file.path << "with perms" << file.mode; - - QJsonArray sourceArray = fileObj.value("Sources").toArray(); - for (QJsonValue val : sourceArray) - { - QJsonObject sourceObj = val.toObject(); - - QString type = sourceObj.value("SourceType").toString(); - if (type == "http") - { - file.sources.append(FileSource("http", sourceObj.value("Url").toString())); - } - else - { - qWarning() << "Unknown source type" << type << "ignored."; - } - } - - qDebug() << "Loaded info for" << file.path; - - list.append(file); - } - - return true; -} - -bool processFileLists -( - const VersionFileList ¤tVersion, - const VersionFileList &newVersion, - const QString &rootPath, - const QString &tempPath, - NetJob::Ptr job, - OperationList &ops -) -{ - // First, if we've loaded the current version's file list, we need to iterate through it and - // delete anything in the current one version's list that isn't in the new version's list. - for (VersionFileEntry entry : currentVersion) - { - QFileInfo toDelete(FS::PathCombine(rootPath, entry.path)); - if (!toDelete.exists()) - { - qCritical() << "Expected file " << toDelete.absoluteFilePath() - << " doesn't exist!"; - } - bool keep = false; - - // - for (VersionFileEntry newEntry : newVersion) - { - if (newEntry.path == entry.path) - { - qDebug() << "Not deleting" << entry.path - << "because it is still present in the new version."; - keep = true; - break; - } - } - - // If the loop reaches the end and we didn't find a match, delete the file. - if (!keep) - { - if (toDelete.exists()) - ops.append(Operation::DeleteOp(entry.path)); - } - } - - // Next, check each file in Prism Launcher's folder and see if we need to update them. - for (VersionFileEntry entry : newVersion) - { - // TODO: Let's not MD5sum a ton of files on the GUI thread. We should probably find a - // way to do this in the background. - QString fileMD5; - QString realEntryPath = FS::PathCombine(rootPath, entry.path); - QFile entryFile(realEntryPath); - QFileInfo entryInfo(realEntryPath); - - bool needs_upgrade = false; - if (!entryFile.exists()) - { - needs_upgrade = true; - } - else - { - bool pass = true; - if (!entryInfo.isReadable()) - { - qCritical() << "File " << realEntryPath << " is not readable."; - pass = false; - } - if (!entryInfo.isWritable()) - { - qCritical() << "File " << realEntryPath << " is not writable."; - pass = false; - } - if (!entryFile.open(QFile::ReadOnly)) - { - qCritical() << "File " << realEntryPath << " cannot be opened for reading."; - pass = false; - } - if (!pass) - { - ops.clear(); - return false; - } - } - - if(!needs_upgrade) - { - QCryptographicHash hash(QCryptographicHash::Md5); - auto foo = entryFile.readAll(); - - hash.addData(foo); - fileMD5 = hash.result().toHex(); - if ((fileMD5 != entry.md5)) - { - qDebug() << "MD5Sum does not match!"; - qDebug() << "Expected:'" << entry.md5 << "'"; - qDebug() << "Got: '" << fileMD5 << "'"; - needs_upgrade = true; - } - } - - // skip file. it doesn't need an upgrade. - if (!needs_upgrade) - { - qDebug() << "File" << realEntryPath << " does not need updating."; - continue; - } - - // yep. this file actually needs an upgrade. PROCEED. - qDebug() << "Found file" << realEntryPath << " that needs updating."; - - // Go through the sources list and find one to use. - // TODO: Make a NetAction that takes a source list and tries each of them until one - // works. For now, we'll just use the first http one. - for (FileSource source : entry.sources) - { - if (source.type != "http") - continue; - - qDebug() << "Will download" << entry.path << "from" << source.url; - - // Download it to updatedir/- where filepath is the file's - // path with slashes replaced by underscores. - QString dlPath = FS::PathCombine(tempPath, QString(entry.path).replace("/", "_")); - - // We need to download the file to the updatefiles folder and add a task - // to copy it to its install path. - auto download = Net::Download::makeFile(source.url, dlPath); - auto rawMd5 = QByteArray::fromHex(entry.md5.toLatin1()); - download->addValidator(new Net::ChecksumValidator(QCryptographicHash::Md5, rawMd5)); - job->addNetAction(download); - ops.append(Operation::CopyOp(dlPath, entry.path, entry.mode)); - } - } - return true; -} -} diff --git a/launcher/updater/GoUpdate.h b/launcher/updater/GoUpdate.h deleted file mode 100644 index 46a679ef..00000000 --- a/launcher/updater/GoUpdate.h +++ /dev/null @@ -1,125 +0,0 @@ -#pragma once -#include -#include - -namespace GoUpdate -{ - -/** - * A temporary object exchanged between updated checker and the actual update task - */ -struct Status -{ - bool updateAvailable = false; - - int newVersionId = -1; - QString newRepoUrl; - - int currentVersionId = -1; - QString currentRepoUrl; - - // path to the root of the application - QString rootPath; -}; - -/** - * Struct that describes an entry in a VersionFileEntry's `Sources` list. - */ -struct FileSource -{ - FileSource(QString type, QString url, QString compression="") - { - this->type = type; - this->url = url; - this->compressionType = compression; - } - - bool operator==(const FileSource &f2) const - { - return type == f2.type && url == f2.url && compressionType == f2.compressionType; - } - - QString type; - QString url; - QString compressionType; -}; -typedef QList FileSourceList; - -/** - * Structure that describes an entry in a GoUpdate version's `Files` list. - */ -struct VersionFileEntry -{ - QString path; - int mode; - FileSourceList sources; - QString md5; - bool operator==(const VersionFileEntry &v2) const - { - return path == v2.path && mode == v2.mode && sources == v2.sources && md5 == v2.md5; - } -}; -typedef QList VersionFileList; - -/** - * Structure that describes an operation to perform when installing updates. - */ -struct Operation -{ - static Operation CopyOp(QString from, QString to, int fmode=0644) - { - return Operation{OP_REPLACE, from, to, fmode}; - } - static Operation DeleteOp(QString file) - { - return Operation{OP_DELETE, QString(), file, 0644}; - } - - // FIXME: for some types, some of the other fields are irrelevant! - bool operator==(const Operation &u2) const - { - return type == u2.type && - source == u2.source && - destination == u2.destination && - destinationMode == u2.destinationMode; - } - - //! Specifies the type of operation that this is. - enum Type - { - OP_REPLACE, - OP_DELETE, - } type; - - //! The source file, if any - QString source; - - //! The destination file. - QString destination; - - //! The mode to change the destination file to. - int destinationMode; -}; -typedef QList OperationList; - -/** - * Loads the file list from the given version info JSON object into the given list. - */ -bool parseVersionInfo(const QByteArray &data, VersionFileList& list, QString &error); - -/*! - * Takes a list of file entries for the current version's files and the new version's files - * and populates the downloadList and operationList with information about how to download and install the update. - */ -bool processFileLists -( - const VersionFileList ¤tVersion, - const VersionFileList &newVersion, - const QString &rootPath, - const QString &tempPath, - NetJob::Ptr job, - OperationList &ops -); - -} -Q_DECLARE_METATYPE(GoUpdate::Status) diff --git a/launcher/updater/MacSparkleUpdater.h b/launcher/updater/MacSparkleUpdater.h index d50dbd68..cee19f7c 100644 --- a/launcher/updater/MacSparkleUpdater.h +++ b/launcher/updater/MacSparkleUpdater.h @@ -119,8 +119,6 @@ private: class Private; Private *priv; - - void loadChannelsFromSettings(); }; #endif //LAUNCHER_MACSPARKLEUPDATER_H diff --git a/launcher/updater/MacSparkleUpdater.mm b/launcher/updater/MacSparkleUpdater.mm index ca6da55a..07337176 100644 --- a/launcher/updater/MacSparkleUpdater.mm +++ b/launcher/updater/MacSparkleUpdater.mm @@ -106,8 +106,6 @@ MacSparkleUpdater::MacSparkleUpdater() priv->updaterObserver.callback = ^(bool canCheck) { emit canCheckForUpdatesChanged(canCheck); }; - - loadChannelsFromSettings(); } MacSparkleUpdater::~MacSparkleUpdater() @@ -165,7 +163,6 @@ void MacSparkleUpdater::setUpdateCheckInterval(double seconds) void MacSparkleUpdater::clearAllowedChannels() { priv->updaterDelegate.allowedChannels = [NSSet set]; - APPLICATION->settings()->set("UpdateChannel", ""); } void MacSparkleUpdater::setAllowedChannel(const QString &channel) @@ -178,7 +175,6 @@ void MacSparkleUpdater::setAllowedChannel(const QString &channel) NSSet *nsChannels = [NSSet setWithObject:channel.toNSString()]; priv->updaterDelegate.allowedChannels = nsChannels; - APPLICATION->settings()->set("UpdateChannel", channel); } void MacSparkleUpdater::setAllowedChannels(const QSet &channels) @@ -199,7 +195,6 @@ void MacSparkleUpdater::setAllowedChannels(const QSet &channels) } priv->updaterDelegate.allowedChannels = nsChannels; - APPLICATION->settings()->set("UpdateChannel", channelsConfig.trimmed()); } void MacSparkleUpdater::setBetaAllowed(bool allowed) @@ -213,10 +208,3 @@ void MacSparkleUpdater::setBetaAllowed(bool allowed) clearAllowedChannels(); } } - -void MacSparkleUpdater::loadChannelsFromSettings() -{ - QStringList channelList = APPLICATION->settings()->get("UpdateChannel").toString().split(" "); - QSet channels(channelList.begin(), channelList.end()); - setAllowedChannels(channels); -} diff --git a/launcher/updater/UpdateChecker.cpp b/launcher/updater/UpdateChecker.cpp deleted file mode 100644 index 78d979ff..00000000 --- a/launcher/updater/UpdateChecker.cpp +++ /dev/null @@ -1,296 +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 "UpdateChecker.h" - -#include -#include -#include -#include - -#define API_VERSION 0 -#define CHANLIST_FORMAT 0 - -#include "BuildConfig.h" - -UpdateChecker::UpdateChecker(shared_qobject_ptr nam, QString channelUrl, QString currentChannel) -{ - m_network = nam; - m_channelUrl = channelUrl; - m_currentChannel = currentChannel; - -#ifdef Q_OS_MAC - m_externalUpdater = new MacSparkleUpdater(); -#endif -} - -QList UpdateChecker::getChannelList() const -{ - return m_channels; -} - -bool UpdateChecker::hasChannels() const -{ - return !m_channels.isEmpty(); -} - -ExternalUpdater* UpdateChecker::getExternalUpdater() -{ - return m_externalUpdater; -} - -void UpdateChecker::checkForUpdate(const QString& updateChannel, bool notifyNoUpdate) -{ - if (m_externalUpdater) - { - m_externalUpdater->setBetaAllowed(updateChannel == "beta"); - if (notifyNoUpdate) - { - qDebug() << "Checking for updates."; - m_externalUpdater->checkForUpdates(); - } else - { - // The updater library already handles automatic update checks. - return; - } - } - else - { - qDebug() << "Checking for updates."; - // If the channel list hasn't loaded yet, load it and defer checking for updates until - // later. - if (!m_chanListLoaded) - { - qDebug() << "Channel list isn't loaded yet. Loading channel list and deferring update check."; - m_checkUpdateWaiting = true; - m_deferredUpdateChannel = updateChannel; - updateChanList(notifyNoUpdate); - return; - } - - if (m_updateChecking) - { - qDebug() << "Ignoring update check request. Already checking for updates."; - return; - } - - // Find the desired channel within the channel list and get its repo URL. If if cannot be - // found, error. - QString stableUrl; - m_newRepoUrl = ""; - for (ChannelListEntry entry: m_channels) - { - qDebug() << "channelEntry = " << entry.id; - if (entry.id == "stable") - { - stableUrl = entry.url; - } - if (entry.id == updateChannel) - { - m_newRepoUrl = entry.url; - qDebug() << "is intended update channel: " << entry.id; - } - if (entry.id == m_currentChannel) - { - m_currentRepoUrl = entry.url; - qDebug() << "is current update channel: " << entry.id; - } - } - - qDebug() << "m_repoUrl = " << m_newRepoUrl; - - if (m_newRepoUrl.isEmpty()) - { - qWarning() << "m_repoUrl was empty. defaulting to 'stable': " << stableUrl; - m_newRepoUrl = stableUrl; - } - - // If nothing applies, error - if (m_newRepoUrl.isEmpty()) - { - qCritical() << "failed to select any update repository for: " << updateChannel; - emit updateCheckFailed(); - return; - } - - m_updateChecking = true; - - QUrl indexUrl = QUrl(m_newRepoUrl).resolved(QUrl("index.json")); - - indexJob = new NetJob("GoUpdate Repository Index", m_network); - indexJob->addNetAction(Net::Download::makeByteArray(indexUrl, &indexData)); - connect(indexJob.get(), &NetJob::succeeded, [this, notifyNoUpdate]() { updateCheckFinished(notifyNoUpdate); }); - connect(indexJob.get(), &NetJob::failed, this, &UpdateChecker::updateCheckFailed); - indexJob->start(); - } -} - -void UpdateChecker::updateCheckFinished(bool notifyNoUpdate) -{ - qDebug() << "Finished downloading repo index. Checking for new versions."; - - QJsonParseError jsonError; - indexJob.reset(); - - QJsonDocument jsonDoc = QJsonDocument::fromJson(indexData, &jsonError); - indexData.clear(); - if (jsonError.error != QJsonParseError::NoError || !jsonDoc.isObject()) - { - qCritical() << "Failed to parse GoUpdate repository index. JSON error" - << jsonError.errorString() << "at offset" << jsonError.offset; - m_updateChecking = false; - return; - } - - QJsonObject object = jsonDoc.object(); - - bool success = false; - int apiVersion = object.value("ApiVersion").toVariant().toInt(&success); - if (apiVersion != API_VERSION || !success) - { - qCritical() << "Failed to check for updates. API version mismatch. We're using" - << API_VERSION << "server has" << apiVersion; - m_updateChecking = false; - return; - } - - qDebug() << "Processing repository version list."; - QJsonObject newestVersion; - QJsonArray versions = object.value("Versions").toArray(); - for (QJsonValue versionVal : versions) - { - QJsonObject version = versionVal.toObject(); - if (newestVersion.value("Id").toVariant().toInt() < - version.value("Id").toVariant().toInt()) - { - newestVersion = version; - } - } - - // We've got the version with the greatest ID number. Now compare it to our current build - // number and update if they're different. - int newBuildNumber = newestVersion.value("Id").toVariant().toInt(); - if (newBuildNumber != m_currentBuild) - { - qDebug() << "Found newer version with ID" << newBuildNumber; - // Update! - GoUpdate::Status updateStatus; - updateStatus.updateAvailable = true; - updateStatus.currentVersionId = m_currentBuild; - updateStatus.currentRepoUrl = m_currentRepoUrl; - updateStatus.newVersionId = newBuildNumber; - updateStatus.newRepoUrl = m_newRepoUrl; - emit updateAvailable(updateStatus); - } - else if (notifyNoUpdate) - { - emit noUpdateFound(); - } - m_updateChecking = false; -} - -void UpdateChecker::updateCheckFailed() -{ - qCritical() << "Update check failed for reasons unknown."; -} - -void UpdateChecker::updateChanList(bool notifyNoUpdate) -{ - qDebug() << "Loading the channel list."; - - if (m_chanListLoading) - { - qDebug() << "Ignoring channel list update request. Already grabbing channel list."; - return; - } - - m_chanListLoading = true; - chanListJob = new NetJob("Update System Channel List", m_network); - chanListJob->addNetAction(Net::Download::makeByteArray(QUrl(m_channelUrl), &chanlistData)); - connect(chanListJob.get(), &NetJob::succeeded, [this, notifyNoUpdate]() { chanListDownloadFinished(notifyNoUpdate); }); - connect(chanListJob.get(), &NetJob::failed, this, &UpdateChecker::chanListDownloadFailed); - chanListJob->start(); -} - -void UpdateChecker::chanListDownloadFinished(bool notifyNoUpdate) -{ - chanListJob.reset(); - - QJsonParseError jsonError; - QJsonDocument jsonDoc = QJsonDocument::fromJson(chanlistData, &jsonError); - chanlistData.clear(); - if (jsonError.error != QJsonParseError::NoError) - { - // TODO: Report errors to the user. - qCritical() << "Failed to parse channel list JSON:" << jsonError.errorString() << "at" << jsonError.offset; - m_chanListLoading = false; - return; - } - - QJsonObject object = jsonDoc.object(); - - bool success = false; - int formatVersion = object.value("format_version").toVariant().toInt(&success); - if (formatVersion != CHANLIST_FORMAT || !success) - { - qCritical() - << "Failed to check for updates. Channel list format version mismatch. We're using" - << CHANLIST_FORMAT << "server has" << formatVersion; - m_chanListLoading = false; - return; - } - - // Load channels into a temporary array. - QList loadedChannels; - QJsonArray channelArray = object.value("channels").toArray(); - for (QJsonValue chanVal : channelArray) - { - QJsonObject channelObj = chanVal.toObject(); - ChannelListEntry entry { - channelObj.value("id").toVariant().toString(), - channelObj.value("name").toVariant().toString(), - channelObj.value("description").toVariant().toString(), - channelObj.value("url").toVariant().toString() - }; - if (entry.id.isEmpty() || entry.name.isEmpty() || entry.url.isEmpty()) - { - qCritical() << "Channel list entry with empty ID, name, or URL. Skipping."; - continue; - } - loadedChannels.append(entry); - } - - // Swap the channel list we just loaded into the object's channel list. - m_channels.swap(loadedChannels); - - m_chanListLoading = false; - m_chanListLoaded = true; - qDebug() << "Successfully loaded UpdateChecker channel list."; - - // If we're waiting to check for updates, do that now. - if (m_checkUpdateWaiting) { - checkForUpdate(m_deferredUpdateChannel, notifyNoUpdate); - } - - emit channelListLoaded(); -} - -void UpdateChecker::chanListDownloadFailed(QString reason) -{ - m_chanListLoading = false; - qCritical() << QString("Failed to download channel list: %1").arg(reason); - emit channelListLoaded(); -} - diff --git a/launcher/updater/UpdateChecker.h b/launcher/updater/UpdateChecker.h deleted file mode 100644 index 42ef318b..00000000 --- a/launcher/updater/UpdateChecker.h +++ /dev/null @@ -1,140 +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 "net/NetJob.h" -#include "GoUpdate.h" -#include "ExternalUpdater.h" - -#ifdef Q_OS_MAC -#include "MacSparkleUpdater.h" -#endif - -class UpdateChecker : public QObject -{ - Q_OBJECT - -public: - UpdateChecker(shared_qobject_ptr nam, QString channelUrl, QString currentChannel); - void checkForUpdate(const QString& updateChannel, bool notifyNoUpdate); - - /*! - * Causes the update checker to download the channel list from the URL specified in config.h (generated by CMake). - * If this isn't called before checkForUpdate(), it will automatically be called. - */ - void updateChanList(bool notifyNoUpdate); - - /*! - * An entry in the channel list. - */ - struct ChannelListEntry - { - QString id; - QString name; - QString description; - QString url; - }; - - /*! - * Returns a the current channel list. - * If the channel list hasn't been loaded, this list will be empty. - */ - QList getChannelList() const; - - /*! - * Returns false if the channel list is empty. - */ - bool hasChannels() const; - - /*! - * Returns a pointer to an object that controls the external updater, or nullptr if an external updater is not used. - */ - ExternalUpdater *getExternalUpdater(); - -signals: - //! Signal emitted when an update is available. Passes the URL for the repo and the ID and name for the version. - void updateAvailable(GoUpdate::Status status); - - //! Signal emitted when the channel list finishes loading or fails to load. - void channelListLoaded(); - - void noUpdateFound(); - -private slots: - void updateCheckFinished(bool notifyNoUpdate); - void updateCheckFailed(); - - void chanListDownloadFinished(bool notifyNoUpdate); - void chanListDownloadFailed(QString reason); - -private: - friend class UpdateCheckerTest; - - shared_qobject_ptr m_network; - - NetJob::Ptr indexJob; - QByteArray indexData; - NetJob::Ptr chanListJob; - QByteArray chanlistData; - - QString m_channelUrl; - - QList m_channels; - - /*! - * True while the system is checking for updates. - * If checkForUpdate is called while this is true, it will be ignored. - */ - bool m_updateChecking = false; - - /*! - * True if the channel list has loaded. - * If this is false, trying to check for updates will call updateChanList first. - */ - bool m_chanListLoaded = false; - - /*! - * Set to true while the channel list is currently loading. - */ - bool m_chanListLoading = false; - - /*! - * Set to true when checkForUpdate is called while the channel list isn't loaded. - * When the channel list finishes loading, if this is true, the update checker will check for updates. - */ - bool m_checkUpdateWaiting = false; - - /*! - * if m_checkUpdateWaiting, this is the last used update channel - */ - QString m_deferredUpdateChannel; - - int m_currentBuild = -1; - QString m_currentChannel; - QString m_currentRepoUrl; - - QString m_newRepoUrl; - - /*! - * If not a nullptr, then the updater here will be used instead of the old updater that uses GoUpdate when - * checking for updates. - * - * As a result, signals from this class won't be emitted, and most of the functions in this class other - * than checkForUpdate are not useful. Call functions from this external updater object instead. - */ - ExternalUpdater *m_externalUpdater = nullptr; -}; - From 127b094c4158f7a2315bb35cea05f5644a0db1c5 Mon Sep 17 00:00:00 2001 From: TheKodeToad Date: Wed, 14 Dec 2022 15:02:04 +0000 Subject: [PATCH 002/152] Improve handling of destructive actions Signed-off-by: TheKodeToad --- launcher/FileSystem.cpp | 5 +- launcher/FileSystem.h | 5 +- launcher/minecraft/World.cpp | 7 ++- launcher/minecraft/mod/Resource.cpp | 4 ++ launcher/ui/GuiUtil.cpp | 29 ++++++++++- launcher/ui/GuiUtil.h | 2 +- launcher/ui/MainWindow.cpp | 39 ++++++++------- .../pages/instance/ExternalResourcesPage.cpp | 48 +++++++++++++++++-- .../ui/pages/instance/ExternalResourcesPage.h | 3 +- launcher/ui/pages/instance/LogPage.cpp | 30 +++++------- launcher/ui/pages/instance/ModFolderPage.cpp | 10 ++-- launcher/ui/pages/instance/ModFolderPage.h | 5 +- launcher/ui/pages/instance/OtherLogsPage.cpp | 31 ++++++++---- .../ui/pages/instance/ScreenshotsPage.cpp | 48 ++++++++++++++++--- launcher/ui/pages/instance/ServersPage.cpp | 15 +++++- launcher/ui/pages/instance/WorldListPage.cpp | 18 ++++--- launcher/ui/pages/instance/WorldListPage.ui | 2 +- 17 files changed, 218 insertions(+), 83 deletions(-) diff --git a/launcher/FileSystem.cpp b/launcher/FileSystem.cpp index 3e8e10a5..b3af4f4e 100644 --- a/launcher/FileSystem.cpp +++ b/launcher/FileSystem.cpp @@ -1,7 +1,8 @@ // SPDX-License-Identifier: GPL-3.0-only /* - * PolyMC - Minecraft Launcher + * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu + * Copyright (C) 2022 TheKodeToad * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -226,7 +227,7 @@ bool deletePath(QString path) return err.value() == 0; } -bool trash(QString path, QString *pathInTrash = nullptr) +bool trash(QString path, QString *pathInTrash) { #if QT_VERSION < QT_VERSION_CHECK(5, 15, 0) return false; diff --git a/launcher/FileSystem.h b/launcher/FileSystem.h index ac893725..15233b66 100644 --- a/launcher/FileSystem.h +++ b/launcher/FileSystem.h @@ -1,7 +1,8 @@ // SPDX-License-Identifier: GPL-3.0-only /* - * PolyMC - Minecraft Launcher + * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu + * Copyright (C) 2022 TheKodeToad * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -129,7 +130,7 @@ bool deletePath(QString path); /** * Trash a folder / file */ -bool trash(QString path, QString *pathInTrash); +bool trash(QString path, QString *pathInTrash = nullptr); QString PathCombine(const QString& path1, const QString& path2); QString PathCombine(const QString& path1, const QString& path2, const QString& path3); diff --git a/launcher/minecraft/World.cpp b/launcher/minecraft/World.cpp index 90fcf337..d310f8b9 100644 --- a/launcher/minecraft/World.cpp +++ b/launcher/minecraft/World.cpp @@ -1,7 +1,8 @@ // SPDX-License-Identifier: GPL-3.0-only /* - * PolyMC - Minecraft Launcher + * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu + * Copyright (C) 2022 TheKodeToad * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -545,6 +546,10 @@ bool World::replace(World &with) bool World::destroy() { if(!is_valid) return false; + + if (FS::trash(m_containerFile.filePath())) + return true; + if (m_containerFile.isDir()) { QDir d(m_containerFile.filePath()); diff --git a/launcher/minecraft/mod/Resource.cpp b/launcher/minecraft/mod/Resource.cpp index 0fbcfd7c..7c572d92 100644 --- a/launcher/minecraft/mod/Resource.cpp +++ b/launcher/minecraft/mod/Resource.cpp @@ -143,5 +143,9 @@ bool Resource::enable(EnableAction action) bool Resource::destroy() { m_type = ResourceType::UNKNOWN; + + if (FS::trash(m_file_info.filePath())) + return true; + return FS::deletePath(m_file_info.filePath()); } diff --git a/launcher/ui/GuiUtil.cpp b/launcher/ui/GuiUtil.cpp index 5a62e4d0..241354cb 100644 --- a/launcher/ui/GuiUtil.cpp +++ b/launcher/ui/GuiUtil.cpp @@ -1,8 +1,9 @@ // SPDX-License-Identifier: GPL-3.0-only /* - * PolyMC - Minecraft Launcher + * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Lenny McLennington * Copyright (C) 2022 Sefa Eyeoglu + * Copyright (C) 2022 TheKodeToad * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -49,11 +50,35 @@ #include #include -QString GuiUtil::uploadPaste(const QString &text, QWidget *parentWidget) +QString GuiUtil::uploadPaste(const QString &name, const QString &text, QWidget *parentWidget) { ProgressDialog dialog(parentWidget); auto pasteTypeSetting = static_cast(APPLICATION->settings()->get("PastebinType").toInt()); auto pasteCustomAPIBaseSetting = APPLICATION->settings()->get("PastebinCustomAPIBase").toString(); + + { + QUrl baseUrl; + if (pasteCustomAPIBaseSetting.isEmpty()) + baseUrl = PasteUpload::PasteTypes[pasteTypeSetting].defaultBase; + else + baseUrl = pasteCustomAPIBaseSetting; + + if (baseUrl.isValid()) { + auto response = CustomMessageBox::selectable(parentWidget, "Confirm Upload", + QObject::tr("About to upload: %1\n" + "Uploading to: %2\n" + "You should double-check for personal information.\n\n" + "Are you sure?") + .arg(name) + .arg(baseUrl.host()), + QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No, QMessageBox::No) + ->exec(); + + if (response != QMessageBox::Yes) + return "canceled"; + } + } + std::unique_ptr paste(new PasteUpload(parentWidget, text, pasteCustomAPIBaseSetting, pasteTypeSetting)); dialog.execWithTask(paste.get()); diff --git a/launcher/ui/GuiUtil.h b/launcher/ui/GuiUtil.h index 5e109383..bf93b3c5 100644 --- a/launcher/ui/GuiUtil.h +++ b/launcher/ui/GuiUtil.h @@ -4,7 +4,7 @@ namespace GuiUtil { -QString uploadPaste(const QString &text, QWidget *parentWidget); +QString uploadPaste(const QString &name, const QString &text, QWidget *parentWidget); void setClipboardText(const QString &text); QStringList BrowseForFiles(QString context, QString caption, QString filter, QString defaultPath, QWidget *parentWidget); QString BrowseForFile(QString context, QString caption, QString filter, QString defaultPath, QWidget *parentWidget); diff --git a/launcher/ui/MainWindow.cpp b/launcher/ui/MainWindow.cpp index 2f1976cc..4ddef6d4 100644 --- a/launcher/ui/MainWindow.cpp +++ b/launcher/ui/MainWindow.cpp @@ -1,7 +1,8 @@ // SPDX-License-Identifier: GPL-3.0-only /* - * PolyMC - Minecraft Launcher + * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu + * Copyright (C) 2022 TheKodeToad * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -490,7 +491,7 @@ public: if (!BuildConfig.BUG_TRACKER_URL.isEmpty()) { helpMenu->addAction(actionReportBug); } - + if(!BuildConfig.MATRIX_URL.isEmpty()) { helpMenu->addAction(actionMATRIX); } @@ -2093,21 +2094,23 @@ void MainWindow::on_actionDeleteInstance_triggered() auto id = m_selectedInstance->id(); - auto response = - CustomMessageBox::selectable(this, tr("CAREFUL!"), - tr("About to delete: %1\nThis may be permanent and will completely delete the instance.\n\nAre you sure?") - .arg(m_selectedInstance->name()), - QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No, QMessageBox::No) - ->exec(); + auto response = CustomMessageBox::selectable(this, tr("CAREFUL!"), + tr("About to delete: %1\n" + "This may be permanent and will completely delete the instance.\n\n" + "Are you sure?") + .arg(m_selectedInstance->name()), + QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No, QMessageBox::No) + ->exec(); - if (response == QMessageBox::Yes) { - if (APPLICATION->instances()->trashInstance(id)) { - ui->actionUndoTrashInstance->setEnabled(APPLICATION->instances()->trashedSomething()); - return; - } + if (response != QMessageBox::Yes) + return; - APPLICATION->instances()->deleteInstance(id); + if (APPLICATION->instances()->trashInstance(id)) { + ui->actionUndoTrashInstance->setEnabled(APPLICATION->instances()->trashedSomething()); + return; } + + APPLICATION->instances()->deleteInstance(id); } void MainWindow::on_actionExportInstance_triggered() @@ -2252,7 +2255,7 @@ void MainWindow::on_actionCreateInstanceShortcut_triggered() } QString iconPath = FS::PathCombine(m_selectedInstance->instanceRoot(), "icon.png"); - + QFile iconFile(iconPath); if (!iconFile.open(QFile::WriteOnly)) { @@ -2261,7 +2264,7 @@ void MainWindow::on_actionCreateInstanceShortcut_triggered() } bool success = icon->icon().pixmap(64, 64).save(&iconFile, "PNG"); iconFile.close(); - + if (!success) { iconFile.remove(); @@ -2302,7 +2305,7 @@ void MainWindow::on_actionCreateInstanceShortcut_triggered() } QString iconPath = FS::PathCombine(m_selectedInstance->instanceRoot(), "icon.ico"); - + // part of fix for weird bug involving the window icon being replaced // dunno why it happens, but this 2-line fix seems to be enough, so w/e auto appIcon = APPLICATION->getThemedIcon("logo"); @@ -2325,7 +2328,7 @@ void MainWindow::on_actionCreateInstanceShortcut_triggered() QMessageBox::critical(this, tr("Create instance shortcut"), tr("Failed to create icon for shortcut.")); return; } - + if (FS::createShortcut(FS::PathCombine(desktopPath, m_selectedInstance->name()), QApplication::applicationFilePath(), { "--launch", m_selectedInstance->id() }, m_selectedInstance->name(), iconPath)) { diff --git a/launcher/ui/pages/instance/ExternalResourcesPage.cpp b/launcher/ui/pages/instance/ExternalResourcesPage.cpp index c66d1368..41ccd1db 100644 --- a/launcher/ui/pages/instance/ExternalResourcesPage.cpp +++ b/launcher/ui/pages/instance/ExternalResourcesPage.cpp @@ -1,4 +1,5 @@ #include "ExternalResourcesPage.h" +#include "ui/dialogs/CustomMessageBox.h" #include "ui_ExternalResourcesPage.h" #include "DesktopServices.h" @@ -128,7 +129,7 @@ bool ExternalResourcesPage::eventFilter(QObject* obj, QEvent* ev) { if (ev->type() != QEvent::KeyPress) return QWidget::eventFilter(obj, ev); - + QKeyEvent* keyEvent = static_cast(ev); if (obj == ui->treeView) return listFilter(keyEvent); @@ -140,7 +141,6 @@ void ExternalResourcesPage::addItem() { if (!m_controlsEnabled) return; - auto list = GuiUtil::BrowseForFiles( helpPage(), tr("Select %1", "Select whatever type of files the page contains. Example: 'Loader Mods'").arg(displayName()), @@ -157,8 +157,49 @@ void ExternalResourcesPage::removeItem() { if (!m_controlsEnabled) return; - + auto selection = m_filterModel->mapSelectionToSource(ui->treeView->selectionModel()->selection()); + + int count = 0; + bool folder = false; + for (auto i : selection.indexes()) { + if (i.column() == 0) { + count++; + + // if a folder is selected, show the confirmation dialog + if (m_model->at(i.row()).fileinfo().isDir()) + folder = true; + } + } + + bool enough = count > 1; + + if (enough || folder) { + QString text; + if (enough) + text = tr("About to remove: %1 items\n" + "This may be permanent and they will be gone from the folder.\n\n" + "Are you sure?") + .arg(count); + else + text = tr("About to remove: %1 (folder)\n" + "This may be permanent and it will be gone from the parent folder.\n\n" + "Are you sure?") + .arg(m_model->at(selection.indexes().at(0).row()).fileinfo().fileName()); + + auto response = CustomMessageBox::selectable(this, tr("CAREFUL!"), text, QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No, + QMessageBox::No) + ->exec(); + + if (response != QMessageBox::Yes) + return; + } + + removeItems(selection); +} + +void ExternalResourcesPage::removeItems(const QItemSelection& selection) +{ m_model->deleteResources(selection.indexes()); } @@ -209,4 +250,3 @@ bool ExternalResourcesPage::onSelectionChanged(const QModelIndex& current, const return true; } - diff --git a/launcher/ui/pages/instance/ExternalResourcesPage.h b/launcher/ui/pages/instance/ExternalResourcesPage.h index 2d1a5b51..d17fbb7f 100644 --- a/launcher/ui/pages/instance/ExternalResourcesPage.h +++ b/launcher/ui/pages/instance/ExternalResourcesPage.h @@ -50,7 +50,8 @@ class ExternalResourcesPage : public QMainWindow, public BasePage { void filterTextChanged(const QString& newContents); virtual void addItem(); - virtual void removeItem(); + void removeItem(); + virtual void removeItems(const QItemSelection &selection); virtual void enableItem(); virtual void disableItem(); diff --git a/launcher/ui/pages/instance/LogPage.cpp b/launcher/ui/pages/instance/LogPage.cpp index 31c3e925..2a6504a2 100644 --- a/launcher/ui/pages/instance/LogPage.cpp +++ b/launcher/ui/pages/instance/LogPage.cpp @@ -1,8 +1,9 @@ // SPDX-License-Identifier: GPL-3.0-only /* - * PolyMC - Minecraft Launcher + * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 Jamie Mansfield * Copyright (C) 2022 Sefa Eyeoglu + * Copyright (C) 2022 TheKodeToad * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -277,28 +278,21 @@ void LogPage::on_btnPaste_clicked() //FIXME: turn this into a proper task and move the upload logic out of GuiUtil! m_model->append( MessageLevel::Launcher, - QString("%2: Log upload triggered at: %1").arg( - QDateTime::currentDateTime().toString(Qt::RFC2822Date), - BuildConfig.LAUNCHER_DISPLAYNAME + QString("Log upload triggered at: %1").arg( + QDateTime::currentDateTime().toString(Qt::RFC2822Date) ) ); - auto url = GuiUtil::uploadPaste(m_model->toPlainText(), this); - if(!url.isEmpty()) + auto url = GuiUtil::uploadPaste(tr("Minecraft Log"), m_model->toPlainText(), this); + if(url == "canceled") { - m_model->append( - MessageLevel::Launcher, - QString("%2: Log uploaded to: %1").arg( - url, - BuildConfig.LAUNCHER_DISPLAYNAME - ) - ); + m_model->append(MessageLevel::Error, QString("Log upload canceled")); } - else + else if(!url.isEmpty()) { - m_model->append( - MessageLevel::Error, - QString("%1: Log upload failed!").arg(BuildConfig.LAUNCHER_DISPLAYNAME) - ); + m_model->append(MessageLevel::Launcher, QString("Log uploaded to: %1").arg(url)); + } + else { + m_model->append(MessageLevel::Error, QString("Log upload failed!")); } } diff --git a/launcher/ui/pages/instance/ModFolderPage.cpp b/launcher/ui/pages/instance/ModFolderPage.cpp index 0a2e6155..627e71e5 100644 --- a/launcher/ui/pages/instance/ModFolderPage.cpp +++ b/launcher/ui/pages/instance/ModFolderPage.cpp @@ -1,8 +1,9 @@ // SPDX-License-Identifier: GPL-3.0-only /* - * PolyMC - Minecraft Launcher + * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 Jamie Mansfield * Copyright (C) 2022 Sefa Eyeoglu + * Copyright (C) 2022 TheKodeToad * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -139,13 +140,8 @@ bool ModFolderPage::onSelectionChanged(const QModelIndex& current, const QModelI return true; } -void ModFolderPage::removeItem() +void ModFolderPage::removeItems(const QItemSelection &selection) { - - if (!m_controlsEnabled) - return; - - auto selection = m_filterModel->mapSelectionToSource(ui->treeView->selectionModel()->selection()); m_model->deleteMods(selection.indexes()); } diff --git a/launcher/ui/pages/instance/ModFolderPage.h b/launcher/ui/pages/instance/ModFolderPage.h index f20adf34..ff58b38a 100644 --- a/launcher/ui/pages/instance/ModFolderPage.h +++ b/launcher/ui/pages/instance/ModFolderPage.h @@ -1,8 +1,9 @@ // SPDX-License-Identifier: GPL-3.0-only /* - * PolyMC - Minecraft Launcher + * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 Jamie Mansfield * Copyright (C) 2022 Sefa Eyeoglu + * Copyright (C) 2022 TheKodeToad * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -59,7 +60,7 @@ class ModFolderPage : public ExternalResourcesPage { private slots: void runningStateChanged(bool running); - void removeItem() override; + void removeItems(const QItemSelection &selection) override; void installMods(); void updateMods(); diff --git a/launcher/ui/pages/instance/OtherLogsPage.cpp b/launcher/ui/pages/instance/OtherLogsPage.cpp index 0c1939c6..ad444e6b 100644 --- a/launcher/ui/pages/instance/OtherLogsPage.cpp +++ b/launcher/ui/pages/instance/OtherLogsPage.cpp @@ -1,7 +1,8 @@ // SPDX-License-Identifier: GPL-3.0-only /* - * PolyMC - Minecraft Launcher + * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 Jamie Mansfield + * Copyright (C) 2022 TheKodeToad * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -204,7 +205,7 @@ void OtherLogsPage::on_btnReload_clicked() void OtherLogsPage::on_btnPaste_clicked() { - GuiUtil::uploadPaste(ui->text->toPlainText(), this); + GuiUtil::uploadPaste(m_currentFile, ui->text->toPlainText(), this); } void OtherLogsPage::on_btnCopy_clicked() @@ -219,13 +220,21 @@ void OtherLogsPage::on_btnDelete_clicked() setControlsEnabled(false); return; } - if (QMessageBox::question(this, tr("Delete"), - tr("Do you really want to delete %1?").arg(m_currentFile), - QMessageBox::Yes, QMessageBox::No) == QMessageBox::No) - { + if (QMessageBox::question(this, tr("CAREFUL!"), + tr("About to delete: %1\n" + "This may be permanent and it will be gone from the logs folder.\n\n" + "Are you sure?") + .arg(m_currentFile), + QMessageBox::Yes, QMessageBox::No) == QMessageBox::No) { return; } QFile file(FS::PathCombine(m_path, m_currentFile)); + + if (FS::trash(file.fileName())) + { + return; + } + if (!file.remove()) { QMessageBox::critical(this, tr("Error"), tr("Unable to delete %1: %2") @@ -243,15 +252,15 @@ void OtherLogsPage::on_btnClean_clicked() return; } QMessageBox *messageBox = new QMessageBox(this); - messageBox->setWindowTitle(tr("Clean up")); + messageBox->setWindowTitle(tr("CAREFUL!")); if(toDelete.size() > 5) { - messageBox->setText(tr("Do you really want to delete all log files?")); + messageBox->setText(tr("Are you sure you want to delete all log files?")); messageBox->setDetailedText(toDelete.join('\n')); } else { - messageBox->setText(tr("Do you really want to delete these files?\n%1").arg(toDelete.join('\n'))); + messageBox->setText(tr("Are you sure you want to delete all these files?\n%1").arg(toDelete.join('\n'))); } messageBox->setStandardButtons(QMessageBox::Ok | QMessageBox::Cancel); messageBox->setDefaultButton(QMessageBox::Ok); @@ -267,6 +276,10 @@ void OtherLogsPage::on_btnClean_clicked() for(auto item: toDelete) { QFile file(FS::PathCombine(m_path, item)); + if (FS::trash(file.fileName())) + { + continue; + } if (!file.remove()) { failed.push_back(item); diff --git a/launcher/ui/pages/instance/ScreenshotsPage.cpp b/launcher/ui/pages/instance/ScreenshotsPage.cpp index 0092aef3..fff21670 100644 --- a/launcher/ui/pages/instance/ScreenshotsPage.cpp +++ b/launcher/ui/pages/instance/ScreenshotsPage.cpp @@ -1,8 +1,9 @@ // SPDX-License-Identifier: GPL-3.0-only /* - * PolyMC - Minecraft Launcher + * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 Jamie Mansfield * Copyright (C) 2022 Sefa Eyeoglu + * Copyright (C) 2022 TheKodeToad * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -379,6 +380,24 @@ void ScreenshotsPage::on_actionUpload_triggered() if (selection.isEmpty()) return; + + QString text; + if (selection.size() > 1) + text = tr("About to upload: %1 screenshots\n\n" + "Are you sure?") + .arg(selection.size()); + else + text = + tr("About to upload the selected screenshot.\n\n" + "Are you sure?"); + + auto response = CustomMessageBox::selectable(this, "Confirm Upload", text, QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No, + QMessageBox::No) + ->exec(); + + if (response != QMessageBox::Yes) + return; + QList uploaded; auto job = NetJob::Ptr(new NetJob("Screenshot Upload", APPLICATION->network())); if(selection.size() < 2) @@ -491,17 +510,32 @@ void ScreenshotsPage::on_actionCopy_File_s_triggered() void ScreenshotsPage::on_actionDelete_triggered() { - auto mbox = CustomMessageBox::selectable( - this, tr("Are you sure?"), tr("This will delete all selected screenshots."), - QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No); - std::unique_ptr box(mbox); + auto selected = ui->listView->selectionModel()->selectedIndexes(); - if (box->exec() != QMessageBox::Yes) + int count = ui->listView->selectionModel()->selectedRows().size(); + QString text; + if (count > 1) + text = tr("About to delete: %1 screenshots\n" + "This may be permanent and they will be gone from the folder.\n\n" + "Are you sure?") + .arg(count); + else + text = tr("About to delete the selected screenshot.\n" + "This may be permanent and it will be gone from the folder.\n\n" + "Are you sure?") + .arg(count); + + auto response = + CustomMessageBox::selectable(this, tr("CAREFUL!"), text, QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No)->exec(); + + if (response != QMessageBox::Yes) return; - auto selected = ui->listView->selectionModel()->selectedIndexes(); for (auto item : selected) { + if (FS::trash(m_model->filePath(item))) + continue; + m_model->remove(item); } } diff --git a/launcher/ui/pages/instance/ServersPage.cpp b/launcher/ui/pages/instance/ServersPage.cpp index a625e20b..c636b236 100644 --- a/launcher/ui/pages/instance/ServersPage.cpp +++ b/launcher/ui/pages/instance/ServersPage.cpp @@ -1,8 +1,9 @@ // SPDX-License-Identifier: GPL-3.0-only /* - * PolyMC - Minecraft Launcher + * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 Jamie Mansfield * Copyright (C) 2022 Sefa Eyeoglu + * Copyright (C) 2022 TheKodeToad * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -35,6 +36,7 @@ */ #include "ServersPage.h" +#include "ui/dialogs/CustomMessageBox.h" #include "ui_ServersPage.h" #include @@ -799,6 +801,17 @@ void ServersPage::on_actionAdd_triggered() void ServersPage::on_actionRemove_triggered() { + auto response = CustomMessageBox::selectable(this, tr("CAREFUL!"), + tr("About to remove: %1\n" + "This is permanent and the server will be gone from your list forever (A LONG TIME).\n\n" + "Are you sure?") + .arg(m_model->at(currentServer)->m_name), + QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No, QMessageBox::No) + ->exec(); + + if (response != QMessageBox::Yes) + return; + m_model->removeRow(currentServer); } diff --git a/launcher/ui/pages/instance/WorldListPage.cpp b/launcher/ui/pages/instance/WorldListPage.cpp index 93458ce4..74cb5a05 100644 --- a/launcher/ui/pages/instance/WorldListPage.cpp +++ b/launcher/ui/pages/instance/WorldListPage.cpp @@ -1,8 +1,9 @@ // SPDX-License-Identifier: GPL-3.0-only /* - * PolyMC - Minecraft Launcher + * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 Jamie Mansfield * Copyright (C) 2022 Sefa Eyeoglu + * Copyright (C) 2022 TheKodeToad * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -35,6 +36,7 @@ */ #include "WorldListPage.h" +#include "ui/dialogs/CustomMessageBox.h" #include "ui_WorldListPage.h" #include "minecraft/WorldList.h" @@ -192,12 +194,14 @@ void WorldListPage::on_actionRemove_triggered() if(!proxiedIndex.isValid()) return; - auto result = QMessageBox::question(this, - tr("Are you sure?"), - tr("This will remove the selected world permenantly.\n" - "The world will be gone forever (A LONG TIME).\n" - "\n" - "Do you want to continue?")); + auto result = CustomMessageBox::selectable(this, tr("CAREFUL!"), + tr("About to delete: %1\n" + "The world may be gone forever (A LONG TIME).\n\n" + "Are you sure?") + .arg(m_worlds->allWorlds().at(proxiedIndex.row()).name()), + QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No, QMessageBox::No) + ->exec(); + if(result != QMessageBox::Yes) { return; diff --git a/launcher/ui/pages/instance/WorldListPage.ui b/launcher/ui/pages/instance/WorldListPage.ui index 7c68bfae..d74dd079 100644 --- a/launcher/ui/pages/instance/WorldListPage.ui +++ b/launcher/ui/pages/instance/WorldListPage.ui @@ -109,7 +109,7 @@ - Remove + Delete From ee003cd9ee433a073393bdd47bd20d6d8cb38ca2 Mon Sep 17 00:00:00 2001 From: TheKodeToad Date: Wed, 14 Dec 2022 15:36:42 +0000 Subject: [PATCH 003/152] Add confirmation on customised components Signed-off-by: TheKodeToad --- launcher/ui/pages/instance/VersionPage.cpp | 41 ++++++++++++++++++---- 1 file changed, 35 insertions(+), 6 deletions(-) diff --git a/launcher/ui/pages/instance/VersionPage.cpp b/launcher/ui/pages/instance/VersionPage.cpp index c8a65f10..413b2f85 100644 --- a/launcher/ui/pages/instance/VersionPage.cpp +++ b/launcher/ui/pages/instance/VersionPage.cpp @@ -318,13 +318,29 @@ void VersionPage::on_actionReload_triggered() void VersionPage::on_actionRemove_triggered() { - if (ui->packageView->currentIndex().isValid()) + if (!ui->packageView->currentIndex().isValid()) { - // FIXME: use actual model, not reloading. - if (!m_profile->remove(ui->packageView->currentIndex().row())) - { - QMessageBox::critical(this, tr("Error"), tr("Couldn't remove file")); - } + return; + } + int index = ui->packageView->currentIndex().row(); + auto component = m_profile->getComponent(index); + if (component->isCustom()) + { + auto response = CustomMessageBox::selectable(this, tr("CAREFUL!"), + tr("About to remove: %1\n" + "This is permanent and will completely remove the custom component.\n\n" + "Are you sure?") + .arg(component->getName()), + QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No, QMessageBox::No) + ->exec(); + + if (response != QMessageBox::Yes) + return; + } + // FIXME: use actual model, not reloading. + if (!m_profile->remove(index)) + { + QMessageBox::critical(this, tr("Error"), tr("Couldn't remove file")); } updateButtons(); reloadPackProfile(); @@ -707,6 +723,19 @@ void VersionPage::on_actionRevert_triggered() { return; } + auto component = m_profile->getComponent(version); + + auto response = CustomMessageBox::selectable(this, tr("CAREFUL!"), + tr("About to revert: %1\n" + "This is permanent and will completely revert your customizations.\n\n" + "Are you sure?") + .arg(component->getName()), + QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No, QMessageBox::No) + ->exec(); + + if (response != QMessageBox::Yes) + return; + if(!m_profile->revertToBase(version)) { // TODO: some error box here From 4ee29b388d48cf84d4b120f49bf2313b1d994dca Mon Sep 17 00:00:00 2001 From: leo78913 Date: Thu, 15 Dec 2022 12:02:08 -0300 Subject: [PATCH 004/152] feat: add a provider column to the mods page Signed-off-by: leo78913 --- launcher/minecraft/mod/Mod.cpp | 14 ++++++++++++++ launcher/minecraft/mod/Mod.h | 1 + launcher/minecraft/mod/ModFolderModel.cpp | 10 ++++++++-- launcher/minecraft/mod/ModFolderModel.h | 1 + launcher/minecraft/mod/Resource.h | 3 ++- 5 files changed, 26 insertions(+), 3 deletions(-) diff --git a/launcher/minecraft/mod/Mod.cpp b/launcher/minecraft/mod/Mod.cpp index 39023f69..d491d980 100644 --- a/launcher/minecraft/mod/Mod.cpp +++ b/launcher/minecraft/mod/Mod.cpp @@ -44,6 +44,8 @@ #include "MetadataHandler.h" #include "Version.h" +static ModPlatform::ProviderCapabilities ProviderCaps; + Mod::Mod(const QFileInfo& file) : Resource(file), m_local_details() { m_enabled = (file.suffix() != "disabled"); @@ -91,6 +93,10 @@ std::pair Mod::compare(const Resource& other, SortType type) const if (this_ver < other_ver) return { -1, type == SortType::VERSION }; } + case SortType::PROVIDER: + auto compare_result = QString::compare(provider(), cast_other->provider(), Qt::CaseInsensitive); + if (compare_result != 0) + return { compare_result, type == SortType::PROVIDER }; } return { 0, false }; } @@ -189,4 +195,12 @@ void Mod::finishResolvingWithDetails(ModDetails&& details) m_local_details = std::move(details); if (metadata) setMetadata(std::move(metadata)); +}; + +auto Mod::provider() const -> QString +{ + if (metadata()) { + return ProviderCaps.readableName(metadata()->provider); + } + return "Unknown"; } diff --git a/launcher/minecraft/mod/Mod.h b/launcher/minecraft/mod/Mod.h index f336bec4..16d2bb32 100644 --- a/launcher/minecraft/mod/Mod.h +++ b/launcher/minecraft/mod/Mod.h @@ -61,6 +61,7 @@ public: auto description() const -> QString; auto authors() const -> QStringList; auto status() const -> ModStatus; + auto provider() const -> QString; auto metadata() -> std::shared_ptr; auto metadata() const -> const std::shared_ptr; diff --git a/launcher/minecraft/mod/ModFolderModel.cpp b/launcher/minecraft/mod/ModFolderModel.cpp index 4ccc5d4d..5aadc2f1 100644 --- a/launcher/minecraft/mod/ModFolderModel.cpp +++ b/launcher/minecraft/mod/ModFolderModel.cpp @@ -48,10 +48,11 @@ #include "minecraft/mod/tasks/LocalModParseTask.h" #include "minecraft/mod/tasks/ModFolderLoadTask.h" +#include "modplatform/ModIndex.h" ModFolderModel::ModFolderModel(const QString &dir, bool is_indexed) : ResourceFolderModel(QDir(dir)), m_is_indexed(is_indexed) { - m_column_sort_keys = { SortType::ENABLED, SortType::NAME, SortType::VERSION, SortType::DATE }; + m_column_sort_keys = { SortType::ENABLED, SortType::NAME, SortType::VERSION, SortType::DATE, SortType::PROVIDER }; } QVariant ModFolderModel::data(const QModelIndex &index, int role) const @@ -82,7 +83,8 @@ QVariant ModFolderModel::data(const QModelIndex &index, int role) const } case DateColumn: return m_resources[row]->dateTimeChanged(); - + case ProviderColumn: + return at(row)->provider(); default: return QVariant(); } @@ -118,6 +120,8 @@ QVariant ModFolderModel::headerData(int section, Qt::Orientation orientation, in return tr("Version"); case DateColumn: return tr("Last changed"); + case ProviderColumn: + return tr("Provider"); default: return QVariant(); } @@ -133,6 +137,8 @@ QVariant ModFolderModel::headerData(int section, Qt::Orientation orientation, in return tr("The version of the mod."); case DateColumn: return tr("The date and time this mod was last changed (or added)."); + case ProviderColumn: + return tr("Where the mod was downloaded from."); default: return QVariant(); } diff --git a/launcher/minecraft/mod/ModFolderModel.h b/launcher/minecraft/mod/ModFolderModel.h index 93980319..6898f6eb 100644 --- a/launcher/minecraft/mod/ModFolderModel.h +++ b/launcher/minecraft/mod/ModFolderModel.h @@ -67,6 +67,7 @@ public: NameColumn, VersionColumn, DateColumn, + ProviderColumn, NUM_COLUMNS }; enum ModStatusAction { diff --git a/launcher/minecraft/mod/Resource.h b/launcher/minecraft/mod/Resource.h index f9bd811e..0c37f3a3 100644 --- a/launcher/minecraft/mod/Resource.h +++ b/launcher/minecraft/mod/Resource.h @@ -20,7 +20,8 @@ enum class SortType { DATE, VERSION, ENABLED, - PACK_FORMAT + PACK_FORMAT, + PROVIDER }; enum class EnableAction { From aa3633d2d7591f4a68208d425e645fa4fc5c10a1 Mon Sep 17 00:00:00 2001 From: leo78913 Date: Fri, 16 Dec 2022 11:13:54 -0300 Subject: [PATCH 005/152] fix: translate unknown mod provider Co-authored-by: flow Signed-off-by: leo78913 --- launcher/minecraft/mod/Mod.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/launcher/minecraft/mod/Mod.cpp b/launcher/minecraft/mod/Mod.cpp index d491d980..1be8e7e3 100644 --- a/launcher/minecraft/mod/Mod.cpp +++ b/launcher/minecraft/mod/Mod.cpp @@ -202,5 +202,6 @@ auto Mod::provider() const -> QString if (metadata()) { return ProviderCaps.readableName(metadata()->provider); } - return "Unknown"; + //: Unknown mod provider (i.e. not Modrinth, CurseForge, etc...) + return tr("Unknown"); } From 6dc19c85a81af8f23ab31cf7e6fc905b753b951b Mon Sep 17 00:00:00 2001 From: RaptaG <77157639+RaptaG@users.noreply.github.com> Date: Sun, 18 Dec 2022 15:23:22 +0200 Subject: [PATCH 006/152] Improve the README Not very serious changes, just some enhancements to make it look better! Signed-off-by: RaptaG <77157639+RaptaG@users.noreply.github.com> --- README.md | 75 ++++++++++++++++++++++++++++--------------------------- 1 file changed, 38 insertions(+), 37 deletions(-) diff --git a/README.md b/README.md index f02b5695..c5a95dbe 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,15 @@ -

+

- Prism Launcher + Prism Launcher

- -Prism Launcher is a custom launcher for Minecraft that allows you to easily manage multiple installations of Minecraft at once. - -This is a **fork** of the MultiMC Launcher and is not endorsed by MultiMC. - +

+ Prism Launcher is a custom launcher for Minecraft that allows you to easily manage multiple installations of Minecraft at once.
+
This is a fork of the MultiMC Launcher and is not endorsed by it. +

## Installation @@ -18,7 +17,7 @@ This is a **fork** of the MultiMC Launcher and is not endorsed by MultiMC. Packaging status -- All downloads and instructions for Prism Launcher can be found on our [Website](https://prismlauncher.org/download/). +- All downloads and instructions for Prism Launcher can be found on our [Website](https://prismlauncher.org/download). - Last build status can be found in the [GitHub Actions](https://github.com/PrismLauncher/PrismLauncher/actions). ### Development Builds @@ -29,44 +28,33 @@ Prebuilt Development builds are provided for **Linux**, **Windows** and **macOS* For **Arch**, **Debian**, **Fedora**, **OpenSUSE (Tumbleweed)** and **Gentoo**, respectively, you can use these packages for the latest development versions: -[![prismlauncher-git](https://img.shields.io/badge/aur-prismlauncher--git-1793D1?style=flat-square&logo=archlinux&logoColor=white)](https://aur.archlinux.org/packages/prismlauncher-git/) [![prismlauncher-git](https://img.shields.io/badge/aur-prismlauncher--qt5--git-1793D1?style=flat-square&logo=archlinux&logoColor=white)](https://aur.archlinux.org/packages/prismlauncher-qt5-git/) [![prismlauncher-git](https://img.shields.io/badge/mpr-prismlauncher--git-A80030?style=flat-square&logo=debian&logoColor=white)](https://mpr.makedeb.org/packages/prismlauncher-git) [![prismlauncher-nightly](https://img.shields.io/badge/copr-prismlauncher--nightly-51A2DA?style=flat-square&logo=fedora&logoColor=white)](https://copr.fedorainfracloud.org/coprs/g3tchoo/prismlauncher/) [![prismlauncher-nightly](https://img.shields.io/badge/OBS-prismlauncher--nightly-3AB6A9?style=flat-square&logo=opensuse&logoColor=white)](https://build.opensuse.org/project/show/home:getchoo) [![prismlauncher-9999](https://img.shields.io/badge/gentoo-prismlauncher--9999-4D4270?style=flat-square&logo=gentoo&logoColor=white)](https://packages.gentoo.org/packages/games-action/prismlauncher) +[![prismlauncher-git](https://img.shields.io/badge/aur-prismlauncher--git-1793D1?label=AUR&logo=archlinux&logoColor=white)](https://aur.archlinux.org/packages/prismlauncher-git) [![prismlauncher-git](https://img.shields.io/badge/aur-prismlauncher--qt5--git-1793D1?label=AUR&logo=archlinux&logoColor=white)](https://aur.archlinux.org/packages/prismlauncher-qt5-git) [![prismlauncher-git](https://img.shields.io/badge/mpr-prismlauncher--git-A80030?label=MPR&logo=debian&logoColor=white)](https://mpr.makedeb.org/packages/prismlauncher-git)
[![prismlauncher-nightly](https://img.shields.io/badge/copr-prismlauncher--nightly-51A2DA?label=CORP&logo=fedora&logoColor=white)](https://copr.fedorainfracloud.org/coprs/g3tchoo/prismlauncher/) [![prismlauncher-nightly](https://img.shields.io/badge/OBS-prismlauncher--nightly-3AB6A9?logo=opensuse&logoColor=white)](https://build.opensuse.org/project/show/home:getchoo) [![prismlauncher-9999](https://img.shields.io/badge/gentoo-prismlauncher--9999-4D4270?label=Gentoo&logo=gentoo&logoColor=white)](https://packages.gentoo.org/packages/games-action/prismlauncher) + +These packages are also availiable to all the distributions based on the ones mentioned above. ## Community & Support -Feel free to create a GitHub issue if you find a bug or want to suggest a new feature. We have multiple community spaces where other community members can help you. +Feel free to create a GitHub issue if you find a bug or want to suggest a new feature. We have multiple community spaces where other community members can help you: -#### Join our Discord server: -[![Prism Launcher Discord server](https://discordapp.com/api/guilds/1031648380885147709/widget.png?style=banner2)](https://discord.gg/prismlauncher) +1) **Our Discord server:** -#### Join our Matrix space: -[![PrismLauncher Space](https://img.shields.io/matrix/prismlauncher:matrix.org?style=for-the-badge&logo=matrix)](https://matrix.to/#/#prismlauncher:matrix.org) +[![Prism Launcher Discord server](https://discordapp.com/api/guilds/1031648380885147709/widget.png?style=banner3)](https://discord.gg/prismlauncher) + +2) **Our Matrix space:** + +[![PrismLauncher Space](https://img.shields.io/matrix/prismlauncher:matrix.org?style=for-the-badge&label=Matrix%20Space&logo=matrix&color=purple)](https://matrix.to/#/#prismlauncher:matrix.org) + +3) **Our Subreddit:** -#### Join our Subreddit: [![r/PrismLauncher](https://img.shields.io/reddit/subreddit-subscribers/prismlauncher?style=for-the-badge&logo=reddit)](https://www.reddit.com/r/PrismLauncher/) -## Building - -If you want to build Prism Launcher yourself, check the [Build Instructions](https://prismlauncher.org/wiki/development/build-instructions/). - ## Translations The translation effort for PrismLauncher is hosted on [Weblate](https://hosted.weblate.org/projects/prismlauncher/launcher/) and information about translating Prism Launcher is available at -## Forking/Redistributing/Custom builds policy +## Building -We don't care what you do with your fork/custom build as long as you follow the terms of the [license](LICENSE) (this is a legal responsibility), and if you made code changes rather than just packaging a custom build, please do the following as a basic courtesy: - -- Make it clear that your fork is not PrismLauncher and is not endorsed by or affiliated with the PrismLauncher project (). -- Go through [CMakeLists.txt](CMakeLists.txt) and change PrismLauncher's API keys to your own or set them to empty strings (`""`) to disable them (this way the program will still compile but the functionality requiring those keys will be disabled). - -If you have any questions or want any clarification on the above conditions please make an issue and ask us. - -Be aware that if you build this software without removing the provided API keys in [CMakeLists.txt](CMakeLists.txt) you are accepting the following terms and conditions: - -- [Microsoft Identity Platform Terms of Use](https://docs.microsoft.com/en-us/legal/microsoft-identity-platform/terms-of-use) -- [CurseForge 3rd Party API Terms and Conditions](https://support.curseforge.com/en/support/solutions/articles/9000207405-curse-forge-3rd-party-api-terms-and-conditions) - -If you do not agree with these terms and conditions, then remove the associated API keys from the [CMakeLists.txt](CMakeLists.txt) file by setting them to an empty string (`""`). +If you want to build Prism Launcher yourself, check the [Build Instructions](https://prismlauncher.org/wiki/development/build-instructions/). ## Sponsors & Partners @@ -84,7 +72,7 @@ Thanks to Weblate for hosting our translation efforts. Translation status -Thanks to Netlify for providing us their excellent web services, as part of their [Open Source program](https://www.netlify.com/open-source/) +Thanks to Netlify for providing us their excellent web services, as part of their [Open Source program](https://www.netlify.com/open-source/). Deploys by Netlify @@ -92,11 +80,24 @@ Thanks to the awesome people over at [MacStadium](https://www.macstadium.com/), Powered by MacStadium +## Forking/Redistributing/Custom builds policy -## License +We don't care what you do with your fork/custom build as long as you follow the terms of the [license](LICENSE) (this is a legal responsibility), and if you made code changes rather than just packaging a custom build, please do the following as a basic courtesy: + +- Make it clear that your fork is not PrismLauncher and is not endorsed by or affiliated with the PrismLauncher project (). +- Go through [CMakeLists.txt](CMakeLists.txt) and change PrismLauncher's API keys to your own or set them to empty strings (`""`) to disable them (this way the program will still compile but the functionality requiring those keys will be disabled). + +If you have any questions or want any clarification on the above conditions please make an issue and ask us. + +Be aware that if you build this software without removing the provided API keys in [CMakeLists.txt](CMakeLists.txt) you are accepting the following terms and conditions: + +- [Microsoft Identity Platform Terms of Use](https://docs.microsoft.com/en-us/legal/microsoft-identity-platform/terms-of-use) +- [CurseForge 3rd Party API Terms and Conditions](https://support.curseforge.com/en/support/solutions/articles/9000207405-curse-forge-3rd-party-api-terms-and-conditions) + +If you do not agree with these terms and conditions, then remove the associated API keys from the [CMakeLists.txt](CMakeLists.txt) file by setting them to an empty string (`""`). + +## License ![https://github.com/PrismLauncher/PrismLauncher/blob/develop/LICENSE](https://img.shields.io/github/license/PrismLauncher/PrismLauncher?label=License&logo=gnu&color=C4282D) All launcher code is available under the GPL-3.0-only license. -![https://github.com/PrismLauncher/PrismLauncher/blob/develop/LICENSE](https://img.shields.io/github/license/PrismLauncher/PrismLauncher?style=for-the-badge&logo=gnu&color=C4282D) - The logo and related assets are under the CC BY-SA 4.0 license. From a566d1c5de3c648804eb882511e20e7b6c410703 Mon Sep 17 00:00:00 2001 From: RaptaG <77157639+RaptaG@users.noreply.github.com> Date: Sun, 18 Dec 2022 18:01:44 +0200 Subject: [PATCH 007/152] Change numbered list to bullet list Signed-off-by: RaptaG <77157639+RaptaG@users.noreply.github.com> --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index c5a95dbe..8765da93 100644 --- a/README.md +++ b/README.md @@ -36,15 +36,15 @@ These packages are also availiable to all the distributions based on the ones me Feel free to create a GitHub issue if you find a bug or want to suggest a new feature. We have multiple community spaces where other community members can help you: -1) **Our Discord server:** +- **Our Discord server:** [![Prism Launcher Discord server](https://discordapp.com/api/guilds/1031648380885147709/widget.png?style=banner3)](https://discord.gg/prismlauncher) -2) **Our Matrix space:** +- **Our Matrix space:** [![PrismLauncher Space](https://img.shields.io/matrix/prismlauncher:matrix.org?style=for-the-badge&label=Matrix%20Space&logo=matrix&color=purple)](https://matrix.to/#/#prismlauncher:matrix.org) -3) **Our Subreddit:** +- **Our Subreddit:** [![r/PrismLauncher](https://img.shields.io/reddit/subreddit-subscribers/prismlauncher?style=for-the-badge&logo=reddit)](https://www.reddit.com/r/PrismLauncher/) From 07de285299231c4684a3e3d186205a518ab1a8b2 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 21 Dec 2022 14:46:35 +0000 Subject: [PATCH 008/152] chore(deps): update actions/cache action to v3.2.0 --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1ba5d0e4..fe2cb647 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -165,7 +165,7 @@ jobs: - name: Retrieve ccache cache (Windows MinGW-w64) if: runner.os == 'Windows' && matrix.msystem != '' && inputs.build_type == 'Debug' - uses: actions/cache@v3.0.11 + uses: actions/cache@v3.2.0 with: path: '${{ github.workspace }}\.ccache' key: ${{ matrix.os }}-mingw-w64 From c85867395d310fa14a48f60eec17416b41fb486b Mon Sep 17 00:00:00 2001 From: flow Date: Thu, 18 Aug 2022 21:32:57 -0300 Subject: [PATCH 009/152] feat: use Qt logging facilities instead of our own This system allows us to globally define categories, and control whether they are shown or not at runtime. It also does some things by it's own, so we can remove some (uhhh) code. Lastly, this allows changing the behavior of the logger at runtime via environment variables that Qt takes care of for us. Signed-off-by: flow --- launcher/Application.cpp | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/launcher/Application.cpp b/launcher/Application.cpp index 3f313ee4..eef4fd7a 100644 --- a/launcher/Application.cpp +++ b/launcher/Application.cpp @@ -146,19 +146,12 @@ static const QLatin1String liveCheckFile("live.check"); PixmapCache* PixmapCache::s_instance = nullptr; namespace { + +/** This is used so that we can output to the log file in addition to the CLI. */ void appDebugOutput(QtMsgType type, const QMessageLogContext &context, const QString &msg) { - const char *levels = "DWCFIS"; - const QString format("%1 %2 %3\n"); - - qint64 msecstotal = APPLICATION->timeSinceStart(); - qint64 seconds = msecstotal / 1000; - qint64 msecs = msecstotal % 1000; - QString foo; - char buf[1025] = {0}; - ::snprintf(buf, 1024, "%5lld.%03lld", seconds, msecs); - - QString out = format.arg(buf).arg(levels[type]).arg(msg); + QString out = qFormatLogMessage(type, context, msg); + out += QChar::LineFeed; APPLICATION->logFile->write(out.toUtf8()); APPLICATION->logFile->flush(); @@ -431,6 +424,15 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv) return; } qInstallMessageHandler(appDebugOutput); + + // TODO: Set filter rules based on CLI arguments + qSetMessagePattern( + "%{time process}" " " + "%{if-debug}D%{endif}" "%{if-info}I%{endif}" "%{if-warning}W%{endif}" "%{if-critical}C%{endif}" "%{if-fatal}F%{endif}" + " " "|" " " + "%{if-category}[%{category}]: %{endif}" + "%{message}"); + qDebug() << "<> Log initialized."; } From ee3e65d759da95aa524aad76c2a190792f716560 Mon Sep 17 00:00:00 2001 From: flow Date: Thu, 22 Dec 2022 18:16:21 -0300 Subject: [PATCH 010/152] feat(docs): add note about logging env variables in man page Signed-off-by: flow --- launcher/Application.cpp | 1 - program_info/prismlauncher.6.scd | 18 ++++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/launcher/Application.cpp b/launcher/Application.cpp index eef4fd7a..ff34a168 100644 --- a/launcher/Application.cpp +++ b/launcher/Application.cpp @@ -425,7 +425,6 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv) } qInstallMessageHandler(appDebugOutput); - // TODO: Set filter rules based on CLI arguments qSetMessagePattern( "%{time process}" " " "%{if-debug}D%{endif}" "%{if-info}I%{endif}" "%{if-warning}W%{endif}" "%{if-critical}C%{endif}" "%{if-fatal}F%{endif}" diff --git a/program_info/prismlauncher.6.scd b/program_info/prismlauncher.6.scd index f979e457..e1ebfff3 100644 --- a/program_info/prismlauncher.6.scd +++ b/program_info/prismlauncher.6.scd @@ -41,6 +41,24 @@ Here are the current features of Prism Launcher. *-a, --profile*=PROFILE Use the account specified by PROFILE (only valid in combination with --launch). +# ENVIRONMENT + +The behavior of the launcher can be customized by the following environment +variables, besides other common Qt variables: + +*QT_LOGGING_RULES* + Specifies which logging categories are shown in the logs. One can + enable/disable multiple categories by separating them with a semicolon (;). + + The specific syntax, and alternatives to this setting, can be found at + https://doc.qt.io/qt-6/qloggingcategory.html#configuring-categories. + +*QT_MESSAGE_PATTERN* + Specifies the format in which the console output will be shown. + + Available options, as well as syntax, can be viewed at + https://doc.qt.io/qt-6/qtglobal.html#qSetMessagePattern. + # EXIT STATUS *0* From 01139c3b50eeb30787e87aeea37948b672763c73 Mon Sep 17 00:00:00 2001 From: seth Date: Thu, 22 Dec 2022 19:25:04 -0500 Subject: [PATCH 011/152] fix: assume builds are stable when git isn't installed Signed-off-by: seth --- buildconfig/BuildConfig.cpp.in | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/buildconfig/BuildConfig.cpp.in b/buildconfig/BuildConfig.cpp.in index 1262ce8e..02c021cf 100644 --- a/buildconfig/BuildConfig.cpp.in +++ b/buildconfig/BuildConfig.cpp.in @@ -76,7 +76,8 @@ Config::Config() // Assume that builds outside of Git repos are "stable" if (GIT_REFSPEC == QStringLiteral("GITDIR-NOTFOUND") - || GIT_TAG == QStringLiteral("GITDIR-NOTFOUND")) + || GIT_TAG == QStringLiteral("GITDIR-NOTFOUND") + || GIT_TAG == QStringLiteral("GIT-NOTFOUND")) { GIT_REFSPEC = "refs/heads/stable"; GIT_TAG = versionString(); From 3227859992b90b4b4b70af42a7761a7aaed330d0 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 23 Dec 2022 08:18:30 +0000 Subject: [PATCH 012/152] chore(deps): update actions/cache action to v3.2.1 --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index fe2cb647..f415741d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -165,7 +165,7 @@ jobs: - name: Retrieve ccache cache (Windows MinGW-w64) if: runner.os == 'Windows' && matrix.msystem != '' && inputs.build_type == 'Debug' - uses: actions/cache@v3.2.0 + uses: actions/cache@v3.2.1 with: path: '${{ github.workspace }}\.ccache' key: ${{ matrix.os }}-mingw-w64 From f932ffcc5b2184bc29f6f9465b7e42dd2c4527d2 Mon Sep 17 00:00:00 2001 From: seth Date: Fri, 23 Dec 2022 20:50:08 -0500 Subject: [PATCH 013/152] fix: check if GIT_REFSPEC is empty Signed-off-by: seth --- buildconfig/BuildConfig.cpp.in | 1 + 1 file changed, 1 insertion(+) diff --git a/buildconfig/BuildConfig.cpp.in b/buildconfig/BuildConfig.cpp.in index 02c021cf..35373189 100644 --- a/buildconfig/BuildConfig.cpp.in +++ b/buildconfig/BuildConfig.cpp.in @@ -77,6 +77,7 @@ Config::Config() // Assume that builds outside of Git repos are "stable" if (GIT_REFSPEC == QStringLiteral("GITDIR-NOTFOUND") || GIT_TAG == QStringLiteral("GITDIR-NOTFOUND") + || GIT_REFSPEC == QStringLiteral("") || GIT_TAG == QStringLiteral("GIT-NOTFOUND")) { GIT_REFSPEC = "refs/heads/stable"; From cbe5af235ca2fc990efa0a2db9e4951f127f0131 Mon Sep 17 00:00:00 2001 From: TheKodeToad Date: Sat, 17 Dec 2022 09:26:06 +0000 Subject: [PATCH 014/152] Make requested changes Signed-off-by: TheKodeToad --- launcher/ui/GuiUtil.cpp | 5 ++- launcher/ui/MainWindow.cpp | 2 +- .../pages/instance/ExternalResourcesPage.cpp | 33 ++++++++++--------- launcher/ui/pages/instance/OtherLogsPage.cpp | 4 +-- .../ui/pages/instance/ScreenshotsPage.cpp | 2 +- launcher/ui/pages/instance/ServersPage.cpp | 2 +- launcher/ui/pages/instance/VersionPage.cpp | 4 +-- launcher/ui/pages/instance/WorldListPage.cpp | 2 +- 8 files changed, 27 insertions(+), 27 deletions(-) diff --git a/launcher/ui/GuiUtil.cpp b/launcher/ui/GuiUtil.cpp index 241354cb..6a22ec2f 100644 --- a/launcher/ui/GuiUtil.cpp +++ b/launcher/ui/GuiUtil.cpp @@ -64,13 +64,12 @@ QString GuiUtil::uploadPaste(const QString &name, const QString &text, QWidget * baseUrl = pasteCustomAPIBaseSetting; if (baseUrl.isValid()) { - auto response = CustomMessageBox::selectable(parentWidget, "Confirm Upload", + auto response = CustomMessageBox::selectable(parentWidget, QObject::tr("Confirm Upload"), QObject::tr("About to upload: %1\n" "Uploading to: %2\n" "You should double-check for personal information.\n\n" "Are you sure?") - .arg(name) - .arg(baseUrl.host()), + .arg(name, baseUrl.host()), QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No, QMessageBox::No) ->exec(); diff --git a/launcher/ui/MainWindow.cpp b/launcher/ui/MainWindow.cpp index 4ddef6d4..7442b955 100644 --- a/launcher/ui/MainWindow.cpp +++ b/launcher/ui/MainWindow.cpp @@ -2094,7 +2094,7 @@ void MainWindow::on_actionDeleteInstance_triggered() auto id = m_selectedInstance->id(); - auto response = CustomMessageBox::selectable(this, tr("CAREFUL!"), + auto response = CustomMessageBox::selectable(this, tr("Confirm Deletion"), tr("About to delete: %1\n" "This may be permanent and will completely delete the instance.\n\n" "Are you sure?") diff --git a/launcher/ui/pages/instance/ExternalResourcesPage.cpp b/launcher/ui/pages/instance/ExternalResourcesPage.cpp index 41ccd1db..6f1abbff 100644 --- a/launcher/ui/pages/instance/ExternalResourcesPage.cpp +++ b/launcher/ui/pages/instance/ExternalResourcesPage.cpp @@ -162,7 +162,7 @@ void ExternalResourcesPage::removeItem() int count = 0; bool folder = false; - for (auto i : selection.indexes()) { + for (auto& i : selection.indexes()) { if (i.column() == 0) { count++; @@ -172,23 +172,24 @@ void ExternalResourcesPage::removeItem() } } - bool enough = count > 1; + QString text; + bool multiple = count > 1; - if (enough || folder) { - QString text; - if (enough) - text = tr("About to remove: %1 items\n" - "This may be permanent and they will be gone from the folder.\n\n" - "Are you sure?") - .arg(count); - else - text = tr("About to remove: %1 (folder)\n" - "This may be permanent and it will be gone from the parent folder.\n\n" - "Are you sure?") - .arg(m_model->at(selection.indexes().at(0).row()).fileinfo().fileName()); + if (multiple) { + text = tr("About to remove: %1 items\n" + "This may be permanent and they will be gone from the folder.\n\n" + "Are you sure?") + .arg(count); + } else if (folder) { + text = tr("About to remove: %1 (folder)\n" + "This may be permanent and it will be gone from the parent folder.\n\n" + "Are you sure?") + .arg(m_model->at(selection.indexes().at(0).row()).fileinfo().fileName()); + } - auto response = CustomMessageBox::selectable(this, tr("CAREFUL!"), text, QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No, - QMessageBox::No) + if (!text.isEmpty()) { + auto response = CustomMessageBox::selectable(this, tr("Confirm Removal"), text, QMessageBox::Warning, + QMessageBox::Yes | QMessageBox::No, QMessageBox::No) ->exec(); if (response != QMessageBox::Yes) diff --git a/launcher/ui/pages/instance/OtherLogsPage.cpp b/launcher/ui/pages/instance/OtherLogsPage.cpp index ad444e6b..1be2a3f8 100644 --- a/launcher/ui/pages/instance/OtherLogsPage.cpp +++ b/launcher/ui/pages/instance/OtherLogsPage.cpp @@ -220,7 +220,7 @@ void OtherLogsPage::on_btnDelete_clicked() setControlsEnabled(false); return; } - if (QMessageBox::question(this, tr("CAREFUL!"), + if (QMessageBox::question(this, tr("Confirm Deletion"), tr("About to delete: %1\n" "This may be permanent and it will be gone from the logs folder.\n\n" "Are you sure?") @@ -252,7 +252,7 @@ void OtherLogsPage::on_btnClean_clicked() return; } QMessageBox *messageBox = new QMessageBox(this); - messageBox->setWindowTitle(tr("CAREFUL!")); + messageBox->setWindowTitle(tr("Confirm Cleanup")); if(toDelete.size() > 5) { messageBox->setText(tr("Are you sure you want to delete all log files?")); diff --git a/launcher/ui/pages/instance/ScreenshotsPage.cpp b/launcher/ui/pages/instance/ScreenshotsPage.cpp index fff21670..4b756766 100644 --- a/launcher/ui/pages/instance/ScreenshotsPage.cpp +++ b/launcher/ui/pages/instance/ScreenshotsPage.cpp @@ -526,7 +526,7 @@ void ScreenshotsPage::on_actionDelete_triggered() .arg(count); auto response = - CustomMessageBox::selectable(this, tr("CAREFUL!"), text, QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No)->exec(); + CustomMessageBox::selectable(this, tr("Confirm Deletion"), text, QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No)->exec(); if (response != QMessageBox::Yes) return; diff --git a/launcher/ui/pages/instance/ServersPage.cpp b/launcher/ui/pages/instance/ServersPage.cpp index c636b236..6925ffb4 100644 --- a/launcher/ui/pages/instance/ServersPage.cpp +++ b/launcher/ui/pages/instance/ServersPage.cpp @@ -801,7 +801,7 @@ void ServersPage::on_actionAdd_triggered() void ServersPage::on_actionRemove_triggered() { - auto response = CustomMessageBox::selectable(this, tr("CAREFUL!"), + auto response = CustomMessageBox::selectable(this, tr("Confirm Removal"), tr("About to remove: %1\n" "This is permanent and the server will be gone from your list forever (A LONG TIME).\n\n" "Are you sure?") diff --git a/launcher/ui/pages/instance/VersionPage.cpp b/launcher/ui/pages/instance/VersionPage.cpp index 413b2f85..08ab8641 100644 --- a/launcher/ui/pages/instance/VersionPage.cpp +++ b/launcher/ui/pages/instance/VersionPage.cpp @@ -326,7 +326,7 @@ void VersionPage::on_actionRemove_triggered() auto component = m_profile->getComponent(index); if (component->isCustom()) { - auto response = CustomMessageBox::selectable(this, tr("CAREFUL!"), + auto response = CustomMessageBox::selectable(this, tr("Confirm Removal"), tr("About to remove: %1\n" "This is permanent and will completely remove the custom component.\n\n" "Are you sure?") @@ -725,7 +725,7 @@ void VersionPage::on_actionRevert_triggered() } auto component = m_profile->getComponent(version); - auto response = CustomMessageBox::selectable(this, tr("CAREFUL!"), + auto response = CustomMessageBox::selectable(this, tr("Confirm Reversion"), tr("About to revert: %1\n" "This is permanent and will completely revert your customizations.\n\n" "Are you sure?") diff --git a/launcher/ui/pages/instance/WorldListPage.cpp b/launcher/ui/pages/instance/WorldListPage.cpp index 74cb5a05..c98f1e5a 100644 --- a/launcher/ui/pages/instance/WorldListPage.cpp +++ b/launcher/ui/pages/instance/WorldListPage.cpp @@ -194,7 +194,7 @@ void WorldListPage::on_actionRemove_triggered() if(!proxiedIndex.isValid()) return; - auto result = CustomMessageBox::selectable(this, tr("CAREFUL!"), + auto result = CustomMessageBox::selectable(this, tr("Confirm Deletion"), tr("About to delete: %1\n" "The world may be gone forever (A LONG TIME).\n\n" "Are you sure?") From e4296c48c86c6c0a0523630563808af09a30e923 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 24 Dec 2022 16:20:44 +0000 Subject: [PATCH 015/152] chore(deps): update flatpak/flatpak-github-actions action to v5 --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f415741d..dd27ba30 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -546,7 +546,7 @@ jobs: submodules: 'true' - name: Build Flatpak (Linux) if: inputs.build_type == 'Debug' - uses: flatpak/flatpak-github-actions/flatpak-builder@v4 + uses: flatpak/flatpak-github-actions/flatpak-builder@v5 with: bundle: "Prism Launcher.flatpak" manifest-path: flatpak/org.prismlauncher.PrismLauncher.yml From 64c51a70a3aa110131fb6ad0cabc07ccfdcbb1c0 Mon Sep 17 00:00:00 2001 From: Rachel Powers <508861+Ryex@users.noreply.github.com> Date: Fri, 9 Dec 2022 20:26:05 -0700 Subject: [PATCH 016/152] feat: add initial support for parseing datapacks Signed-off-by: Rachel Powers <508861+Ryex@users.noreply.github.com> --- launcher/CMakeLists.txt | 4 + launcher/minecraft/mod/DataPack.cpp | 110 ++++++++++++ launcher/minecraft/mod/DataPack.h | 73 ++++++++ launcher/minecraft/mod/ResourcePack.cpp | 4 +- .../mod/tasks/LocalDataPackParseTask.cpp | 169 ++++++++++++++++++ .../mod/tasks/LocalDataPackParseTask.h | 64 +++++++ .../mod/tasks/LocalResourcePackParseTask.cpp | 82 ++++++--- .../mod/tasks/LocalResourcePackParseTask.h | 8 +- .../mod/tasks/LocalTexturePackParseTask.cpp | 59 ++++-- .../mod/tasks/LocalTexturePackParseTask.h | 8 +- tests/CMakeLists.txt | 3 + tests/DataPackParse_test.cpp | 76 ++++++++ .../another_test_folder/pack.mcmeta | Bin 0 -> 104 bytes .../DataPackParse/test_data_pack_boogaloo.zip | Bin 0 -> 898 bytes .../DataPackParse/test_folder/pack.mcmeta | Bin 0 -> 86 bytes 15 files changed, 610 insertions(+), 50 deletions(-) create mode 100644 launcher/minecraft/mod/DataPack.cpp create mode 100644 launcher/minecraft/mod/DataPack.h create mode 100644 launcher/minecraft/mod/tasks/LocalDataPackParseTask.cpp create mode 100644 launcher/minecraft/mod/tasks/LocalDataPackParseTask.h create mode 100644 tests/DataPackParse_test.cpp create mode 100644 tests/testdata/DataPackParse/another_test_folder/pack.mcmeta create mode 100644 tests/testdata/DataPackParse/test_data_pack_boogaloo.zip create mode 100644 tests/testdata/DataPackParse/test_folder/pack.mcmeta diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index a0d92b6e..c12e6740 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -331,6 +331,8 @@ set(MINECRAFT_SOURCES minecraft/mod/Resource.cpp minecraft/mod/ResourceFolderModel.h minecraft/mod/ResourceFolderModel.cpp + minecraft/mod/DataPack.h + minecraft/mod/DataPack.cpp minecraft/mod/ResourcePack.h minecraft/mod/ResourcePack.cpp minecraft/mod/ResourcePackFolderModel.h @@ -347,6 +349,8 @@ set(MINECRAFT_SOURCES minecraft/mod/tasks/LocalModParseTask.cpp minecraft/mod/tasks/LocalModUpdateTask.h minecraft/mod/tasks/LocalModUpdateTask.cpp + minecraft/mod/tasks/LocalDataPackParseTask.h + minecraft/mod/tasks/LocalDataPackParseTask.cpp minecraft/mod/tasks/LocalResourcePackParseTask.h minecraft/mod/tasks/LocalResourcePackParseTask.cpp minecraft/mod/tasks/LocalTexturePackParseTask.h diff --git a/launcher/minecraft/mod/DataPack.cpp b/launcher/minecraft/mod/DataPack.cpp new file mode 100644 index 00000000..3f275160 --- /dev/null +++ b/launcher/minecraft/mod/DataPack.cpp @@ -0,0 +1,110 @@ +// SPDX-FileCopyrightText: 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> +// +// SPDX-License-Identifier: GPL-3.0-only + +/* + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "DataPack.h" + +#include +#include +#include + +#include "Version.h" + +#include "minecraft/mod/tasks/LocalDataPackParseTask.h" + +// Values taken from: +// https://minecraft.fandom.com/wiki/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") } }, +}; + +void DataPack::setPackFormat(int new_format_id) +{ + QMutexLocker locker(&m_data_lock); + + if (!s_pack_format_versions.contains(new_format_id)) { + qWarning() << "Pack format '%1' is not a recognized resource pack id!"; + } + + m_pack_format = new_format_id; +} + +void DataPack::setDescription(QString new_description) +{ + QMutexLocker locker(&m_data_lock); + + m_description = new_description; +} + +std::pair DataPack::compatibleVersions() const +{ + if (!s_pack_format_versions.contains(m_pack_format)) { + return { {}, {} }; + } + + return s_pack_format_versions.constFind(m_pack_format).value(); +} + +std::pair 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; + } + 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 }; + if (this_ver < other_ver) + return { -1, type == SortType::PACK_FORMAT }; + } + } + return { 0, false }; +} + +bool DataPack::applyFilter(QRegularExpression filter) const +{ + if (filter.match(description()).hasMatch()) + return true; + + if (filter.match(QString::number(packFormat())).hasMatch()) + return true; + + if (filter.match(compatibleVersions().first.toString()).hasMatch()) + return true; + if (filter.match(compatibleVersions().second.toString()).hasMatch()) + return true; + + return Resource::applyFilter(filter); +} + +bool DataPack::valid() const +{ + return m_pack_format != 0; +} diff --git a/launcher/minecraft/mod/DataPack.h b/launcher/minecraft/mod/DataPack.h new file mode 100644 index 00000000..17d9b65e --- /dev/null +++ b/launcher/minecraft/mod/DataPack.h @@ -0,0 +1,73 @@ +// SPDX-FileCopyrightText: 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> +// +// SPDX-License-Identifier: GPL-3.0-only + +/* + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include "Resource.h" + +#include + +class Version; + +/* TODO: + * + * Store localized descriptions + * */ + +class DataPack : public Resource { + Q_OBJECT + public: + using Ptr = shared_qobject_ptr; + + DataPack(QObject* parent = nullptr) : Resource(parent) {} + DataPack(QFileInfo file_info) : Resource(file_info) {} + + /** Gets the numerical ID of the pack format. */ + [[nodiscard]] int packFormat() const { return m_pack_format; } + /** Gets, respectively, the lower and upper versions supported by the set pack format. */ + [[nodiscard]] std::pair compatibleVersions() const; + + /** Gets the description of the resource pack. */ + [[nodiscard]] QString description() const { return m_description; } + + /** Thread-safe. */ + void setPackFormat(int new_format_id); + + /** Thread-safe. */ + void setDescription(QString new_description); + + bool valid() const override; + + [[nodiscard]] auto compare(Resource const& other, SortType type) const -> std::pair override; + [[nodiscard]] bool applyFilter(QRegularExpression filter) const override; + + protected: + mutable QMutex m_data_lock; + + /* The 'version' of a resource pack, as defined in the pack.mcmeta file. + * See https://minecraft.fandom.com/wiki/Tutorials/Creating_a_resource_pack#Formatting_pack.mcmeta + */ + int m_pack_format = 0; + + /** The resource pack's description, as defined in the pack.mcmeta file. + */ + QString m_description; +}; diff --git a/launcher/minecraft/mod/ResourcePack.cpp b/launcher/minecraft/mod/ResourcePack.cpp index 3a2fd771..47da4fea 100644 --- a/launcher/minecraft/mod/ResourcePack.cpp +++ b/launcher/minecraft/mod/ResourcePack.cpp @@ -17,7 +17,9 @@ static const QMap> s_pack_format_versions = { { 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("1.19.3"), Version("1.19.3") } }, + { 9, { Version("1.19"), Version("1.19.2") } }, + // { 11, { Version("22w42a"), Version("22w44a") } } + { 12, { Version("1.19.3"), Version("1.19.3") } }, }; void ResourcePack::setPackFormat(int new_format_id) diff --git a/launcher/minecraft/mod/tasks/LocalDataPackParseTask.cpp b/launcher/minecraft/mod/tasks/LocalDataPackParseTask.cpp new file mode 100644 index 00000000..8bc8278b --- /dev/null +++ b/launcher/minecraft/mod/tasks/LocalDataPackParseTask.cpp @@ -0,0 +1,169 @@ +// SPDX-FileCopyrightText: 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> +// +// SPDX-License-Identifier: GPL-3.0-only + +/* + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "LocalDataPackParseTask.h" + +#include "FileSystem.h" +#include "Json.h" + +#include +#include +#include + +#include + +namespace DataPackUtils { + +bool process(DataPack& pack, ProcessingLevel level) +{ + switch (pack.type()) { + case ResourceType::FOLDER: + return DataPackUtils::processFolder(pack, level); + case ResourceType::ZIPFILE: + return DataPackUtils::processZIP(pack, level); + default: + qWarning() << "Invalid type for resource pack parse task!"; + return false; + } +} + +bool processFolder(DataPack& pack, ProcessingLevel level) +{ + Q_ASSERT(pack.type() == ResourceType::FOLDER); + + QFileInfo mcmeta_file_info(FS::PathCombine(pack.fileinfo().filePath(), "pack.mcmeta")); + if (mcmeta_file_info.exists() && mcmeta_file_info.isFile()) { + QFile mcmeta_file(mcmeta_file_info.filePath()); + if (!mcmeta_file.open(QIODevice::ReadOnly)) + return false; // can't open mcmeta file + + auto data = mcmeta_file.readAll(); + + bool mcmeta_result = DataPackUtils::processMCMeta(pack, std::move(data)); + + mcmeta_file.close(); + if (!mcmeta_result) { + return false; // mcmeta invalid + } + } else { + return false; // mcmeta file isn't a valid file + } + + QFileInfo data_dir_info(FS::PathCombine(pack.fileinfo().filePath(), "data")); + if (!data_dir_info.exists() || !data_dir_info.isDir()) { + return false; // data dir does not exists or isn't valid + } + + if (level == ProcessingLevel::BasicInfoOnly) { + return true; // only need basic info already checked + } + + return true; // all tests passed +} + +bool processZIP(DataPack& pack, ProcessingLevel level) +{ + Q_ASSERT(pack.type() == ResourceType::ZIPFILE); + + QuaZip zip(pack.fileinfo().filePath()); + if (!zip.open(QuaZip::mdUnzip)) + return false; // can't open zip file + + QuaZipFile file(&zip); + + if (zip.setCurrentFile("pack.mcmeta")) { + if (!file.open(QIODevice::ReadOnly)) { + qCritical() << "Failed to open file in zip."; + zip.close(); + return false; + } + + auto data = file.readAll(); + + bool mcmeta_result = DataPackUtils::processMCMeta(pack, std::move(data)); + + file.close(); + if (!mcmeta_result) { + return false; // mcmeta invalid + } + } else { + return false; // could not set pack.mcmeta as current file. + } + + QuaZipDir zipDir(&zip); + if (!zipDir.exists("/data")) { + return false; // data dir does not exists at zip root + } + + if (level == ProcessingLevel::BasicInfoOnly) { + zip.close(); + return true; // only need basic info already checked + } + + zip.close(); + + return true; +} + +// https://minecraft.fandom.com/wiki/Tutorials/Creating_a_resource_pack#Formatting_pack.mcmeta +bool processMCMeta(DataPack& pack, QByteArray&& raw_data) +{ + try { + auto json_doc = QJsonDocument::fromJson(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", "")); + } catch (Json::JsonException& e) { + qWarning() << "JsonException: " << e.what() << e.cause(); + return false; + } + return true; +} + +bool validate(QFileInfo file) +{ + DataPack dp{ file }; + return DataPackUtils::process(dp, ProcessingLevel::BasicInfoOnly) && dp.valid(); +} + +} // namespace DataPackUtils + +LocalDataPackParseTask::LocalDataPackParseTask(int token, DataPack& dp) + : Task(nullptr, false), m_token(token), m_resource_pack(dp) +{} + +bool LocalDataPackParseTask::abort() +{ + m_aborted = true; + return true; +} + +void LocalDataPackParseTask::executeTask() +{ + if (!DataPackUtils::process(m_resource_pack)) + return; + + if (m_aborted) + emitAborted(); + else + emitSucceeded(); +} diff --git a/launcher/minecraft/mod/tasks/LocalDataPackParseTask.h b/launcher/minecraft/mod/tasks/LocalDataPackParseTask.h new file mode 100644 index 00000000..ee64df46 --- /dev/null +++ b/launcher/minecraft/mod/tasks/LocalDataPackParseTask.h @@ -0,0 +1,64 @@ +// SPDX-FileCopyrightText: 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> +// +// SPDX-License-Identifier: GPL-3.0-only + +/* + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include +#include + +#include "minecraft/mod/DataPack.h" + +#include "tasks/Task.h" + +namespace DataPackUtils { + +enum class ProcessingLevel { Full, BasicInfoOnly }; + +bool process(DataPack& pack, ProcessingLevel level = ProcessingLevel::Full); + +bool processZIP(DataPack& pack, ProcessingLevel level = ProcessingLevel::Full); +bool processFolder(DataPack& pack, ProcessingLevel level = ProcessingLevel::Full); + +bool processMCMeta(DataPack& pack, QByteArray&& raw_data); + +/** Checks whether a file is valid as a resource pack or not. */ +bool validate(QFileInfo file); +} // namespace ResourcePackUtils + +class LocalDataPackParseTask : public Task { + Q_OBJECT + public: + LocalDataPackParseTask(int token, DataPack& rp); + + [[nodiscard]] bool canAbort() const override { return true; } + bool abort() override; + + void executeTask() override; + + [[nodiscard]] int token() const { return m_token; } + + private: + int m_token; + + DataPack& m_resource_pack; + + bool m_aborted = false; +}; diff --git a/launcher/minecraft/mod/tasks/LocalResourcePackParseTask.cpp b/launcher/minecraft/mod/tasks/LocalResourcePackParseTask.cpp index 6fd4b024..18d7383d 100644 --- a/launcher/minecraft/mod/tasks/LocalResourcePackParseTask.cpp +++ b/launcher/minecraft/mod/tasks/LocalResourcePackParseTask.cpp @@ -23,6 +23,7 @@ #include #include +#include #include @@ -32,58 +33,74 @@ bool process(ResourcePack& pack, ProcessingLevel level) { switch (pack.type()) { case ResourceType::FOLDER: - ResourcePackUtils::processFolder(pack, level); - return true; + return ResourcePackUtils::processFolder(pack, level); case ResourceType::ZIPFILE: - ResourcePackUtils::processZIP(pack, level); - return true; + return ResourcePackUtils::processZIP(pack, level); default: qWarning() << "Invalid type for resource pack parse task!"; return false; } } -void processFolder(ResourcePack& pack, ProcessingLevel level) +bool processFolder(ResourcePack& pack, ProcessingLevel level) { Q_ASSERT(pack.type() == ResourceType::FOLDER); QFileInfo mcmeta_file_info(FS::PathCombine(pack.fileinfo().filePath(), "pack.mcmeta")); - if (mcmeta_file_info.isFile()) { + if (mcmeta_file_info.exists() && mcmeta_file_info.isFile()) { QFile mcmeta_file(mcmeta_file_info.filePath()); if (!mcmeta_file.open(QIODevice::ReadOnly)) - return; + return false; // can't open mcmeta file auto data = mcmeta_file.readAll(); - ResourcePackUtils::processMCMeta(pack, std::move(data)); + bool mcmeta_result = ResourcePackUtils::processMCMeta(pack, std::move(data)); mcmeta_file.close(); + if (!mcmeta_result) { + return false; // mcmeta invalid + } + } else { + return false; // mcmeta file isn't a valid file } - if (level == ProcessingLevel::BasicInfoOnly) - return; + QFileInfo assets_dir_info(FS::PathCombine(pack.fileinfo().filePath(), "assets")); + if (!assets_dir_info.exists() || !assets_dir_info.isDir()) { + return false; // assets dir does not exists or isn't valid + } + if (level == ProcessingLevel::BasicInfoOnly) { + return true; // only need basic info already checked + } + QFileInfo image_file_info(FS::PathCombine(pack.fileinfo().filePath(), "pack.png")); - if (image_file_info.isFile()) { + if (image_file_info.exists() && image_file_info.isFile()) { QFile mcmeta_file(image_file_info.filePath()); if (!mcmeta_file.open(QIODevice::ReadOnly)) - return; + return false; // can't open pack.png file auto data = mcmeta_file.readAll(); - ResourcePackUtils::processPackPNG(pack, std::move(data)); + bool pack_png_result = ResourcePackUtils::processPackPNG(pack, std::move(data)); mcmeta_file.close(); + if (!pack_png_result) { + return false; // pack.png invalid + } + } else { + return false; // pack.png does not exists or is not a valid file. } + + return true; // all tests passed } -void processZIP(ResourcePack& pack, ProcessingLevel level) +bool processZIP(ResourcePack& pack, ProcessingLevel level) { Q_ASSERT(pack.type() == ResourceType::ZIPFILE); QuaZip zip(pack.fileinfo().filePath()); if (!zip.open(QuaZip::mdUnzip)) - return; + return false; // can't open zip file QuaZipFile file(&zip); @@ -91,40 +108,57 @@ void processZIP(ResourcePack& pack, ProcessingLevel level) if (!file.open(QIODevice::ReadOnly)) { qCritical() << "Failed to open file in zip."; zip.close(); - return; + return false; } auto data = file.readAll(); - ResourcePackUtils::processMCMeta(pack, std::move(data)); + bool mcmeta_result = ResourcePackUtils::processMCMeta(pack, std::move(data)); file.close(); + if (!mcmeta_result) { + return false; // mcmeta invalid + } + } else { + return false; // could not set pack.mcmeta as current file. + } + + QuaZipDir zipDir(&zip); + if (!zipDir.exists("/assets")) { + return false; // assets dir does not exists at zip root } if (level == ProcessingLevel::BasicInfoOnly) { zip.close(); - return; + return true; // only need basic info already checked } if (zip.setCurrentFile("pack.png")) { if (!file.open(QIODevice::ReadOnly)) { qCritical() << "Failed to open file in zip."; zip.close(); - return; + return false; } auto data = file.readAll(); - ResourcePackUtils::processPackPNG(pack, std::move(data)); + bool pack_png_result = ResourcePackUtils::processPackPNG(pack, std::move(data)); file.close(); + if (!pack_png_result) { + return false; // pack.png invalid + } + } else { + return false; // could not set pack.mcmeta as current file. } zip.close(); + + return true; } // https://minecraft.fandom.com/wiki/Tutorials/Creating_a_resource_pack#Formatting_pack.mcmeta -void processMCMeta(ResourcePack& pack, QByteArray&& raw_data) +bool processMCMeta(ResourcePack& pack, QByteArray&& raw_data) { try { auto json_doc = QJsonDocument::fromJson(raw_data); @@ -134,17 +168,21 @@ void processMCMeta(ResourcePack& pack, QByteArray&& raw_data) pack.setDescription(Json::ensureString(pack_obj, "description", "")); } catch (Json::JsonException& e) { qWarning() << "JsonException: " << e.what() << e.cause(); + return false; } + return true; } -void processPackPNG(ResourcePack& pack, QByteArray&& raw_data) +bool processPackPNG(ResourcePack& pack, QByteArray&& raw_data) { auto img = QImage::fromData(raw_data); if (!img.isNull()) { pack.setImage(img); } else { qWarning() << "Failed to parse pack.png."; + return false; } + return true; } bool validate(QFileInfo file) diff --git a/launcher/minecraft/mod/tasks/LocalResourcePackParseTask.h b/launcher/minecraft/mod/tasks/LocalResourcePackParseTask.h index 69dbd6ad..d0c24c2b 100644 --- a/launcher/minecraft/mod/tasks/LocalResourcePackParseTask.h +++ b/launcher/minecraft/mod/tasks/LocalResourcePackParseTask.h @@ -31,11 +31,11 @@ enum class ProcessingLevel { Full, BasicInfoOnly }; bool process(ResourcePack& pack, ProcessingLevel level = ProcessingLevel::Full); -void processZIP(ResourcePack& pack, ProcessingLevel level = ProcessingLevel::Full); -void processFolder(ResourcePack& pack, ProcessingLevel level = ProcessingLevel::Full); +bool processZIP(ResourcePack& pack, ProcessingLevel level = ProcessingLevel::Full); +bool processFolder(ResourcePack& pack, ProcessingLevel level = ProcessingLevel::Full); -void processMCMeta(ResourcePack& pack, QByteArray&& raw_data); -void processPackPNG(ResourcePack& pack, QByteArray&& raw_data); +bool processMCMeta(ResourcePack& pack, QByteArray&& raw_data); +bool processPackPNG(ResourcePack& pack, QByteArray&& raw_data); /** Checks whether a file is valid as a resource pack or not. */ bool validate(QFileInfo file); diff --git a/launcher/minecraft/mod/tasks/LocalTexturePackParseTask.cpp b/launcher/minecraft/mod/tasks/LocalTexturePackParseTask.cpp index adb19aca..e4492f12 100644 --- a/launcher/minecraft/mod/tasks/LocalTexturePackParseTask.cpp +++ b/launcher/minecraft/mod/tasks/LocalTexturePackParseTask.cpp @@ -32,18 +32,16 @@ bool process(TexturePack& pack, ProcessingLevel level) { switch (pack.type()) { case ResourceType::FOLDER: - TexturePackUtils::processFolder(pack, level); - return true; + return TexturePackUtils::processFolder(pack, level); case ResourceType::ZIPFILE: - TexturePackUtils::processZIP(pack, level); - return true; + return TexturePackUtils::processZIP(pack, level); default: qWarning() << "Invalid type for resource pack parse task!"; return false; } } -void processFolder(TexturePack& pack, ProcessingLevel level) +bool processFolder(TexturePack& pack, ProcessingLevel level) { Q_ASSERT(pack.type() == ResourceType::FOLDER); @@ -51,39 +49,51 @@ void processFolder(TexturePack& pack, ProcessingLevel level) if (mcmeta_file_info.isFile()) { QFile mcmeta_file(mcmeta_file_info.filePath()); if (!mcmeta_file.open(QIODevice::ReadOnly)) - return; + return false; auto data = mcmeta_file.readAll(); - TexturePackUtils::processPackTXT(pack, std::move(data)); + bool packTXT_result = TexturePackUtils::processPackTXT(pack, std::move(data)); mcmeta_file.close(); + if (!packTXT_result) { + return false; + } + } else { + return false; } if (level == ProcessingLevel::BasicInfoOnly) - return; + return true; QFileInfo image_file_info(FS::PathCombine(pack.fileinfo().filePath(), "pack.png")); if (image_file_info.isFile()) { QFile mcmeta_file(image_file_info.filePath()); if (!mcmeta_file.open(QIODevice::ReadOnly)) - return; + return false; auto data = mcmeta_file.readAll(); - TexturePackUtils::processPackPNG(pack, std::move(data)); + bool packPNG_result = TexturePackUtils::processPackPNG(pack, std::move(data)); mcmeta_file.close(); + if (!packPNG_result) { + return false; + } + } else { + return false; } + + return true; } -void processZIP(TexturePack& pack, ProcessingLevel level) +bool processZIP(TexturePack& pack, ProcessingLevel level) { Q_ASSERT(pack.type() == ResourceType::ZIPFILE); QuaZip zip(pack.fileinfo().filePath()); if (!zip.open(QuaZip::mdUnzip)) - return; + return false; QuaZipFile file(&zip); @@ -91,51 +101,62 @@ void processZIP(TexturePack& pack, ProcessingLevel level) if (!file.open(QIODevice::ReadOnly)) { qCritical() << "Failed to open file in zip."; zip.close(); - return; + return false; } auto data = file.readAll(); - TexturePackUtils::processPackTXT(pack, std::move(data)); + bool packTXT_result = TexturePackUtils::processPackTXT(pack, std::move(data)); file.close(); + if (!packTXT_result) { + return false; + } } if (level == ProcessingLevel::BasicInfoOnly) { zip.close(); - return; + return false; } if (zip.setCurrentFile("pack.png")) { if (!file.open(QIODevice::ReadOnly)) { qCritical() << "Failed to open file in zip."; zip.close(); - return; + return false; } auto data = file.readAll(); - TexturePackUtils::processPackPNG(pack, std::move(data)); + bool packPNG_result = TexturePackUtils::processPackPNG(pack, std::move(data)); file.close(); + if (!packPNG_result) { + return false; + } } zip.close(); + + return true; } -void processPackTXT(TexturePack& pack, QByteArray&& raw_data) +bool processPackTXT(TexturePack& pack, QByteArray&& raw_data) { pack.setDescription(QString(raw_data)); + return true; } -void processPackPNG(TexturePack& pack, QByteArray&& raw_data) +bool processPackPNG(TexturePack& pack, QByteArray&& raw_data) { auto img = QImage::fromData(raw_data); if (!img.isNull()) { pack.setImage(img); } else { qWarning() << "Failed to parse pack.png."; + return false; } + return true; } bool validate(QFileInfo file) diff --git a/launcher/minecraft/mod/tasks/LocalTexturePackParseTask.h b/launcher/minecraft/mod/tasks/LocalTexturePackParseTask.h index 9f7aab75..1589f8cb 100644 --- a/launcher/minecraft/mod/tasks/LocalTexturePackParseTask.h +++ b/launcher/minecraft/mod/tasks/LocalTexturePackParseTask.h @@ -32,11 +32,11 @@ enum class ProcessingLevel { Full, BasicInfoOnly }; bool process(TexturePack& pack, ProcessingLevel level = ProcessingLevel::Full); -void processZIP(TexturePack& pack, ProcessingLevel level = ProcessingLevel::Full); -void processFolder(TexturePack& pack, ProcessingLevel level = ProcessingLevel::Full); +bool processZIP(TexturePack& pack, ProcessingLevel level = ProcessingLevel::Full); +bool processFolder(TexturePack& pack, ProcessingLevel level = ProcessingLevel::Full); -void processPackTXT(TexturePack& pack, QByteArray&& raw_data); -void processPackPNG(TexturePack& pack, QByteArray&& raw_data); +bool processPackTXT(TexturePack& pack, QByteArray&& raw_data); +bool processPackPNG(TexturePack& pack, QByteArray&& raw_data); /** Checks whether a file is valid as a texture pack or not. */ bool validate(QFileInfo file); diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 630f1200..be33b8db 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -27,6 +27,9 @@ ecm_add_test(ResourcePackParse_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VER ecm_add_test(TexturePackParse_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test TEST_NAME TexturePackParse) +ecm_add_test(DataPackParse_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test + TEST_NAME DataPackParse) + ecm_add_test(ParseUtils_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test TEST_NAME ParseUtils) diff --git a/tests/DataPackParse_test.cpp b/tests/DataPackParse_test.cpp new file mode 100644 index 00000000..7307035f --- /dev/null +++ b/tests/DataPackParse_test.cpp @@ -0,0 +1,76 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 flowln + * + * 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 +#include + +#include + +#include +#include + +class DataPackParseTest : public QObject { + Q_OBJECT + + private slots: + void test_parseZIP() + { + QString source = QFINDTESTDATA("testdata/DataPackParse"); + + QString zip_dp = FS::PathCombine(source, "test_data_pack_boogaloo.zip"); + DataPack pack { QFileInfo(zip_dp) }; + + bool valid = DataPackUtils::processZIP(pack); + + QVERIFY(pack.packFormat() == 4); + QVERIFY(pack.description() == "Some data pack 2 boobgaloo"); + QVERIFY(valid == true); + } + + void test_parseFolder() + { + QString source = QFINDTESTDATA("testdata/DataPackParse"); + + QString folder_dp = FS::PathCombine(source, "test_folder"); + DataPack pack { QFileInfo(folder_dp) }; + + bool valid = DataPackUtils::processFolder(pack); + + QVERIFY(pack.packFormat() == 10); + QVERIFY(pack.description() == "Some data pack, maybe"); + QVERIFY(valid == true); + } + + void test_parseFolder2() + { + QString source = QFINDTESTDATA("testdata/DataPackParse"); + + QString folder_dp = FS::PathCombine(source, "another_test_folder"); + DataPack pack { QFileInfo(folder_dp) }; + + bool valid = DataPackUtils::process(pack); + + QVERIFY(pack.packFormat() == 6); + QVERIFY(pack.description() == "Some data pack three, leaves on the tree"); + QVERIFY(valid == true); + } +}; + +QTEST_GUILESS_MAIN(DataPackParseTest) + +#include "DataPackParse_test.moc" diff --git a/tests/testdata/DataPackParse/another_test_folder/pack.mcmeta b/tests/testdata/DataPackParse/another_test_folder/pack.mcmeta new file mode 100644 index 0000000000000000000000000000000000000000..5509d007bb14d1019563189ba3d5296f545b883d GIT binary patch literal 104 zcmXZTI|_g>5QO1Arx;SFw(uGrK$c`h(L6{Nn~=M^qTS2~e>Z?F;B)mtTj@04q!%HUIzs literal 0 HcmV?d00001 diff --git a/tests/testdata/DataPackParse/test_data_pack_boogaloo.zip b/tests/testdata/DataPackParse/test_data_pack_boogaloo.zip new file mode 100644 index 0000000000000000000000000000000000000000..cb0b9f3c69fe8722e31224c154edf66c39fa6cbd GIT binary patch literal 898 zcmWIWW@Zs#0D;^EouOa`lwbwYDTyVC`T;nVaKn_Ol;-AE;!!Aos<0$6y%>*bQ7o!6 zOHy<3XpzUIB`rTczMv>SKMhIq<+-RnRVS=DDX~Z|t2jRo5*ADh91I5YJ40uf#RT{O zHAew4C@cyRle6`5lXFu`5?4P93JB2h@HrQ@DQE@TriFWcZ27b3&Jm#n3p)8+OjNg8 z?9|x2K*iXeUt^WXocXg?O_&rhX$3>jx`ZVYrp%u|W!{X*^VhS4#Gej5&B_qq&B$cW zj60TqHiN-iM-T;#Gu&E0E`@=&j>juRt47_!$ z0y5EcL*p2?5ujLxfwzupflRDMAjdhvFl@07Gi*uYE5e2$(g4IzT&VzIs6Hb~nh5Y_ SWdljF1K|mv)Dj?OU;qF;qQe^i literal 0 HcmV?d00001 diff --git a/tests/testdata/DataPackParse/test_folder/pack.mcmeta b/tests/testdata/DataPackParse/test_folder/pack.mcmeta new file mode 100644 index 0000000000000000000000000000000000000000..dbfc7e9b871808e54e2c2bf05e6de02d443fd0a3 GIT binary patch literal 86 zcmb>CQczGTNKDRFvQnr9vZ1{AwEUvn#1f#Op@9xWI3=|>xhS)sBr`t`D6ABmpPQNJ? literal 0 HcmV?d00001 From 25e23e50cadf8e72105ca70e347d65595d2d3f16 Mon Sep 17 00:00:00 2001 From: Rachel Powers <508861+Ryex@users.noreply.github.com> Date: Fri, 9 Dec 2022 21:15:39 -0700 Subject: [PATCH 017/152] fix: force add of ignored testdata files Signed-off-by: Rachel Powers <508861+Ryex@users.noreply.github.com> --- .../data/dummy/tags/item/foo_proof/bar.json | Bin 0 -> 2 bytes .../data/dummy/tags/item/foo_proof/bar.json | Bin 0 -> 2 bytes 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 tests/testdata/DataPackParse/another_test_folder/data/dummy/tags/item/foo_proof/bar.json create mode 100644 tests/testdata/DataPackParse/test_folder/data/dummy/tags/item/foo_proof/bar.json diff --git a/tests/testdata/DataPackParse/another_test_folder/data/dummy/tags/item/foo_proof/bar.json b/tests/testdata/DataPackParse/another_test_folder/data/dummy/tags/item/foo_proof/bar.json new file mode 100644 index 0000000000000000000000000000000000000000..9e26dfeeb6e641a33dae4961196235bdb965b21b GIT binary patch literal 2 Jcmb=f1ponc0Qmp_ literal 0 HcmV?d00001 diff --git a/tests/testdata/DataPackParse/test_folder/data/dummy/tags/item/foo_proof/bar.json b/tests/testdata/DataPackParse/test_folder/data/dummy/tags/item/foo_proof/bar.json new file mode 100644 index 0000000000000000000000000000000000000000..9e26dfeeb6e641a33dae4961196235bdb965b21b GIT binary patch literal 2 Jcmb=f1ponc0Qmp_ literal 0 HcmV?d00001 From 878614ff68163bbc95cbfc35611765f21a83bfed Mon Sep 17 00:00:00 2001 From: Rachel Powers <508861+Ryex@users.noreply.github.com> Date: Sat, 10 Dec 2022 00:52:50 -0700 Subject: [PATCH 018/152] feat: add a `ModUtils::validate` moves the reading of mod files into `ModUtils` namespace Signed-off-by: Rachel Powers <508861+Ryex@users.noreply.github.com> --- launcher/minecraft/mod/DataPack.cpp | 2 - launcher/minecraft/mod/Mod.cpp | 10 ++ launcher/minecraft/mod/Mod.h | 3 + launcher/minecraft/mod/ModDetails.h | 6 +- .../mod/tasks/LocalDataPackParseTask.h | 5 +- .../minecraft/mod/tasks/LocalModParseTask.cpp | 153 +++++++++++------- .../minecraft/mod/tasks/LocalModParseTask.h | 19 +++ .../mod/tasks/LocalShaderPackParseTask copy.h | 0 .../mod/tasks/LocalShaderPackParseTask.h | 0 9 files changed, 136 insertions(+), 62 deletions(-) create mode 100644 launcher/minecraft/mod/tasks/LocalShaderPackParseTask copy.h create mode 100644 launcher/minecraft/mod/tasks/LocalShaderPackParseTask.h diff --git a/launcher/minecraft/mod/DataPack.cpp b/launcher/minecraft/mod/DataPack.cpp index 3f275160..6c333285 100644 --- a/launcher/minecraft/mod/DataPack.cpp +++ b/launcher/minecraft/mod/DataPack.cpp @@ -27,8 +27,6 @@ #include "Version.h" -#include "minecraft/mod/tasks/LocalDataPackParseTask.h" - // Values taken from: // https://minecraft.fandom.com/wiki/Tutorials/Creating_a_data_pack#%22pack_format%22 static const QMap> s_pack_format_versions = { diff --git a/launcher/minecraft/mod/Mod.cpp b/launcher/minecraft/mod/Mod.cpp index 39023f69..8b00354d 100644 --- a/launcher/minecraft/mod/Mod.cpp +++ b/launcher/minecraft/mod/Mod.cpp @@ -43,6 +43,7 @@ #include "MetadataHandler.h" #include "Version.h" +#include "minecraft/mod/ModDetails.h" Mod::Mod(const QFileInfo& file) : Resource(file), m_local_details() { @@ -68,6 +69,10 @@ void Mod::setMetadata(std::shared_ptr&& metadata) m_local_details.metadata = metadata; } +void Mod::setDetails(const ModDetails& details) { + m_local_details = details; +} + std::pair Mod::compare(const Resource& other, SortType type) const { auto cast_other = dynamic_cast(&other); @@ -190,3 +195,8 @@ void Mod::finishResolvingWithDetails(ModDetails&& details) if (metadata) setMetadata(std::move(metadata)); } + +bool Mod::valid() const +{ + return !m_local_details.mod_id.isEmpty(); +} \ No newline at end of file diff --git a/launcher/minecraft/mod/Mod.h b/launcher/minecraft/mod/Mod.h index f336bec4..b6d264fe 100644 --- a/launcher/minecraft/mod/Mod.h +++ b/launcher/minecraft/mod/Mod.h @@ -68,6 +68,9 @@ public: void setStatus(ModStatus status); void setMetadata(std::shared_ptr&& metadata); void setMetadata(const Metadata::ModStruct& metadata) { setMetadata(std::make_shared(metadata)); } + void setDetails(const ModDetails& details); + + bool valid() const override; [[nodiscard]] auto compare(Resource const& other, SortType type) const -> std::pair override; [[nodiscard]] bool applyFilter(QRegularExpression filter) const override; diff --git a/launcher/minecraft/mod/ModDetails.h b/launcher/minecraft/mod/ModDetails.h index dd84b0a3..176e4fc1 100644 --- a/launcher/minecraft/mod/ModDetails.h +++ b/launcher/minecraft/mod/ModDetails.h @@ -81,7 +81,7 @@ struct ModDetails ModDetails() = default; /** Metadata should be handled manually to properly set the mod status. */ - ModDetails(ModDetails& other) + ModDetails(const ModDetails& other) : mod_id(other.mod_id) , name(other.name) , version(other.version) @@ -92,7 +92,7 @@ struct ModDetails , status(other.status) {} - ModDetails& operator=(ModDetails& other) + ModDetails& operator=(const ModDetails& other) { this->mod_id = other.mod_id; this->name = other.name; @@ -106,7 +106,7 @@ struct ModDetails return *this; } - ModDetails& operator=(ModDetails&& other) + ModDetails& operator=(const ModDetails&& other) { this->mod_id = other.mod_id; this->name = other.name; diff --git a/launcher/minecraft/mod/tasks/LocalDataPackParseTask.h b/launcher/minecraft/mod/tasks/LocalDataPackParseTask.h index ee64df46..9f6ece5c 100644 --- a/launcher/minecraft/mod/tasks/LocalDataPackParseTask.h +++ b/launcher/minecraft/mod/tasks/LocalDataPackParseTask.h @@ -39,9 +39,10 @@ bool processFolder(DataPack& pack, ProcessingLevel level = ProcessingLevel::Full bool processMCMeta(DataPack& pack, QByteArray&& raw_data); -/** Checks whether a file is valid as a resource pack or not. */ +/** Checks whether a file is valid as a data pack or not. */ bool validate(QFileInfo file); -} // namespace ResourcePackUtils + +} // namespace DataPackUtils class LocalDataPackParseTask : public Task { Q_OBJECT diff --git a/launcher/minecraft/mod/tasks/LocalModParseTask.cpp b/launcher/minecraft/mod/tasks/LocalModParseTask.cpp index 774f6114..e8fd39b6 100644 --- a/launcher/minecraft/mod/tasks/LocalModParseTask.cpp +++ b/launcher/minecraft/mod/tasks/LocalModParseTask.cpp @@ -11,9 +11,10 @@ #include "FileSystem.h" #include "Json.h" +#include "minecraft/mod/ModDetails.h" #include "settings/INIFile.h" -namespace { +namespace ModUtils { // NEW format // https://github.com/MinecraftForge/FML/wiki/FML-mod-information-file/6f62b37cea040daf350dc253eae6326dd9c822c3 @@ -283,35 +284,45 @@ ModDetails ReadLiteModInfo(QByteArray contents) return details; } -} // namespace +bool process(Mod& mod, ProcessingLevel level) { + switch (mod.type()) { + case ResourceType::FOLDER: + return processFolder(mod, level); + case ResourceType::ZIPFILE: + return processZIP(mod, level); + case ResourceType::LITEMOD: + return processLitemod(mod); + default: + qWarning() << "Invalid type for resource pack parse task!"; + return false; + } +} -LocalModParseTask::LocalModParseTask(int token, ResourceType type, const QFileInfo& modFile) - : Task(nullptr, false), m_token(token), m_type(type), m_modFile(modFile), m_result(new Result()) -{} +bool processZIP(Mod& mod, ProcessingLevel level) { -void LocalModParseTask::processAsZip() -{ - QuaZip zip(m_modFile.filePath()); + ModDetails details; + + QuaZip zip(mod.fileinfo().filePath()); if (!zip.open(QuaZip::mdUnzip)) - return; + return false; QuaZipFile file(&zip); if (zip.setCurrentFile("META-INF/mods.toml")) { if (!file.open(QIODevice::ReadOnly)) { zip.close(); - return; + return false; } - m_result->details = ReadMCModTOML(file.readAll()); + details = ReadMCModTOML(file.readAll()); file.close(); - + // to replace ${file.jarVersion} with the actual version, as needed - if (m_result->details.version == "${file.jarVersion}") { + if (details.version == "${file.jarVersion}") { if (zip.setCurrentFile("META-INF/MANIFEST.MF")) { if (!file.open(QIODevice::ReadOnly)) { zip.close(); - return; + return false; } // quick and dirty line-by-line parser @@ -330,93 +341,134 @@ void LocalModParseTask::processAsZip() manifestVersion = "NONE"; } - m_result->details.version = manifestVersion; + details.version = manifestVersion; file.close(); } } + zip.close(); - return; + mod.setDetails(details); + + return true; } else if (zip.setCurrentFile("mcmod.info")) { if (!file.open(QIODevice::ReadOnly)) { zip.close(); - return; + return false; } - m_result->details = ReadMCModInfo(file.readAll()); + details = ReadMCModInfo(file.readAll()); file.close(); zip.close(); - return; + + mod.setDetails(details); + return true; } else if (zip.setCurrentFile("quilt.mod.json")) { if (!file.open(QIODevice::ReadOnly)) { zip.close(); - return; + return false; } - m_result->details = ReadQuiltModInfo(file.readAll()); + details = ReadQuiltModInfo(file.readAll()); file.close(); zip.close(); - return; + + mod.setDetails(details); + return true; } else if (zip.setCurrentFile("fabric.mod.json")) { if (!file.open(QIODevice::ReadOnly)) { zip.close(); - return; + return false; } - m_result->details = ReadFabricModInfo(file.readAll()); + details = ReadFabricModInfo(file.readAll()); file.close(); zip.close(); - return; + + mod.setDetails(details); + return true; } else if (zip.setCurrentFile("forgeversion.properties")) { if (!file.open(QIODevice::ReadOnly)) { zip.close(); - return; + return false; } - m_result->details = ReadForgeInfo(file.readAll()); + details = ReadForgeInfo(file.readAll()); file.close(); zip.close(); - return; + + mod.setDetails(details); + return true; } zip.close(); + return false; // no valid mod found in archive } -void LocalModParseTask::processAsFolder() -{ - QFileInfo mcmod_info(FS::PathCombine(m_modFile.filePath(), "mcmod.info")); - if (mcmod_info.isFile()) { +bool processFolder(Mod& mod, ProcessingLevel level) { + + ModDetails details; + + QFileInfo mcmod_info(FS::PathCombine(mod.fileinfo().filePath(), "mcmod.info")); + if (mcmod_info.exists() && mcmod_info.isFile()) { QFile mcmod(mcmod_info.filePath()); if (!mcmod.open(QIODevice::ReadOnly)) - return; + return false; auto data = mcmod.readAll(); if (data.isEmpty() || data.isNull()) - return; - m_result->details = ReadMCModInfo(data); + return false; + details = ReadMCModInfo(data); + + mod.setDetails(details); + return true; } + + return false; // no valid mcmod.info file found } -void LocalModParseTask::processAsLitemod() -{ - QuaZip zip(m_modFile.filePath()); +bool processLitemod(Mod& mod, ProcessingLevel level) { + + ModDetails details; + + QuaZip zip(mod.fileinfo().filePath()); if (!zip.open(QuaZip::mdUnzip)) - return; + return false; QuaZipFile file(&zip); if (zip.setCurrentFile("litemod.json")) { if (!file.open(QIODevice::ReadOnly)) { zip.close(); - return; + return false; } - m_result->details = ReadLiteModInfo(file.readAll()); + details = ReadLiteModInfo(file.readAll()); file.close(); + + mod.setDetails(details); + return true; } zip.close(); + + return false; // no valid litemod.json found in archive } +/** Checks whether a file is valid as a mod or not. */ +bool validate(QFileInfo file) { + + Mod mod{ file }; + return ModUtils::process(mod, ProcessingLevel::BasicInfoOnly) && mod.valid(); +} + +} // namespace ModUtils + + +LocalModParseTask::LocalModParseTask(int token, ResourceType type, const QFileInfo& modFile) + : Task(nullptr, false), m_token(token), m_type(type), m_modFile(modFile), m_result(new Result()) +{} + + bool LocalModParseTask::abort() { m_aborted.store(true); @@ -424,20 +476,11 @@ bool LocalModParseTask::abort() } void LocalModParseTask::executeTask() -{ - switch (m_type) { - case ResourceType::ZIPFILE: - processAsZip(); - break; - case ResourceType::FOLDER: - processAsFolder(); - break; - case ResourceType::LITEMOD: - processAsLitemod(); - break; - default: - break; - } +{ + Mod mod{ m_modFile }; + ModUtils::process(mod, ModUtils::ProcessingLevel::Full); + + m_result->details = mod.details(); if (m_aborted) emit finished(); diff --git a/launcher/minecraft/mod/tasks/LocalModParseTask.h b/launcher/minecraft/mod/tasks/LocalModParseTask.h index 413eb2d1..c9512166 100644 --- a/launcher/minecraft/mod/tasks/LocalModParseTask.h +++ b/launcher/minecraft/mod/tasks/LocalModParseTask.h @@ -8,6 +8,25 @@ #include "tasks/Task.h" +namespace ModUtils { + +ModDetails ReadFabricModInfo(QByteArray contents); +ModDetails ReadQuiltModInfo(QByteArray contents); +ModDetails ReadForgeInfo(QByteArray contents); +ModDetails ReadLiteModInfo(QByteArray contents); + +enum class ProcessingLevel { Full, BasicInfoOnly }; + +bool process(Mod& mod, ProcessingLevel level = ProcessingLevel::Full); + +bool processZIP(Mod& mod, ProcessingLevel level = ProcessingLevel::Full); +bool processFolder(Mod& mod, ProcessingLevel level = ProcessingLevel::Full); +bool processLitemod(Mod& mod, ProcessingLevel level = ProcessingLevel::Full); + +/** Checks whether a file is valid as a mod or not. */ +bool validate(QFileInfo file); +} // namespace ModUtils + class LocalModParseTask : public Task { Q_OBJECT diff --git a/launcher/minecraft/mod/tasks/LocalShaderPackParseTask copy.h b/launcher/minecraft/mod/tasks/LocalShaderPackParseTask copy.h new file mode 100644 index 00000000..e69de29b diff --git a/launcher/minecraft/mod/tasks/LocalShaderPackParseTask.h b/launcher/minecraft/mod/tasks/LocalShaderPackParseTask.h new file mode 100644 index 00000000..e69de29b From ccfe605920fba14d9e798bb26642d22ee45fe860 Mon Sep 17 00:00:00 2001 From: Rachel Powers <508861+Ryex@users.noreply.github.com> Date: Sat, 24 Dec 2022 11:22:06 -0700 Subject: [PATCH 019/152] feat: add shaderpack validation Signed-off-by: Rachel Powers <508861+Ryex@users.noreply.github.com> --- launcher/minecraft/mod/ShaderPack.cpp | 39 ++++++ launcher/minecraft/mod/ShaderPack.h | 66 ++++++++++ .../mod/tasks/LocalDataPackParseTask.h | 2 +- .../mod/tasks/LocalResourcePackParseTask.cpp | 8 +- .../mod/tasks/LocalShaderPackParseTask copy.h | 0 .../mod/tasks/LocalShaderPackParseTask.cpp | 116 ++++++++++++++++++ .../mod/tasks/LocalShaderPackParseTask.h | 63 ++++++++++ 7 files changed, 289 insertions(+), 5 deletions(-) create mode 100644 launcher/minecraft/mod/ShaderPack.cpp create mode 100644 launcher/minecraft/mod/ShaderPack.h delete mode 100644 launcher/minecraft/mod/tasks/LocalShaderPackParseTask copy.h create mode 100644 launcher/minecraft/mod/tasks/LocalShaderPackParseTask.cpp diff --git a/launcher/minecraft/mod/ShaderPack.cpp b/launcher/minecraft/mod/ShaderPack.cpp new file mode 100644 index 00000000..b8d427c7 --- /dev/null +++ b/launcher/minecraft/mod/ShaderPack.cpp @@ -0,0 +1,39 @@ + +// SPDX-FileCopyrightText: 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> +// +// SPDX-License-Identifier: GPL-3.0-only + +/* + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "ShaderPack.h" + +#include "minecraft/mod/tasks/LocalShaderPackParseTask.h" + + +void ShaderPack::setPackFormat(ShaderPackFormat new_format) +{ + QMutexLocker locker(&m_data_lock); + + + m_pack_format = new_format; +} + +bool ShaderPack::valid() const +{ + return m_pack_format != ShaderPackFormat::INVALID; +} diff --git a/launcher/minecraft/mod/ShaderPack.h b/launcher/minecraft/mod/ShaderPack.h new file mode 100644 index 00000000..e6ee0757 --- /dev/null +++ b/launcher/minecraft/mod/ShaderPack.h @@ -0,0 +1,66 @@ +// SPDX-FileCopyrightText: 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> +// +// SPDX-License-Identifier: GPL-3.0-only + +/* + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include "Resource.h" + +/* Info: + * Currently For Optifine / Iris shader packs, + * could be expanded to support others should they exsist? + * + * This class and enum are mostly here as placeholders for validating + * that a shaderpack exsists and is in the right format, + * namely that they contain a folder named 'shaders'. + * + * In the technical sense it would be possible to parse files like `shaders/shaders.properties` + * to get information like the availble profiles but this is not all that usefull without more knoledge of the + * shader mod used to be able to change settings + * + */ + +#include + +enum ShaderPackFormat { + VALID, + INVALID +}; + +class ShaderPack : public Resource { + Q_OBJECT + public: + using Ptr = shared_qobject_ptr; + + [[nodiscard]] ShaderPackFormat packFormat() const { return m_pack_format; } + + ShaderPack(QObject* parent = nullptr) : Resource(parent) {} + ShaderPack(QFileInfo file_info) : Resource(file_info) {} + + /** Thread-safe. */ + void setPackFormat(ShaderPackFormat new_format); + + bool valid() const override; + + protected: + mutable QMutex m_data_lock; + + ShaderPackFormat m_pack_format = ShaderPackFormat::INVALID; +}; diff --git a/launcher/minecraft/mod/tasks/LocalDataPackParseTask.h b/launcher/minecraft/mod/tasks/LocalDataPackParseTask.h index 9f6ece5c..54e3d398 100644 --- a/launcher/minecraft/mod/tasks/LocalDataPackParseTask.h +++ b/launcher/minecraft/mod/tasks/LocalDataPackParseTask.h @@ -47,7 +47,7 @@ bool validate(QFileInfo file); class LocalDataPackParseTask : public Task { Q_OBJECT public: - LocalDataPackParseTask(int token, DataPack& rp); + LocalDataPackParseTask(int token, DataPack& dp); [[nodiscard]] bool canAbort() const override { return true; } bool abort() override; diff --git a/launcher/minecraft/mod/tasks/LocalResourcePackParseTask.cpp b/launcher/minecraft/mod/tasks/LocalResourcePackParseTask.cpp index 18d7383d..2c41c9ae 100644 --- a/launcher/minecraft/mod/tasks/LocalResourcePackParseTask.cpp +++ b/launcher/minecraft/mod/tasks/LocalResourcePackParseTask.cpp @@ -75,15 +75,15 @@ bool processFolder(ResourcePack& pack, ProcessingLevel level) QFileInfo image_file_info(FS::PathCombine(pack.fileinfo().filePath(), "pack.png")); if (image_file_info.exists() && image_file_info.isFile()) { - QFile mcmeta_file(image_file_info.filePath()); - if (!mcmeta_file.open(QIODevice::ReadOnly)) + QFile pack_png_file(image_file_info.filePath()); + if (!pack_png_file.open(QIODevice::ReadOnly)) return false; // can't open pack.png file - auto data = mcmeta_file.readAll(); + auto data = pack_png_file.readAll(); bool pack_png_result = ResourcePackUtils::processPackPNG(pack, std::move(data)); - mcmeta_file.close(); + pack_png_file.close(); if (!pack_png_result) { return false; // pack.png invalid } diff --git a/launcher/minecraft/mod/tasks/LocalShaderPackParseTask copy.h b/launcher/minecraft/mod/tasks/LocalShaderPackParseTask copy.h deleted file mode 100644 index e69de29b..00000000 diff --git a/launcher/minecraft/mod/tasks/LocalShaderPackParseTask.cpp b/launcher/minecraft/mod/tasks/LocalShaderPackParseTask.cpp new file mode 100644 index 00000000..088853b9 --- /dev/null +++ b/launcher/minecraft/mod/tasks/LocalShaderPackParseTask.cpp @@ -0,0 +1,116 @@ +// SPDX-FileCopyrightText: 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> +// +// SPDX-License-Identifier: GPL-3.0-only + +/* + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "LocalShaderPackParseTask.h" + +#include "FileSystem.h" + +#include +#include +#include + +namespace ShaderPackUtils { + +bool process(ShaderPack& pack, ProcessingLevel level) +{ + switch (pack.type()) { + case ResourceType::FOLDER: + return ShaderPackUtils::processFolder(pack, level); + case ResourceType::ZIPFILE: + return ShaderPackUtils::processZIP(pack, level); + default: + qWarning() << "Invalid type for shader pack parse task!"; + return false; + } +} + +bool processFolder(ShaderPack& pack, ProcessingLevel level) +{ + Q_ASSERT(pack.type() == ResourceType::FOLDER); + + QFileInfo shaders_dir_info(FS::PathCombine(pack.fileinfo().filePath(), "shaders")); + if (!shaders_dir_info.exists() || !shaders_dir_info.isDir()) { + return false; // assets dir does not exists or isn't valid + } + pack.setPackFormat(ShaderPackFormat::VALID); + + if (level == ProcessingLevel::BasicInfoOnly) { + return true; // only need basic info already checked + } + + return true; // all tests passed +} + +bool processZIP(ShaderPack& pack, ProcessingLevel level) +{ + Q_ASSERT(pack.type() == ResourceType::ZIPFILE); + + QuaZip zip(pack.fileinfo().filePath()); + if (!zip.open(QuaZip::mdUnzip)) + return false; // can't open zip file + + QuaZipFile file(&zip); + + QuaZipDir zipDir(&zip); + if (!zipDir.exists("/shaders")) { + return false; // assets dir does not exists at zip root + } + pack.setPackFormat(ShaderPackFormat::VALID); + + if (level == ProcessingLevel::BasicInfoOnly) { + zip.close(); + return true; // only need basic info already checked + } + + zip.close(); + + return true; +} + + +bool validate(QFileInfo file) +{ + ShaderPack sp{ file }; + return ShaderPackUtils::process(sp, ProcessingLevel::BasicInfoOnly) && sp.valid(); +} + +} // namespace ShaderPackUtils + +LocalShaderPackParseTask::LocalShaderPackParseTask(int token, ShaderPack& sp) + : Task(nullptr, false), m_token(token), m_shader_pack(sp) +{} + +bool LocalShaderPackParseTask::abort() +{ + m_aborted = true; + return true; +} + +void LocalShaderPackParseTask::executeTask() +{ + if (!ShaderPackUtils::process(m_shader_pack)) + return; + + if (m_aborted) + emitAborted(); + else + emitSucceeded(); +} diff --git a/launcher/minecraft/mod/tasks/LocalShaderPackParseTask.h b/launcher/minecraft/mod/tasks/LocalShaderPackParseTask.h index e69de29b..5d113508 100644 --- a/launcher/minecraft/mod/tasks/LocalShaderPackParseTask.h +++ b/launcher/minecraft/mod/tasks/LocalShaderPackParseTask.h @@ -0,0 +1,63 @@ +// SPDX-FileCopyrightText: 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> +// +// SPDX-License-Identifier: GPL-3.0-only + +/* + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + + +#pragma once + +#include +#include + +#include "minecraft/mod/ShaderPack.h" + +#include "tasks/Task.h" + +namespace ShaderPackUtils { + +enum class ProcessingLevel { Full, BasicInfoOnly }; + +bool process(ShaderPack& pack, ProcessingLevel level = ProcessingLevel::Full); + +bool processZIP(ShaderPack& pack, ProcessingLevel level = ProcessingLevel::Full); +bool processFolder(ShaderPack& pack, ProcessingLevel level = ProcessingLevel::Full); + +/** Checks whether a file is valid as a resource pack or not. */ +bool validate(QFileInfo file); +} // namespace ShaderPackUtils + +class LocalShaderPackParseTask : public Task { + Q_OBJECT + public: + LocalShaderPackParseTask(int token, ShaderPack& sp); + + [[nodiscard]] bool canAbort() const override { return true; } + bool abort() override; + + void executeTask() override; + + [[nodiscard]] int token() const { return m_token; } + + private: + int m_token; + + ShaderPack& m_shader_pack; + + bool m_aborted = false; +}; From eb31a951a18287f943a1e3d021629dde8b73fd15 Mon Sep 17 00:00:00 2001 From: Rachel Powers <508861+Ryex@users.noreply.github.com> Date: Sat, 24 Dec 2022 15:58:24 -0700 Subject: [PATCH 020/152] feat: worldSave parsing and validation Signed-off-by: Rachel Powers <508861+Ryex@users.noreply.github.com> --- launcher/minecraft/mod/WorldSave.cpp | 45 +++++ launcher/minecraft/mod/WorldSave.h | 67 +++++++ .../mod/tasks/LocalWorldSaveParseTask.cpp | 177 ++++++++++++++++++ .../mod/tasks/LocalWorldSaveParseTask.h | 62 ++++++ 4 files changed, 351 insertions(+) create mode 100644 launcher/minecraft/mod/WorldSave.cpp create mode 100644 launcher/minecraft/mod/WorldSave.h create mode 100644 launcher/minecraft/mod/tasks/LocalWorldSaveParseTask.cpp create mode 100644 launcher/minecraft/mod/tasks/LocalWorldSaveParseTask.h diff --git a/launcher/minecraft/mod/WorldSave.cpp b/launcher/minecraft/mod/WorldSave.cpp new file mode 100644 index 00000000..9a626fc1 --- /dev/null +++ b/launcher/minecraft/mod/WorldSave.cpp @@ -0,0 +1,45 @@ +// SPDX-FileCopyrightText: 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> +// +// SPDX-License-Identifier: GPL-3.0-only + +/* + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "WorldSave.h" + +#include "minecraft/mod/tasks/LocalWorldSaveParseTask.h" + +void WorldSave::setSaveFormat(WorldSaveFormat new_save_format) +{ + QMutexLocker locker(&m_data_lock); + + + m_save_format = new_save_format; +} + +void WorldSave::setSaveDirName(QString dir_name) +{ + QMutexLocker locker(&m_data_lock); + + + m_save_dir_name = dir_name; +} + +bool WorldSave::valid() const +{ + return m_save_format != WorldSaveFormat::INVALID; +} \ No newline at end of file diff --git a/launcher/minecraft/mod/WorldSave.h b/launcher/minecraft/mod/WorldSave.h new file mode 100644 index 00000000..f48f42b9 --- /dev/null +++ b/launcher/minecraft/mod/WorldSave.h @@ -0,0 +1,67 @@ +// SPDX-FileCopyrightText: 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> +// +// SPDX-License-Identifier: GPL-3.0-only + +/* + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include "Resource.h" + +#include + +class Version; + +enum WorldSaveFormat { + SINGLE, + MULTI, + INVALID +}; + +class WorldSave : public Resource { + Q_OBJECT + public: + using Ptr = shared_qobject_ptr; + + WorldSave(QObject* parent = nullptr) : Resource(parent) {} + WorldSave(QFileInfo file_info) : Resource(file_info) {} + + /** Gets the format of the save. */ + [[nodiscard]] WorldSaveFormat saveFormat() const { return m_save_format; } + /** Gets the name of the save dir (first found in multi mode). */ + [[nodiscard]] QString saveDirName() const { return m_save_dir_name; } + + /** Thread-safe. */ + void setSaveFormat(WorldSaveFormat new_save_format); + /** Thread-safe. */ + void setSaveDirName(QString dir_name); + + bool valid() const override; + + + protected: + mutable QMutex m_data_lock; + + /* The 'version' of a resource pack, as defined in the pack.mcmeta file. + * See https://minecraft.fandom.com/wiki/Tutorials/Creating_a_resource_pack#Formatting_pack.mcmeta + */ + WorldSaveFormat m_save_format = WorldSaveFormat::INVALID; + + QString m_save_dir_name; + +}; diff --git a/launcher/minecraft/mod/tasks/LocalWorldSaveParseTask.cpp b/launcher/minecraft/mod/tasks/LocalWorldSaveParseTask.cpp new file mode 100644 index 00000000..5405d308 --- /dev/null +++ b/launcher/minecraft/mod/tasks/LocalWorldSaveParseTask.cpp @@ -0,0 +1,177 @@ + +// SPDX-FileCopyrightText: 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> +// +// SPDX-License-Identifier: GPL-3.0-only + +/* + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "LocalWorldSaveParseTask.h" + +#include "FileSystem.h" + +#include +#include +#include +#include +#include +#include + +namespace WorldSaveUtils { + +bool process(WorldSave& pack, ProcessingLevel level) +{ + switch (pack.type()) { + case ResourceType::FOLDER: + return WorldSaveUtils::processFolder(pack, level); + case ResourceType::ZIPFILE: + return WorldSaveUtils::processZIP(pack, level); + default: + qWarning() << "Invalid type for shader pack parse task!"; + return false; + } +} + + +static std::tuple contains_level_dat(QDir dir, bool saves = false) +{ + for(auto const& entry : dir.entryInfoList()) { + if (!entry.isDir()) { + continue; + } + if (!saves && entry.fileName() == "saves") { + return contains_level_dat(QDir(entry.filePath()), true); + } + QFileInfo level_dat(FS::PathCombine(entry.filePath(), "level.dat")); + if (level_dat.exists() && level_dat.isFile()) { + return std::make_tuple(true, entry.fileName(), saves); + } + } + return std::make_tuple(false, "", saves); +} + + +bool processFolder(WorldSave& save, ProcessingLevel level) +{ + Q_ASSERT(save.type() == ResourceType::FOLDER); + + auto [ found, save_dir_name, found_saves_dir ] = contains_level_dat(QDir(save.fileinfo().filePath())); + + if (!found) { + return false; + } + + save.setSaveDirName(save_dir_name); + + if (found_saves_dir) { + save.setSaveFormat(WorldSaveFormat::MULTI); + } else { + save.setSaveFormat(WorldSaveFormat::SINGLE); + } + + if (level == ProcessingLevel::BasicInfoOnly) { + return true; // only need basic info already checked + } + + // resurved for more intensive processing + + return true; // all tests passed +} + +static std::tuple contains_level_dat(QuaZip& zip) +{ + bool saves = false; + QuaZipDir zipDir(&zip); + if (zipDir.exists("/saves")) { + saves = true; + zipDir.cd("/saves"); + } + + for (auto const& entry : zipDir.entryList()) { + zipDir.cd(entry); + if (zipDir.exists("level.dat")) { + return std::make_tuple(true, entry, saves); + } + zipDir.cd(".."); + } + return std::make_tuple(false, "", saves); +} + +bool processZIP(WorldSave& save, ProcessingLevel level) +{ + Q_ASSERT(save.type() == ResourceType::ZIPFILE); + + QuaZip zip(save.fileinfo().filePath()); + if (!zip.open(QuaZip::mdUnzip)) + return false; // can't open zip file + + auto [ found, save_dir_name, found_saves_dir ] = contains_level_dat(zip); + + + if (!found) { + return false; + } + + save.setSaveDirName(save_dir_name); + + if (found_saves_dir) { + save.setSaveFormat(WorldSaveFormat::MULTI); + } else { + save.setSaveFormat(WorldSaveFormat::SINGLE); + } + + if (level == ProcessingLevel::BasicInfoOnly) { + zip.close(); + return true; // only need basic info already checked + } + + // resurved for more intensive processing + + zip.close(); + + return true; +} + + +bool validate(QFileInfo file) +{ + WorldSave sp{ file }; + return WorldSaveUtils::process(sp, ProcessingLevel::BasicInfoOnly) && sp.valid(); +} + +} // namespace WorldSaveUtils + +LocalWorldSaveParseTask::LocalWorldSaveParseTask(int token, WorldSave& save) + : Task(nullptr, false), m_token(token), m_save(save) +{} + +bool LocalWorldSaveParseTask::abort() +{ + m_aborted = true; + return true; +} + +void LocalWorldSaveParseTask::executeTask() +{ + if (!WorldSaveUtils::process(m_save)) + return; + + if (m_aborted) + emitAborted(); + else + emitSucceeded(); +} diff --git a/launcher/minecraft/mod/tasks/LocalWorldSaveParseTask.h b/launcher/minecraft/mod/tasks/LocalWorldSaveParseTask.h new file mode 100644 index 00000000..44153735 --- /dev/null +++ b/launcher/minecraft/mod/tasks/LocalWorldSaveParseTask.h @@ -0,0 +1,62 @@ +// SPDX-FileCopyrightText: 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> +// +// SPDX-License-Identifier: GPL-3.0-only + +/* + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include +#include + +#include "minecraft/mod/WorldSave.h" + +#include "tasks/Task.h" + +namespace WorldSaveUtils { + +enum class ProcessingLevel { Full, BasicInfoOnly }; + +bool process(WorldSave& save, ProcessingLevel level = ProcessingLevel::Full); + +bool processZIP(WorldSave& pack, ProcessingLevel level = ProcessingLevel::Full); +bool processFolder(WorldSave& pack, ProcessingLevel level = ProcessingLevel::Full); + +bool validate(QFileInfo file); + +} // namespace WorldSaveUtils + +class LocalWorldSaveParseTask : public Task { + Q_OBJECT + public: + LocalWorldSaveParseTask(int token, WorldSave& save); + + [[nodiscard]] bool canAbort() const override { return true; } + bool abort() override; + + void executeTask() override; + + [[nodiscard]] int token() const { return m_token; } + + private: + int m_token; + + WorldSave& m_save; + + bool m_aborted = false; +}; \ No newline at end of file From a7c9b2f172754aa476a23deabe074a649cefdd11 Mon Sep 17 00:00:00 2001 From: Rachel Powers <508861+Ryex@users.noreply.github.com> Date: Sat, 24 Dec 2022 17:43:43 -0700 Subject: [PATCH 021/152] feat: validate world saves Signed-off-by: Rachel Powers <508861+Ryex@users.noreply.github.com> --- launcher/CMakeLists.txt | 8 ++ launcher/minecraft/mod/ShaderPack.h | 2 +- launcher/minecraft/mod/WorldSave.h | 2 +- .../mod/tasks/LocalWorldSaveParseTask.cpp | 3 + tests/CMakeLists.txt | 6 ++ tests/DataPackParse_test.cpp | 7 +- tests/ShaderPackParse_test.cpp | 77 ++++++++++++++ tests/WorldSaveParse_test.cpp | 94 ++++++++++++++++++ .../testdata/ShaderPackParse/shaderpack1.zip | Bin 0 -> 242 bytes .../shaderpack2/shaders/shaders.properties | Bin .../testdata/ShaderPackParse/shaderpack3.zip | Bin 0 -> 128 bytes .../WorldSaveParse/minecraft_save_1.zip | Bin 0 -> 184 bytes .../WorldSaveParse/minecraft_save_2.zip | Bin 0 -> 352 bytes .../minecraft_save_3/world_3/level.dat | Bin .../minecraft_save_4/saves/world_4/level.dat | Bin 15 files changed, 195 insertions(+), 4 deletions(-) create mode 100644 tests/ShaderPackParse_test.cpp create mode 100644 tests/WorldSaveParse_test.cpp create mode 100644 tests/testdata/ShaderPackParse/shaderpack1.zip create mode 100644 tests/testdata/ShaderPackParse/shaderpack2/shaders/shaders.properties create mode 100644 tests/testdata/ShaderPackParse/shaderpack3.zip create mode 100644 tests/testdata/WorldSaveParse/minecraft_save_1.zip create mode 100644 tests/testdata/WorldSaveParse/minecraft_save_2.zip create mode 100644 tests/testdata/WorldSaveParse/minecraft_save_3/world_3/level.dat create mode 100644 tests/testdata/WorldSaveParse/minecraft_save_4/saves/world_4/level.dat diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index c12e6740..853e1c03 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -339,6 +339,10 @@ set(MINECRAFT_SOURCES minecraft/mod/ResourcePackFolderModel.cpp minecraft/mod/TexturePack.h minecraft/mod/TexturePack.cpp + minecraft/mod/ShaderPack.h + minecraft/mod/ShaderPack.cpp + minecraft/mod/WorldSave.h + minecraft/mod/WorldSave.cpp minecraft/mod/TexturePackFolderModel.h minecraft/mod/TexturePackFolderModel.cpp minecraft/mod/ShaderPackFolderModel.h @@ -355,6 +359,10 @@ set(MINECRAFT_SOURCES minecraft/mod/tasks/LocalResourcePackParseTask.cpp minecraft/mod/tasks/LocalTexturePackParseTask.h minecraft/mod/tasks/LocalTexturePackParseTask.cpp + minecraft/mod/tasks/LocalShaderPackParseTask.h + minecraft/mod/tasks/LocalShaderPackParseTask.cpp + minecraft/mod/tasks/LocalWorldSaveParseTask.h + minecraft/mod/tasks/LocalWorldSaveParseTask.cpp # Assets minecraft/AssetsUtils.h diff --git a/launcher/minecraft/mod/ShaderPack.h b/launcher/minecraft/mod/ShaderPack.h index e6ee0757..a0dad7a1 100644 --- a/launcher/minecraft/mod/ShaderPack.h +++ b/launcher/minecraft/mod/ShaderPack.h @@ -39,7 +39,7 @@ #include -enum ShaderPackFormat { +enum class ShaderPackFormat { VALID, INVALID }; diff --git a/launcher/minecraft/mod/WorldSave.h b/launcher/minecraft/mod/WorldSave.h index f48f42b9..f703f34c 100644 --- a/launcher/minecraft/mod/WorldSave.h +++ b/launcher/minecraft/mod/WorldSave.h @@ -27,7 +27,7 @@ class Version; -enum WorldSaveFormat { +enum class WorldSaveFormat { SINGLE, MULTI, INVALID diff --git a/launcher/minecraft/mod/tasks/LocalWorldSaveParseTask.cpp b/launcher/minecraft/mod/tasks/LocalWorldSaveParseTask.cpp index 5405d308..b7f2420a 100644 --- a/launcher/minecraft/mod/tasks/LocalWorldSaveParseTask.cpp +++ b/launcher/minecraft/mod/tasks/LocalWorldSaveParseTask.cpp @@ -121,6 +121,9 @@ bool processZIP(WorldSave& save, ProcessingLevel level) auto [ found, save_dir_name, found_saves_dir ] = contains_level_dat(zip); + if (save_dir_name.endsWith("/")) { + save_dir_name.chop(1); + } if (!found) { return false; diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index be33b8db..9f84a9a7 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -30,6 +30,12 @@ ecm_add_test(TexturePackParse_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERS ecm_add_test(DataPackParse_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test TEST_NAME DataPackParse) +ecm_add_test(ShaderPackParse_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test + TEST_NAME ShaderPackParse) + +ecm_add_test(WorldSaveParse_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test + TEST_NAME WorldSaveParse) + ecm_add_test(ParseUtils_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test TEST_NAME ParseUtils) diff --git a/tests/DataPackParse_test.cpp b/tests/DataPackParse_test.cpp index 7307035f..61ce1e2b 100644 --- a/tests/DataPackParse_test.cpp +++ b/tests/DataPackParse_test.cpp @@ -1,7 +1,10 @@ +// SPDX-FileCopyrightText: 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> +// // SPDX-License-Identifier: GPL-3.0-only + /* - * PolyMC - Minecraft Launcher - * Copyright (c) 2022 flowln + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/tests/ShaderPackParse_test.cpp b/tests/ShaderPackParse_test.cpp new file mode 100644 index 00000000..7df105c6 --- /dev/null +++ b/tests/ShaderPackParse_test.cpp @@ -0,0 +1,77 @@ + +// SPDX-FileCopyrightText: 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> +// +// SPDX-License-Identifier: GPL-3.0-only + +/* + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include +#include + +#include + +#include +#include + +class ShaderPackParseTest : public QObject { + Q_OBJECT + + private slots: + void test_parseZIP() + { + QString source = QFINDTESTDATA("testdata/ShaderPackParse"); + + QString zip_sp = FS::PathCombine(source, "shaderpack1.zip"); + ShaderPack pack { QFileInfo(zip_sp) }; + + bool valid = ShaderPackUtils::processZIP(pack); + + QVERIFY(pack.packFormat() == ShaderPackFormat::VALID); + QVERIFY(valid == true); + } + + void test_parseFolder() + { + QString source = QFINDTESTDATA("testdata/ShaderPackParse"); + + QString folder_sp = FS::PathCombine(source, "shaderpack2"); + ShaderPack pack { QFileInfo(folder_sp) }; + + bool valid = ShaderPackUtils::processFolder(pack); + + QVERIFY(pack.packFormat() == ShaderPackFormat::VALID); + QVERIFY(valid == true); + } + + void test_parseZIP2() + { + QString source = QFINDTESTDATA("testdata/ShaderPackParse"); + + QString folder_sp = FS::PathCombine(source, "shaderpack3.zip"); + ShaderPack pack { QFileInfo(folder_sp) }; + + bool valid = ShaderPackUtils::process(pack); + + QVERIFY(pack.packFormat() == ShaderPackFormat::INVALID); + QVERIFY(valid == false); + } +}; + +QTEST_GUILESS_MAIN(ShaderPackParseTest) + +#include "ShaderPackParse_test.moc" diff --git a/tests/WorldSaveParse_test.cpp b/tests/WorldSaveParse_test.cpp new file mode 100644 index 00000000..4a8c3d29 --- /dev/null +++ b/tests/WorldSaveParse_test.cpp @@ -0,0 +1,94 @@ + +// SPDX-FileCopyrightText: 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> +// +// SPDX-License-Identifier: GPL-3.0-only + +/* + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include +#include + +#include + +#include +#include + +class WorldSaveParseTest : public QObject { + Q_OBJECT + + private slots: + void test_parseZIP() + { + QString source = QFINDTESTDATA("testdata/WorldSaveParse"); + + QString zip_ws = FS::PathCombine(source, "minecraft_save_1.zip") ; + WorldSave save { QFileInfo(zip_ws) }; + + bool valid = WorldSaveUtils::processZIP(save); + + QVERIFY(save.saveFormat() == WorldSaveFormat::SINGLE); + QVERIFY(save.saveDirName() == "world_1"); + QVERIFY(valid == true); + } + + void test_parse_ZIP2() + { + QString source = QFINDTESTDATA("testdata/WorldSaveParse"); + + QString zip_ws = FS::PathCombine(source, "minecraft_save_2.zip") ; + WorldSave save { QFileInfo(zip_ws) }; + + bool valid = WorldSaveUtils::processZIP(save); + + QVERIFY(save.saveFormat() == WorldSaveFormat::MULTI); + QVERIFY(save.saveDirName() == "world_2"); + QVERIFY(valid == true); + } + + void test_parseFolder() + { + QString source = QFINDTESTDATA("testdata/WorldSaveParse"); + + QString folder_ws = FS::PathCombine(source, "minecraft_save_3"); + WorldSave save { QFileInfo(folder_ws) }; + + bool valid = WorldSaveUtils::processFolder(save); + + QVERIFY(save.saveFormat() == WorldSaveFormat::SINGLE); + QVERIFY(save.saveDirName() == "world_3"); + QVERIFY(valid == true); + } + + void test_parseFolder2() + { + QString source = QFINDTESTDATA("testdata/WorldSaveParse"); + + QString folder_ws = FS::PathCombine(source, "minecraft_save_4"); + WorldSave save { QFileInfo(folder_ws) }; + + bool valid = WorldSaveUtils::process(save); + + QVERIFY(save.saveFormat() == WorldSaveFormat::MULTI); + QVERIFY(save.saveDirName() == "world_4"); + QVERIFY(valid == true); + } +}; + +QTEST_GUILESS_MAIN(WorldSaveParseTest) + +#include "WorldSaveParse_test.moc" diff --git a/tests/testdata/ShaderPackParse/shaderpack1.zip b/tests/testdata/ShaderPackParse/shaderpack1.zip new file mode 100644 index 0000000000000000000000000000000000000000..9a8fb186cfef525b9db060f78f1b3b6ca93a792c GIT binary patch literal 242 zcmWIWW@Zs#0D;o(8KGbXl;8l;#TkhysYS*50dQ6BXsV=;R6$ki6%^$cq!yKArWOZy uGcwsT<2D~=-&;oz3t<~V7dHD~x|TGmA?dw-8806jev8UO$Q literal 0 HcmV?d00001 diff --git a/tests/testdata/WorldSaveParse/minecraft_save_1.zip b/tests/testdata/WorldSaveParse/minecraft_save_1.zip new file mode 100644 index 0000000000000000000000000000000000000000..832a243d660deabd3309f410ae9ccbf654735996 GIT binary patch literal 184 zcmWIWW@h1H0D+YaGeW@(C?Uuo!%&`Il#>!~sGpNsmYSoNl2{TN!pXqAu621b2$xoH xGcdBeU}j(d69L|gOmfV)43mJHy`&Mu#9}ln#Apm-S=m4u7=bVxNPB}g3;C$rL;y~6#4*gtNi9pw(Mw4zfg1=i6vIG9COKwYPLqH-Qh?#DBZ!IaP*#XT lNib8K0cIux!;(f13^S1&jmvOWHjq=8fN&#_o(bYG003(eNtFNq literal 0 HcmV?d00001 diff --git a/tests/testdata/WorldSaveParse/minecraft_save_3/world_3/level.dat b/tests/testdata/WorldSaveParse/minecraft_save_3/world_3/level.dat new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/tests/testdata/WorldSaveParse/minecraft_save_4/saves/world_4/level.dat b/tests/testdata/WorldSaveParse/minecraft_save_4/saves/world_4/level.dat new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 From cfce54fe46f7d3db39e50c4113cb9fc74d6719e2 Mon Sep 17 00:00:00 2001 From: Rachel Powers <508861+Ryex@users.noreply.github.com> Date: Sat, 24 Dec 2022 18:08:08 -0700 Subject: [PATCH 022/152] fix: update parse tests Signed-off-by: Rachel Powers <508861+Ryex@users.noreply.github.com> --- .../mod/tasks/LocalTexturePackParseTask.cpp | 2 +- .../mod/tasks/LocalWorldSaveParseTask.h | 2 +- tests/ResourcePackParse_test.cpp | 9 ++++++--- tests/TexturePackParse_test.cpp | 9 ++++++--- .../test_resource_pack_idk.zip | Bin 322 -> 804 bytes 5 files changed, 14 insertions(+), 8 deletions(-) diff --git a/launcher/minecraft/mod/tasks/LocalTexturePackParseTask.cpp b/launcher/minecraft/mod/tasks/LocalTexturePackParseTask.cpp index e4492f12..38f1d7c1 100644 --- a/launcher/minecraft/mod/tasks/LocalTexturePackParseTask.cpp +++ b/launcher/minecraft/mod/tasks/LocalTexturePackParseTask.cpp @@ -116,7 +116,7 @@ bool processZIP(TexturePack& pack, ProcessingLevel level) if (level == ProcessingLevel::BasicInfoOnly) { zip.close(); - return false; + return true; } if (zip.setCurrentFile("pack.png")) { diff --git a/launcher/minecraft/mod/tasks/LocalWorldSaveParseTask.h b/launcher/minecraft/mod/tasks/LocalWorldSaveParseTask.h index 44153735..aa5db0c2 100644 --- a/launcher/minecraft/mod/tasks/LocalWorldSaveParseTask.h +++ b/launcher/minecraft/mod/tasks/LocalWorldSaveParseTask.h @@ -37,7 +37,7 @@ bool process(WorldSave& save, ProcessingLevel level = ProcessingLevel::Full); bool processZIP(WorldSave& pack, ProcessingLevel level = ProcessingLevel::Full); bool processFolder(WorldSave& pack, ProcessingLevel level = ProcessingLevel::Full); -bool validate(QFileInfo file); +bool validate(QFileInfo file); } // namespace WorldSaveUtils diff --git a/tests/ResourcePackParse_test.cpp b/tests/ResourcePackParse_test.cpp index 568c3b63..4192da31 100644 --- a/tests/ResourcePackParse_test.cpp +++ b/tests/ResourcePackParse_test.cpp @@ -35,10 +35,11 @@ class ResourcePackParseTest : public QObject { QString zip_rp = FS::PathCombine(source, "test_resource_pack_idk.zip"); ResourcePack pack { QFileInfo(zip_rp) }; - ResourcePackUtils::processZIP(pack); + bool valid = ResourcePackUtils::processZIP(pack, ResourcePackUtils::ProcessingLevel::BasicInfoOnly); QVERIFY(pack.packFormat() == 3); QVERIFY(pack.description() == "um dois, feijão com arroz, três quatro, feijão no prato, cinco seis, café inglês, sete oito, comer biscoito, nove dez comer pastéis!!"); + QVERIFY(valid == true); } void test_parseFolder() @@ -48,10 +49,11 @@ class ResourcePackParseTest : public QObject { QString folder_rp = FS::PathCombine(source, "test_folder"); ResourcePack pack { QFileInfo(folder_rp) }; - ResourcePackUtils::processFolder(pack); + bool valid = ResourcePackUtils::processFolder(pack, ResourcePackUtils::ProcessingLevel::BasicInfoOnly); QVERIFY(pack.packFormat() == 1); QVERIFY(pack.description() == "Some resource pack maybe"); + QVERIFY(valid == true); } void test_parseFolder2() @@ -61,10 +63,11 @@ class ResourcePackParseTest : public QObject { QString folder_rp = FS::PathCombine(source, "another_test_folder"); ResourcePack pack { QFileInfo(folder_rp) }; - ResourcePackUtils::process(pack); + bool valid = ResourcePackUtils::process(pack, ResourcePackUtils::ProcessingLevel::BasicInfoOnly); QVERIFY(pack.packFormat() == 6); QVERIFY(pack.description() == "o quartel pegou fogo, policia deu sinal, acode acode acode a bandeira nacional"); + QVERIFY(valid == false); } }; diff --git a/tests/TexturePackParse_test.cpp b/tests/TexturePackParse_test.cpp index 0771f79f..4ddc0a3a 100644 --- a/tests/TexturePackParse_test.cpp +++ b/tests/TexturePackParse_test.cpp @@ -36,9 +36,10 @@ class TexturePackParseTest : public QObject { QString zip_rp = FS::PathCombine(source, "test_texture_pack_idk.zip"); TexturePack pack { QFileInfo(zip_rp) }; - TexturePackUtils::processZIP(pack); + bool valid = TexturePackUtils::processZIP(pack); QVERIFY(pack.description() == "joe biden, wake up"); + QVERIFY(valid == true); } void test_parseFolder() @@ -48,9 +49,10 @@ class TexturePackParseTest : public QObject { QString folder_rp = FS::PathCombine(source, "test_texturefolder"); TexturePack pack { QFileInfo(folder_rp) }; - TexturePackUtils::processFolder(pack); + bool valid = TexturePackUtils::processFolder(pack, TexturePackUtils::ProcessingLevel::BasicInfoOnly); QVERIFY(pack.description() == "Some texture pack surely"); + QVERIFY(valid == true); } void test_parseFolder2() @@ -60,9 +62,10 @@ class TexturePackParseTest : public QObject { QString folder_rp = FS::PathCombine(source, "another_test_texturefolder"); TexturePack pack { QFileInfo(folder_rp) }; - TexturePackUtils::process(pack); + bool valid = TexturePackUtils::process(pack, TexturePackUtils::ProcessingLevel::BasicInfoOnly); QVERIFY(pack.description() == "quieres\nfor real"); + QVERIFY(valid == true); } }; diff --git a/tests/testdata/ResourcePackParse/test_resource_pack_idk.zip b/tests/testdata/ResourcePackParse/test_resource_pack_idk.zip index 52b91cdcfc6e2dbb0ecb76c5cb33d07b0184487d..b4e66a609436f535c4c95f6d2512114f9caa28eb 100644 GIT binary patch literal 804 zcmWIWW@Zs#U|`^2FfNk|z0d8=Jq^e^4aD3GG7JTY$=Q0j$+@W|iJ>8!49pvS`I14n zw1S&~k>w>b0|S_F?K9*%WWeK^uJx5K?^#Yx3|p&-E1M9zb!x`Z%o(Rliwu3bTfVga zpZ|Zl&GxhBQl77W#<2O9t_=I57hRk7#wN-iF@DxGMP!ztlfQ;oTf_8UN*_cH8eUFW zSWwG-$31+;zgCwCH$_$DCaX{QFt^(L3ai1~{9PNlycrH{bu~1Y6Yyu6ur1S<#9y+T zCI^X6S!ev~{^r~e`2e`rO!{Yp0)5251R{Wd9f%W)i&IOA^_dxPD-%R0%gxM7O)g4I zE5WNl3Y&(K)QXbQqEu9?t~rK_p@sDao53St%004mv B$V~tM delta 31 gcmZ3&c8F=h-N~C714UTb7=VBg2m^uiZ4ie60D+SQ!2kdN From 8422e3ac01c861125fd6aea441714a2fb38e5ff9 Mon Sep 17 00:00:00 2001 From: Rachel Powers <508861+Ryex@users.noreply.github.com> Date: Sat, 24 Dec 2022 20:38:29 -0700 Subject: [PATCH 023/152] feat: zip resource validation check for flame Signed-off-by: Rachel Powers <508861+Ryex@users.noreply.github.com> --- launcher/minecraft/mod/DataPack.cpp | 2 +- launcher/minecraft/mod/ResourcePack.cpp | 2 +- .../flame/FlameInstanceCreationTask.cpp | 147 ++++++++++++++---- .../flame/FlameInstanceCreationTask.h | 3 + 4 files changed, 120 insertions(+), 34 deletions(-) diff --git a/launcher/minecraft/mod/DataPack.cpp b/launcher/minecraft/mod/DataPack.cpp index 6c333285..ea1d097b 100644 --- a/launcher/minecraft/mod/DataPack.cpp +++ b/launcher/minecraft/mod/DataPack.cpp @@ -41,7 +41,7 @@ void DataPack::setPackFormat(int new_format_id) QMutexLocker locker(&m_data_lock); if (!s_pack_format_versions.contains(new_format_id)) { - qWarning() << "Pack format '%1' is not a recognized resource pack id!"; + qWarning() << "Pack format '" << new_format_id << "' is not a recognized data pack id!"; } m_pack_format = new_format_id; diff --git a/launcher/minecraft/mod/ResourcePack.cpp b/launcher/minecraft/mod/ResourcePack.cpp index 47da4fea..87995215 100644 --- a/launcher/minecraft/mod/ResourcePack.cpp +++ b/launcher/minecraft/mod/ResourcePack.cpp @@ -27,7 +27,7 @@ void ResourcePack::setPackFormat(int new_format_id) QMutexLocker locker(&m_data_lock); if (!s_pack_format_versions.contains(new_format_id)) { - qWarning() << "Pack format '%1' is not a recognized resource pack id!"; + qWarning() << "Pack format '" << new_format_id << "' is not a recognized resource pack id!"; } m_pack_format = new_format_id; diff --git a/launcher/modplatform/flame/FlameInstanceCreationTask.cpp b/launcher/modplatform/flame/FlameInstanceCreationTask.cpp index 1d441f09..2b1bc8d0 100644 --- a/launcher/modplatform/flame/FlameInstanceCreationTask.cpp +++ b/launcher/modplatform/flame/FlameInstanceCreationTask.cpp @@ -53,6 +53,13 @@ #include "ui/dialogs/BlockedModsDialog.h" #include "ui/dialogs/CustomMessageBox.h" +#include +#include +#include +#include +#include +#include + const static QMap forgemap = { { "1.2.5", "3.4.9.171" }, { "1.4.2", "6.0.1.355" }, { "1.4.7", "6.6.2.534" }, @@ -401,6 +408,11 @@ void FlameCreationTask::idResolverSucceeded(QEventLoop& loop) QList blocked_mods; auto anyBlocked = false; for (const auto& result : results.files.values()) { + + if(result.fileName.endsWith(".zip")) { + m_ZIP_resources.append(std::make_pair(result.fileName, result.targetFolder)); + } + if (!result.resolved || result.url.isEmpty()) { BlockedMod blocked_mod; blocked_mod.name = result.fileName; @@ -439,37 +451,6 @@ void FlameCreationTask::idResolverSucceeded(QEventLoop& loop) } } -/// @brief copy the matched blocked mods to the instance staging area -/// @param blocked_mods list of the blocked mods and their matched paths -void FlameCreationTask::copyBlockedMods(QList const& blocked_mods) -{ - setStatus(tr("Copying Blocked Mods...")); - setAbortable(false); - int i = 0; - int total = blocked_mods.length(); - setProgress(i, total); - for (auto const& mod : blocked_mods) { - if (!mod.matched) { - qDebug() << mod.name << "was not matched to a local file, skipping copy"; - continue; - } - - auto dest_path = FS::PathCombine(m_stagingPath, "minecraft", mod.targetFolder, mod.name); - - setStatus(tr("Copying Blocked Mods (%1 out of %2 are done)").arg(QString::number(i), QString::number(total))); - - qDebug() << "Will try to copy" << mod.localPath << "to" << dest_path; - - if (!FS::copy(mod.localPath, dest_path)()) { - qDebug() << "Copy of" << mod.localPath << "to" << dest_path << "Failed"; - } - - i++; - setProgress(i, total); - } - - setAbortable(true); -} void FlameCreationTask::setupDownloadJob(QEventLoop& loop) { @@ -509,7 +490,10 @@ void FlameCreationTask::setupDownloadJob(QEventLoop& loop) } m_mod_id_resolver.reset(); - connect(m_files_job.get(), &NetJob::succeeded, this, [&]() { m_files_job.reset(); }); + connect(m_files_job.get(), &NetJob::succeeded, this, [&]() { + m_files_job.reset(); + validateZIPResouces(); + }); connect(m_files_job.get(), &NetJob::failed, [&](QString reason) { m_files_job.reset(); setError(reason); @@ -520,3 +504,102 @@ void FlameCreationTask::setupDownloadJob(QEventLoop& loop) setStatus(tr("Downloading mods...")); m_files_job->start(); } + +/// @brief copy the matched blocked mods to the instance staging area +/// @param blocked_mods list of the blocked mods and their matched paths +void FlameCreationTask::copyBlockedMods(QList const& blocked_mods) +{ + setStatus(tr("Copying Blocked Mods...")); + setAbortable(false); + int i = 0; + int total = blocked_mods.length(); + setProgress(i, total); + for (auto const& mod : blocked_mods) { + if (!mod.matched) { + qDebug() << mod.name << "was not matched to a local file, skipping copy"; + continue; + } + + auto destPath = FS::PathCombine(m_stagingPath, "minecraft", mod.targetFolder, mod.name); + + setStatus(tr("Copying Blocked Mods (%1 out of %2 are done)").arg(QString::number(i), QString::number(total))); + + qDebug() << "Will try to copy" << mod.localPath << "to" << destPath; + + if (!FS::copy(mod.localPath, destPath)()) { + qDebug() << "Copy of" << mod.localPath << "to" << destPath << "Failed"; + } + + i++; + setProgress(i, total); + } + + setAbortable(true); +} + +static bool moveFile(QString src, QString dst) +{ + if (!FS::copy(src, dst)()) { // copy + qDebug() << "Copy of" << src << "to" << dst << "Failed!"; + return false; + } else { + if (!FS::deletePath(src)) { // remove origonal + qDebug() << "Deleation of" << src << "Failed!"; + return false; + }; + } + return true; +} + +void FlameCreationTask::validateZIPResouces() +{ + qDebug() << "Validating resoucres stored as .zip are in the right place"; + for (auto [fileName, targetFolder] : m_ZIP_resources) { + qDebug() << "Checking" << fileName << "..."; + auto localPath = FS::PathCombine(m_stagingPath, "minecraft", targetFolder, fileName); + QFileInfo localFileInfo(localPath); + if (localFileInfo.exists() && localFileInfo.isFile()) { + if (ResourcePackUtils::validate(localFileInfo)) { + if (targetFolder != "resourcepacks") { + qDebug() << "Target folder of" << fileName << "is incorrect, it's a resource pack."; + auto destPath = FS::PathCombine(m_stagingPath, "minecraft", "resourcepacks", fileName); + qDebug() << "Moveing" << localPath << "to" << destPath; + moveFile(localPath, destPath); + } else { + qDebug() << fileName << "is in the right place :" << targetFolder; + } + } else if (TexturePackUtils::validate(localFileInfo)) { + if (targetFolder != "texturepacks") { + qDebug() << "Target folder of" << fileName << "is incorrect, it's a pre 1.6 texture pack."; + auto destPath = FS::PathCombine(m_stagingPath, "minecraft", "texturepacks", fileName); + qDebug() << "Moveing" << localPath << "to" << destPath; + moveFile(localPath, destPath); + } else { + qDebug() << fileName << "is in the right place :" << targetFolder; + } + } else if (DataPackUtils::validate(localFileInfo)) { + if (targetFolder != "datapacks") { + qDebug() << "Target folder of" << fileName << "is incorrect, it's a data pack."; + auto destPath = FS::PathCombine(m_stagingPath, "minecraft", "datapacks", fileName); + qDebug() << "Moveing" << localPath << "to" << destPath; + moveFile(localPath, destPath); + } else { + qDebug() << fileName << "is in the right place :" << targetFolder; + } + } else if (ModUtils::validate(localFileInfo)) { + if (targetFolder != "mods") { + qDebug() << "Target folder of" << fileName << "is incorrect, it's a mod."; + auto destPath = FS::PathCombine(m_stagingPath, "minecraft", "mods", fileName); + qDebug() << "Moveing" << localPath << "to" << destPath; + moveFile(localPath, destPath); + } else { + qDebug() << fileName << "is in the right place :" << targetFolder; + } + } else { + qDebug() << "Can't Identify" << fileName << "at" << localPath << ", leaving it where it is."; + } + } else { + qDebug() << "Can't find" << localPath << "to validate it, ignoreing"; + } + } +} diff --git a/launcher/modplatform/flame/FlameInstanceCreationTask.h b/launcher/modplatform/flame/FlameInstanceCreationTask.h index 3a1c729f..498e1d6e 100644 --- a/launcher/modplatform/flame/FlameInstanceCreationTask.h +++ b/launcher/modplatform/flame/FlameInstanceCreationTask.h @@ -77,6 +77,7 @@ class FlameCreationTask final : public InstanceCreationTask { void idResolverSucceeded(QEventLoop&); void setupDownloadJob(QEventLoop&); void copyBlockedMods(QList const& blocked_mods); + void validateZIPResouces(); private: QWidget* m_parent = nullptr; @@ -90,5 +91,7 @@ class FlameCreationTask final : public InstanceCreationTask { QString m_managed_id, m_managed_version_id; + QList> m_ZIP_resources; + std::optional m_instance; }; From 78984eea3aa398451dc511712ccb7ec55f93194c Mon Sep 17 00:00:00 2001 From: Rachel Powers <508861+Ryex@users.noreply.github.com> Date: Sun, 25 Dec 2022 16:49:56 -0700 Subject: [PATCH 024/152] feat: support installing worlds during flame pack import. Signed-off-by: Rachel Powers <508861+Ryex@users.noreply.github.com> --- .../flame/FlameInstanceCreationTask.cpp | 72 ++++++++++--------- 1 file changed, 39 insertions(+), 33 deletions(-) diff --git a/launcher/modplatform/flame/FlameInstanceCreationTask.cpp b/launcher/modplatform/flame/FlameInstanceCreationTask.cpp index 2b1bc8d0..204d5c1f 100644 --- a/launcher/modplatform/flame/FlameInstanceCreationTask.cpp +++ b/launcher/modplatform/flame/FlameInstanceCreationTask.cpp @@ -57,6 +57,9 @@ #include #include #include +#include +#include +#include #include #include @@ -537,13 +540,13 @@ void FlameCreationTask::copyBlockedMods(QList const& blocked_mods) setAbortable(true); } -static bool moveFile(QString src, QString dst) +bool moveFile(QString src, QString dst) { if (!FS::copy(src, dst)()) { // copy qDebug() << "Copy of" << src << "to" << dst << "Failed!"; return false; } else { - if (!FS::deletePath(src)) { // remove origonal + if (!FS::deletePath(src)) { // remove original qDebug() << "Deleation of" << src << "Failed!"; return false; }; @@ -551,50 +554,53 @@ static bool moveFile(QString src, QString dst) return true; } + void FlameCreationTask::validateZIPResouces() { qDebug() << "Validating resoucres stored as .zip are in the right place"; for (auto [fileName, targetFolder] : m_ZIP_resources) { + qDebug() << "Checking" << fileName << "..."; auto localPath = FS::PathCombine(m_stagingPath, "minecraft", targetFolder, fileName); + + auto validatePath = [&localPath, this](QString fileName, QString targetFolder, QString realTarget) { + if (targetFolder != "resourcepacks") { + qDebug() << "Target folder of" << fileName << "is incorrect, it's a resource pack."; + auto destPath = FS::PathCombine(m_stagingPath, "minecraft", "resourcepacks", fileName); + qDebug() << "Moving" << localPath << "to" << destPath; + if (moveFile(localPath, destPath)) { + return destPath; + } + } else { + qDebug() << fileName << "is in the right place :" << targetFolder; + } + return localPath; + }; + QFileInfo localFileInfo(localPath); if (localFileInfo.exists() && localFileInfo.isFile()) { if (ResourcePackUtils::validate(localFileInfo)) { - if (targetFolder != "resourcepacks") { - qDebug() << "Target folder of" << fileName << "is incorrect, it's a resource pack."; - auto destPath = FS::PathCombine(m_stagingPath, "minecraft", "resourcepacks", fileName); - qDebug() << "Moveing" << localPath << "to" << destPath; - moveFile(localPath, destPath); - } else { - qDebug() << fileName << "is in the right place :" << targetFolder; - } + validatePath(fileName, targetFolder, "resourcepacks"); } else if (TexturePackUtils::validate(localFileInfo)) { - if (targetFolder != "texturepacks") { - qDebug() << "Target folder of" << fileName << "is incorrect, it's a pre 1.6 texture pack."; - auto destPath = FS::PathCombine(m_stagingPath, "minecraft", "texturepacks", fileName); - qDebug() << "Moveing" << localPath << "to" << destPath; - moveFile(localPath, destPath); - } else { - qDebug() << fileName << "is in the right place :" << targetFolder; - } + validatePath(fileName, targetFolder, "texturepacks"); } else if (DataPackUtils::validate(localFileInfo)) { - if (targetFolder != "datapacks") { - qDebug() << "Target folder of" << fileName << "is incorrect, it's a data pack."; - auto destPath = FS::PathCombine(m_stagingPath, "minecraft", "datapacks", fileName); - qDebug() << "Moveing" << localPath << "to" << destPath; - moveFile(localPath, destPath); - } else { - qDebug() << fileName << "is in the right place :" << targetFolder; - } + validatePath(fileName, targetFolder, "datapacks"); } else if (ModUtils::validate(localFileInfo)) { - if (targetFolder != "mods") { - qDebug() << "Target folder of" << fileName << "is incorrect, it's a mod."; - auto destPath = FS::PathCombine(m_stagingPath, "minecraft", "mods", fileName); - qDebug() << "Moveing" << localPath << "to" << destPath; - moveFile(localPath, destPath); + validatePath(fileName, targetFolder, "mods"); + } else if (WorldSaveUtils::validate(localFileInfo)) { + QString worldPath = validatePath(fileName, targetFolder, "saves"); + + qDebug() << "Installing World from" << worldPath; + World w(worldPath); + if (!w.isValid()) { + qDebug() << "World at" << worldPath << "is not valid, skipping install."; } else { - qDebug() << fileName << "is in the right place :" << targetFolder; - } + w.install(FS::PathCombine(m_stagingPath, "minecraft", "saves")); + } + } else if (ShaderPackUtils::validate(localFileInfo)) { + // in theroy flame API can't do this but who knows, that *may* change ? + // better to handle it if it *does* occure in the future + validatePath(fileName, targetFolder, "shaderpacks"); } else { qDebug() << "Can't Identify" << fileName << "at" << localPath << ", leaving it where it is."; } From b2082bfde7149a5596fe8a467659699ad569f932 Mon Sep 17 00:00:00 2001 From: Rachel Powers <508861+Ryex@users.noreply.github.com> Date: Sun, 25 Dec 2022 17:16:26 -0700 Subject: [PATCH 025/152] fix: explicit QFileInfo converison for qt6 fix: validatePath in validateZIPResouces Signed-off-by: Rachel Powers <508861+Ryex@users.noreply.github.com> --- .../flame/FlameInstanceCreationTask.cpp | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/launcher/modplatform/flame/FlameInstanceCreationTask.cpp b/launcher/modplatform/flame/FlameInstanceCreationTask.cpp index 204d5c1f..b62d05ab 100644 --- a/launcher/modplatform/flame/FlameInstanceCreationTask.cpp +++ b/launcher/modplatform/flame/FlameInstanceCreationTask.cpp @@ -564,15 +564,13 @@ void FlameCreationTask::validateZIPResouces() auto localPath = FS::PathCombine(m_stagingPath, "minecraft", targetFolder, fileName); auto validatePath = [&localPath, this](QString fileName, QString targetFolder, QString realTarget) { - if (targetFolder != "resourcepacks") { - qDebug() << "Target folder of" << fileName << "is incorrect, it's a resource pack."; - auto destPath = FS::PathCombine(m_stagingPath, "minecraft", "resourcepacks", fileName); + if (targetFolder != realTarget) { + qDebug() << "Target folder of" << fileName << "is incorrect, it belongs in" << realTarget; + auto destPath = FS::PathCombine(m_stagingPath, "minecraft", realTarget, fileName); qDebug() << "Moving" << localPath << "to" << destPath; if (moveFile(localPath, destPath)) { return destPath; } - } else { - qDebug() << fileName << "is in the right place :" << targetFolder; } return localPath; }; @@ -580,18 +578,24 @@ void FlameCreationTask::validateZIPResouces() QFileInfo localFileInfo(localPath); if (localFileInfo.exists() && localFileInfo.isFile()) { if (ResourcePackUtils::validate(localFileInfo)) { + qDebug() << fileName << "is a resource pack"; validatePath(fileName, targetFolder, "resourcepacks"); } else if (TexturePackUtils::validate(localFileInfo)) { + qDebug() << fileName << "is a pre 1.6 texture pack"; validatePath(fileName, targetFolder, "texturepacks"); } else if (DataPackUtils::validate(localFileInfo)) { + qDebug() << fileName << "is a data pack"; validatePath(fileName, targetFolder, "datapacks"); } else if (ModUtils::validate(localFileInfo)) { + qDebug() << fileName << "is a mod"; validatePath(fileName, targetFolder, "mods"); } else if (WorldSaveUtils::validate(localFileInfo)) { + qDebug() << fileName << "is a world save"; QString worldPath = validatePath(fileName, targetFolder, "saves"); qDebug() << "Installing World from" << worldPath; - World w(worldPath); + QFileInfo worldFileInfo(worldPath); + World w(worldFileInfo); if (!w.isValid()) { qDebug() << "World at" << worldPath << "is not valid, skipping install."; } else { @@ -600,6 +604,7 @@ void FlameCreationTask::validateZIPResouces() } else if (ShaderPackUtils::validate(localFileInfo)) { // in theroy flame API can't do this but who knows, that *may* change ? // better to handle it if it *does* occure in the future + qDebug() << fileName << "is a shader pack"; validatePath(fileName, targetFolder, "shaderpacks"); } else { qDebug() << "Can't Identify" << fileName << "at" << localPath << ", leaving it where it is."; From bf04becc9e05f147ca595868c9a51da14d1c0c34 Mon Sep 17 00:00:00 2001 From: TheKodeToad Date: Mon, 26 Dec 2022 14:33:50 +0000 Subject: [PATCH 026/152] About to -> you are about to You're is used in some other places but im lazy Signed-off-by: TheKodeToad --- launcher/ui/GuiUtil.cpp | 3 +-- launcher/ui/MainWindow.cpp | 2 +- launcher/ui/pages/instance/ExternalResourcesPage.cpp | 4 ++-- launcher/ui/pages/instance/OtherLogsPage.cpp | 2 +- launcher/ui/pages/instance/ScreenshotsPage.cpp | 8 ++++---- launcher/ui/pages/instance/ServersPage.cpp | 2 +- launcher/ui/pages/instance/VersionPage.cpp | 4 ++-- launcher/ui/pages/instance/WorldListPage.cpp | 2 +- 8 files changed, 13 insertions(+), 14 deletions(-) diff --git a/launcher/ui/GuiUtil.cpp b/launcher/ui/GuiUtil.cpp index 6a22ec2f..855ab400 100644 --- a/launcher/ui/GuiUtil.cpp +++ b/launcher/ui/GuiUtil.cpp @@ -65,8 +65,7 @@ QString GuiUtil::uploadPaste(const QString &name, const QString &text, QWidget * if (baseUrl.isValid()) { auto response = CustomMessageBox::selectable(parentWidget, QObject::tr("Confirm Upload"), - QObject::tr("About to upload: %1\n" - "Uploading to: %2\n" + QObject::tr("You are about to upload \"%1\" to %2.\n" "You should double-check for personal information.\n\n" "Are you sure?") .arg(name, baseUrl.host()), diff --git a/launcher/ui/MainWindow.cpp b/launcher/ui/MainWindow.cpp index 7442b955..c8a1fddc 100644 --- a/launcher/ui/MainWindow.cpp +++ b/launcher/ui/MainWindow.cpp @@ -2095,7 +2095,7 @@ void MainWindow::on_actionDeleteInstance_triggered() auto id = m_selectedInstance->id(); auto response = CustomMessageBox::selectable(this, tr("Confirm Deletion"), - tr("About to delete: %1\n" + tr("You are about to delete \"%1\".\n" "This may be permanent and will completely delete the instance.\n\n" "Are you sure?") .arg(m_selectedInstance->name()), diff --git a/launcher/ui/pages/instance/ExternalResourcesPage.cpp b/launcher/ui/pages/instance/ExternalResourcesPage.cpp index 6f1abbff..1115ddc3 100644 --- a/launcher/ui/pages/instance/ExternalResourcesPage.cpp +++ b/launcher/ui/pages/instance/ExternalResourcesPage.cpp @@ -176,12 +176,12 @@ void ExternalResourcesPage::removeItem() bool multiple = count > 1; if (multiple) { - text = tr("About to remove: %1 items\n" + text = tr("You are about to remove %1 items.\n" "This may be permanent and they will be gone from the folder.\n\n" "Are you sure?") .arg(count); } else if (folder) { - text = tr("About to remove: %1 (folder)\n" + text = tr("You are about to remove the folder \"%1\".\n" "This may be permanent and it will be gone from the parent folder.\n\n" "Are you sure?") .arg(m_model->at(selection.indexes().at(0).row()).fileinfo().fileName()); diff --git a/launcher/ui/pages/instance/OtherLogsPage.cpp b/launcher/ui/pages/instance/OtherLogsPage.cpp index 1be2a3f8..bbdd7324 100644 --- a/launcher/ui/pages/instance/OtherLogsPage.cpp +++ b/launcher/ui/pages/instance/OtherLogsPage.cpp @@ -221,7 +221,7 @@ void OtherLogsPage::on_btnDelete_clicked() return; } if (QMessageBox::question(this, tr("Confirm Deletion"), - tr("About to delete: %1\n" + tr("You are about to delete \"%1\".\n" "This may be permanent and it will be gone from the logs folder.\n\n" "Are you sure?") .arg(m_currentFile), diff --git a/launcher/ui/pages/instance/ScreenshotsPage.cpp b/launcher/ui/pages/instance/ScreenshotsPage.cpp index 4b756766..ca368d3b 100644 --- a/launcher/ui/pages/instance/ScreenshotsPage.cpp +++ b/launcher/ui/pages/instance/ScreenshotsPage.cpp @@ -383,12 +383,12 @@ void ScreenshotsPage::on_actionUpload_triggered() QString text; if (selection.size() > 1) - text = tr("About to upload: %1 screenshots\n\n" + text = tr("You are about to upload %1 screenshots.\n\n" "Are you sure?") .arg(selection.size()); else text = - tr("About to upload the selected screenshot.\n\n" + tr("You are about to upload the selected screenshot.\n\n" "Are you sure?"); auto response = CustomMessageBox::selectable(this, "Confirm Upload", text, QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No, @@ -515,12 +515,12 @@ void ScreenshotsPage::on_actionDelete_triggered() int count = ui->listView->selectionModel()->selectedRows().size(); QString text; if (count > 1) - text = tr("About to delete: %1 screenshots\n" + text = tr("You are about to delete %1 screenshots.\n" "This may be permanent and they will be gone from the folder.\n\n" "Are you sure?") .arg(count); else - text = tr("About to delete the selected screenshot.\n" + text = tr("You are about to delete the selected screenshot.\n" "This may be permanent and it will be gone from the folder.\n\n" "Are you sure?") .arg(count); diff --git a/launcher/ui/pages/instance/ServersPage.cpp b/launcher/ui/pages/instance/ServersPage.cpp index 6925ffb4..6f8591a1 100644 --- a/launcher/ui/pages/instance/ServersPage.cpp +++ b/launcher/ui/pages/instance/ServersPage.cpp @@ -802,7 +802,7 @@ void ServersPage::on_actionAdd_triggered() void ServersPage::on_actionRemove_triggered() { auto response = CustomMessageBox::selectable(this, tr("Confirm Removal"), - tr("About to remove: %1\n" + tr("You are about to remove \"%1\".\n" "This is permanent and the server will be gone from your list forever (A LONG TIME).\n\n" "Are you sure?") .arg(m_model->at(currentServer)->m_name), diff --git a/launcher/ui/pages/instance/VersionPage.cpp b/launcher/ui/pages/instance/VersionPage.cpp index 08ab8641..d200652a 100644 --- a/launcher/ui/pages/instance/VersionPage.cpp +++ b/launcher/ui/pages/instance/VersionPage.cpp @@ -327,7 +327,7 @@ void VersionPage::on_actionRemove_triggered() if (component->isCustom()) { auto response = CustomMessageBox::selectable(this, tr("Confirm Removal"), - tr("About to remove: %1\n" + tr("You are about to remove \"%1\".\n" "This is permanent and will completely remove the custom component.\n\n" "Are you sure?") .arg(component->getName()), @@ -726,7 +726,7 @@ void VersionPage::on_actionRevert_triggered() auto component = m_profile->getComponent(version); auto response = CustomMessageBox::selectable(this, tr("Confirm Reversion"), - tr("About to revert: %1\n" + tr("You are about to revert \"%1\".\n" "This is permanent and will completely revert your customizations.\n\n" "Are you sure?") .arg(component->getName()), diff --git a/launcher/ui/pages/instance/WorldListPage.cpp b/launcher/ui/pages/instance/WorldListPage.cpp index c98f1e5a..0020c461 100644 --- a/launcher/ui/pages/instance/WorldListPage.cpp +++ b/launcher/ui/pages/instance/WorldListPage.cpp @@ -195,7 +195,7 @@ void WorldListPage::on_actionRemove_triggered() return; auto result = CustomMessageBox::selectable(this, tr("Confirm Deletion"), - tr("About to delete: %1\n" + tr("You are about to delete \"%1\".\n" "The world may be gone forever (A LONG TIME).\n\n" "Are you sure?") .arg(m_worlds->allWorlds().at(proxiedIndex.row()).name()), From 434f639b0c9af355703d6c64cfe5bbe9a28d0b9b Mon Sep 17 00:00:00 2001 From: TheKodeToad Date: Mon, 26 Dec 2022 14:58:02 +0000 Subject: [PATCH 027/152] Use optional instead of hardcoded cancelled string Signed-off-by: TheKodeToad --- launcher/ui/GuiUtil.cpp | 7 ++++--- launcher/ui/GuiUtil.h | 3 ++- launcher/ui/pages/instance/LogPage.cpp | 11 ++++++----- 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/launcher/ui/GuiUtil.cpp b/launcher/ui/GuiUtil.cpp index 855ab400..29467c3c 100644 --- a/launcher/ui/GuiUtil.cpp +++ b/launcher/ui/GuiUtil.cpp @@ -50,7 +50,7 @@ #include #include -QString GuiUtil::uploadPaste(const QString &name, const QString &text, QWidget *parentWidget) +std::optional GuiUtil::uploadPaste(const QString &name, const QString &text, QWidget *parentWidget) { ProgressDialog dialog(parentWidget); auto pasteTypeSetting = static_cast(APPLICATION->settings()->get("PastebinType").toInt()); @@ -63,7 +63,8 @@ QString GuiUtil::uploadPaste(const QString &name, const QString &text, QWidget * else baseUrl = pasteCustomAPIBaseSetting; - if (baseUrl.isValid()) { + if (baseUrl.isValid()) + { auto response = CustomMessageBox::selectable(parentWidget, QObject::tr("Confirm Upload"), QObject::tr("You are about to upload \"%1\" to %2.\n" "You should double-check for personal information.\n\n" @@ -73,7 +74,7 @@ QString GuiUtil::uploadPaste(const QString &name, const QString &text, QWidget * ->exec(); if (response != QMessageBox::Yes) - return "canceled"; + return {}; } } diff --git a/launcher/ui/GuiUtil.h b/launcher/ui/GuiUtil.h index bf93b3c5..96ebd9a2 100644 --- a/launcher/ui/GuiUtil.h +++ b/launcher/ui/GuiUtil.h @@ -1,10 +1,11 @@ #pragma once #include +#include namespace GuiUtil { -QString uploadPaste(const QString &name, const QString &text, QWidget *parentWidget); +std::optional uploadPaste(const QString &name, const QString &text, QWidget *parentWidget); void setClipboardText(const QString &text); QStringList BrowseForFiles(QString context, QString caption, QString filter, QString defaultPath, QWidget *parentWidget); QString BrowseForFile(QString context, QString caption, QString filter, QString defaultPath, QWidget *parentWidget); diff --git a/launcher/ui/pages/instance/LogPage.cpp b/launcher/ui/pages/instance/LogPage.cpp index 2a6504a2..8f9e569e 100644 --- a/launcher/ui/pages/instance/LogPage.cpp +++ b/launcher/ui/pages/instance/LogPage.cpp @@ -283,17 +283,18 @@ void LogPage::on_btnPaste_clicked() ) ); auto url = GuiUtil::uploadPaste(tr("Minecraft Log"), m_model->toPlainText(), this); - if(url == "canceled") + if(!url.has_value()) { m_model->append(MessageLevel::Error, QString("Log upload canceled")); } - else if(!url.isEmpty()) + else if (url->isNull()) { - m_model->append(MessageLevel::Launcher, QString("Log uploaded to: %1").arg(url)); - } - else { m_model->append(MessageLevel::Error, QString("Log upload failed!")); } + else + { + m_model->append(MessageLevel::Launcher, QString("Log uploaded to: %1").arg(url.value())); + } } void LogPage::on_btnCopy_clicked() From 70573b6f312bc2e40c50c4d6901f676f4270ebc5 Mon Sep 17 00:00:00 2001 From: Adrien <66513643+AshtakaOOf@users.noreply.github.com> Date: Mon, 26 Dec 2022 19:24:17 +0100 Subject: [PATCH 028/152] Update org.prismlauncher.PrismLauncher.metainfo.xml.in Should be the right properties (I hope) Signed-off-by: Adrien <66513643+AshtakaOOf@users.noreply.github.com> --- ...g.prismlauncher.PrismLauncher.metainfo.xml.in | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/program_info/org.prismlauncher.PrismLauncher.metainfo.xml.in b/program_info/org.prismlauncher.PrismLauncher.metainfo.xml.in index 13a860d9..d4905a90 100644 --- a/program_info/org.prismlauncher.PrismLauncher.metainfo.xml.in +++ b/program_info/org.prismlauncher.PrismLauncher.metainfo.xml.in @@ -30,27 +30,31 @@ The main Prism Launcher window - https://prismlauncher.org/img/screenshots/LauncherDark.png + https://prismlauncher.org/img/screenshots/LauncherDark.png Modpack installation - https://prismlauncher.org/img/screenshots/ModpackInstallDark.png + https://prismlauncher.org/img/screenshots/ModpackInstallDark.png Mod installation - https://prismlauncher.org/img/screenshots/ModInstallDark.png + https://prismlauncher.org/img/screenshots/ModInstallDark.png Mod updating - https://prismlauncher.org/img/screenshots/ModUpdateDark.png + https://prismlauncher.org/img/screenshots/ModUpdateDark.png Instance management - https://prismlauncher.org/img/screenshots/PropertiesDark.png + https://prismlauncher.org/img/screenshots/PropertiesDark.png Cat :) - https://prismlauncher.org/img/screenshots/LauncherCatDark.png + https://prismlauncher.org/img/screenshots/LauncherCatDark.png + + + Customization + https://prismlauncher.org/img/screenshots/CustomizeDark.png From 9f1c79a5ece0d5e45fdda8409a4f5339dfc341f0 Mon Sep 17 00:00:00 2001 From: Adrien <66513643+AshtakaOOf@users.noreply.github.com> Date: Mon, 26 Dec 2022 19:59:46 +0100 Subject: [PATCH 029/152] Update org.prismlauncher.PrismLauncher.metainfo.xml.in Add ModpackUpdate and change some lines Signed-off-by: Adrien <66513643+AshtakaOOf@users.noreply.github.com> --- ...org.prismlauncher.PrismLauncher.metainfo.xml.in | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/program_info/org.prismlauncher.PrismLauncher.metainfo.xml.in b/program_info/org.prismlauncher.PrismLauncher.metainfo.xml.in index d4905a90..b2d565e4 100644 --- a/program_info/org.prismlauncher.PrismLauncher.metainfo.xml.in +++ b/program_info/org.prismlauncher.PrismLauncher.metainfo.xml.in @@ -19,12 +19,15 @@

Features:

  • Easily install game modifications, such as Fabric, Forge and Quilt
  • -
  • Control your Java settings
  • +
  • Easily install and update modpacks from the Launcher
  • +
  • Control your Java settings, and enable Mangohud or Gamemode with a toggle
  • Manage worlds and resource packs from the launcher
  • -
  • See logs and other details easily
  • +
  • See logs and other details easily through a dashboard
  • Kill Minecraft in case of a crash/freeze
  • Isolate Minecraft instances to keep everything clean
  • Install and update mods directly from the launcher
  • +
  • Customize the launcher with themes, and more
  • +
  • And cat :3
@@ -37,6 +40,11 @@ https://prismlauncher.org/img/screenshots/ModpackInstallDark.png + + Modpack updating + https://prismlauncher.org/img/screenshots/ModpackUpdateDark.png + + Mod installation https://prismlauncher.org/img/screenshots/ModInstallDark.png @@ -49,7 +57,7 @@ https://prismlauncher.org/img/screenshots/PropertiesDark.png - Cat :) + Cat :3 https://prismlauncher.org/img/screenshots/LauncherCatDark.png From 463b4fbe0cb041d7d27f4fb0a2b19fad9c0f6089 Mon Sep 17 00:00:00 2001 From: Adrien <66513643+AshtakaOOf@users.noreply.github.com> Date: Mon, 26 Dec 2022 20:09:47 +0100 Subject: [PATCH 030/152] Fix Me when me when me when Signed-off-by: Adrien <66513643+AshtakaOOf@users.noreply.github.com> --- .../org.prismlauncher.PrismLauncher.metainfo.xml.in | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/program_info/org.prismlauncher.PrismLauncher.metainfo.xml.in b/program_info/org.prismlauncher.PrismLauncher.metainfo.xml.in index b2d565e4..96708960 100644 --- a/program_info/org.prismlauncher.PrismLauncher.metainfo.xml.in +++ b/program_info/org.prismlauncher.PrismLauncher.metainfo.xml.in @@ -40,11 +40,10 @@ https://prismlauncher.org/img/screenshots/ModpackInstallDark.png - - Modpack updating - https://prismlauncher.org/img/screenshots/ModpackUpdateDark.png - - + Modpack updating + https://prismlauncher.org/img/screenshots/ModpackUpdateDark.png + + Mod installation https://prismlauncher.org/img/screenshots/ModInstallDark.png From 3691f3a2963c77dbd7b469b4b90ca79b61014d43 Mon Sep 17 00:00:00 2001 From: Rachel Powers <508861+Ryex@users.noreply.github.com> Date: Mon, 26 Dec 2022 14:29:13 -0700 Subject: [PATCH 031/152] fix: cleanup and suggested changes Signed-off-by: Rachel Powers <508861+Ryex@users.noreply.github.com> --- launcher/minecraft/mod/DataPack.cpp | 6 +- launcher/minecraft/mod/DataPack.h | 8 +-- launcher/minecraft/mod/Mod.cpp | 2 +- launcher/minecraft/mod/ResourcePack.cpp | 11 ++-- launcher/minecraft/mod/ShaderPack.cpp | 2 - launcher/minecraft/mod/ShaderPack.h | 24 ++++---- launcher/minecraft/mod/WorldSave.cpp | 6 +- launcher/minecraft/mod/WorldSave.h | 14 ++--- .../mod/tasks/LocalDataPackParseTask.cpp | 46 ++++++++------ .../mod/tasks/LocalDataPackParseTask.h | 2 +- .../minecraft/mod/tasks/LocalModParseTask.cpp | 36 ++++++----- .../minecraft/mod/tasks/LocalModParseTask.h | 15 ++--- .../mod/tasks/LocalResourcePackParseTask.cpp | 60 ++++++++++++------- .../mod/tasks/LocalShaderPackParseTask.cpp | 23 ++++--- .../mod/tasks/LocalShaderPackParseTask.h | 3 +- .../mod/tasks/LocalWorldSaveParseTask.cpp | 56 ++++++++++------- .../mod/tasks/LocalWorldSaveParseTask.h | 2 +- .../flame/FlameInstanceCreationTask.cpp | 43 +++++++------ tests/ResourcePackParse_test.cpp | 2 +- 19 files changed, 187 insertions(+), 174 deletions(-) diff --git a/launcher/minecraft/mod/DataPack.cpp b/launcher/minecraft/mod/DataPack.cpp index ea1d097b..5c58f6b2 100644 --- a/launcher/minecraft/mod/DataPack.cpp +++ b/launcher/minecraft/mod/DataPack.cpp @@ -30,9 +30,9 @@ // Values taken from: // https://minecraft.fandom.com/wiki/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") } }, + { 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") } }, }; diff --git a/launcher/minecraft/mod/DataPack.h b/launcher/minecraft/mod/DataPack.h index 17d9b65e..fc2703c7 100644 --- a/launcher/minecraft/mod/DataPack.h +++ b/launcher/minecraft/mod/DataPack.h @@ -45,7 +45,7 @@ class DataPack : public Resource { /** Gets, respectively, the lower and upper versions supported by the set pack format. */ [[nodiscard]] std::pair compatibleVersions() const; - /** Gets the description of the resource pack. */ + /** Gets the description of the data pack. */ [[nodiscard]] QString description() const { return m_description; } /** Thread-safe. */ @@ -62,12 +62,12 @@ class DataPack : public Resource { protected: mutable QMutex m_data_lock; - /* The 'version' of a resource pack, as defined in the pack.mcmeta file. - * See https://minecraft.fandom.com/wiki/Tutorials/Creating_a_resource_pack#Formatting_pack.mcmeta + /* The 'version' of a data pack, as defined in the pack.mcmeta file. + * See https://minecraft.fandom.com/wiki/Data_pack#pack.mcmeta */ int m_pack_format = 0; - /** The resource pack's description, as defined in the pack.mcmeta file. + /** The data pack's description, as defined in the pack.mcmeta file. */ QString m_description; }; diff --git a/launcher/minecraft/mod/Mod.cpp b/launcher/minecraft/mod/Mod.cpp index 8b00354d..3439b6ee 100644 --- a/launcher/minecraft/mod/Mod.cpp +++ b/launcher/minecraft/mod/Mod.cpp @@ -199,4 +199,4 @@ void Mod::finishResolvingWithDetails(ModDetails&& details) bool Mod::valid() const { return !m_local_details.mod_id.isEmpty(); -} \ No newline at end of file +} diff --git a/launcher/minecraft/mod/ResourcePack.cpp b/launcher/minecraft/mod/ResourcePack.cpp index 87995215..876d5c3e 100644 --- a/launcher/minecraft/mod/ResourcePack.cpp +++ b/launcher/minecraft/mod/ResourcePack.cpp @@ -13,12 +13,11 @@ // Values taken from: // https://minecraft.fandom.com/wiki/Tutorials/Creating_a_resource_pack#Formatting_pack.mcmeta 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") } } + { 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") } }, }; diff --git a/launcher/minecraft/mod/ShaderPack.cpp b/launcher/minecraft/mod/ShaderPack.cpp index b8d427c7..6a9641de 100644 --- a/launcher/minecraft/mod/ShaderPack.cpp +++ b/launcher/minecraft/mod/ShaderPack.cpp @@ -24,12 +24,10 @@ #include "minecraft/mod/tasks/LocalShaderPackParseTask.h" - void ShaderPack::setPackFormat(ShaderPackFormat new_format) { QMutexLocker locker(&m_data_lock); - m_pack_format = new_format; } diff --git a/launcher/minecraft/mod/ShaderPack.h b/launcher/minecraft/mod/ShaderPack.h index a0dad7a1..ec0f9404 100644 --- a/launcher/minecraft/mod/ShaderPack.h +++ b/launcher/minecraft/mod/ShaderPack.h @@ -24,31 +24,27 @@ #include "Resource.h" /* Info: - * Currently For Optifine / Iris shader packs, - * could be expanded to support others should they exsist? + * Currently For Optifine / Iris shader packs, + * could be expanded to support others should they exist? * - * This class and enum are mostly here as placeholders for validating - * that a shaderpack exsists and is in the right format, + * This class and enum are mostly here as placeholders for validating + * that a shaderpack exists and is in the right format, * namely that they contain a folder named 'shaders'. * - * In the technical sense it would be possible to parse files like `shaders/shaders.properties` - * to get information like the availble profiles but this is not all that usefull without more knoledge of the - * shader mod used to be able to change settings - * + * In the technical sense it would be possible to parse files like `shaders/shaders.properties` + * to get information like the available profiles but this is not all that useful without more knowledge of the + * shader mod used to be able to change settings. */ #include -enum class ShaderPackFormat { - VALID, - INVALID -}; +enum class ShaderPackFormat { VALID, INVALID }; class ShaderPack : public Resource { Q_OBJECT public: using Ptr = shared_qobject_ptr; - + [[nodiscard]] ShaderPackFormat packFormat() const { return m_pack_format; } ShaderPack(QObject* parent = nullptr) : Resource(parent) {} @@ -62,5 +58,5 @@ class ShaderPack : public Resource { protected: mutable QMutex m_data_lock; - ShaderPackFormat m_pack_format = ShaderPackFormat::INVALID; + ShaderPackFormat m_pack_format = ShaderPackFormat::INVALID; }; diff --git a/launcher/minecraft/mod/WorldSave.cpp b/launcher/minecraft/mod/WorldSave.cpp index 9a626fc1..7123f512 100644 --- a/launcher/minecraft/mod/WorldSave.cpp +++ b/launcher/minecraft/mod/WorldSave.cpp @@ -27,7 +27,6 @@ void WorldSave::setSaveFormat(WorldSaveFormat new_save_format) { QMutexLocker locker(&m_data_lock); - m_save_format = new_save_format; } @@ -35,11 +34,10 @@ void WorldSave::setSaveDirName(QString dir_name) { QMutexLocker locker(&m_data_lock); - m_save_dir_name = dir_name; } bool WorldSave::valid() const { - return m_save_format != WorldSaveFormat::INVALID; -} \ No newline at end of file + return m_save_format != WorldSaveFormat::INVALID; +} diff --git a/launcher/minecraft/mod/WorldSave.h b/launcher/minecraft/mod/WorldSave.h index f703f34c..5985fc8a 100644 --- a/launcher/minecraft/mod/WorldSave.h +++ b/launcher/minecraft/mod/WorldSave.h @@ -27,11 +27,7 @@ class Version; -enum class WorldSaveFormat { - SINGLE, - MULTI, - INVALID -}; +enum class WorldSaveFormat { SINGLE, MULTI, INVALID }; class WorldSave : public Resource { Q_OBJECT @@ -53,15 +49,13 @@ class WorldSave : public Resource { bool valid() const override; - protected: mutable QMutex m_data_lock; - /* The 'version' of a resource pack, as defined in the pack.mcmeta file. - * See https://minecraft.fandom.com/wiki/Tutorials/Creating_a_resource_pack#Formatting_pack.mcmeta + /** The format in which the save file is in. + * Since saves can be distributed in various slightly different ways, this allows us to treat them separately. */ WorldSaveFormat m_save_format = WorldSaveFormat::INVALID; - QString m_save_dir_name; - + QString m_save_dir_name; }; diff --git a/launcher/minecraft/mod/tasks/LocalDataPackParseTask.cpp b/launcher/minecraft/mod/tasks/LocalDataPackParseTask.cpp index 8bc8278b..3fcb2110 100644 --- a/launcher/minecraft/mod/tasks/LocalDataPackParseTask.cpp +++ b/launcher/minecraft/mod/tasks/LocalDataPackParseTask.cpp @@ -25,8 +25,8 @@ #include "Json.h" #include -#include #include +#include #include @@ -40,7 +40,7 @@ bool process(DataPack& pack, ProcessingLevel level) case ResourceType::ZIPFILE: return DataPackUtils::processZIP(pack, level); default: - qWarning() << "Invalid type for resource pack parse task!"; + qWarning() << "Invalid type for data pack parse task!"; return false; } } @@ -49,11 +49,16 @@ bool processFolder(DataPack& pack, ProcessingLevel level) { Q_ASSERT(pack.type() == ResourceType::FOLDER); + auto mcmeta_invalid = [&pack]() { + qWarning() << "Resource pack at" << pack.fileinfo().filePath() << "does not have a valid pack.mcmeta"; + return false; // the mcmeta is not optional + }; + QFileInfo mcmeta_file_info(FS::PathCombine(pack.fileinfo().filePath(), "pack.mcmeta")); if (mcmeta_file_info.exists() && mcmeta_file_info.isFile()) { QFile mcmeta_file(mcmeta_file_info.filePath()); if (!mcmeta_file.open(QIODevice::ReadOnly)) - return false; // can't open mcmeta file + return mcmeta_invalid(); // can't open mcmeta file auto data = mcmeta_file.readAll(); @@ -61,22 +66,22 @@ bool processFolder(DataPack& pack, ProcessingLevel level) mcmeta_file.close(); if (!mcmeta_result) { - return false; // mcmeta invalid + return mcmeta_invalid(); // mcmeta invalid } } else { - return false; // mcmeta file isn't a valid file + return mcmeta_invalid(); // mcmeta file isn't a valid file } QFileInfo data_dir_info(FS::PathCombine(pack.fileinfo().filePath(), "data")); if (!data_dir_info.exists() || !data_dir_info.isDir()) { - return false; // data dir does not exists or isn't valid + return false; // data dir does not exists or isn't valid } if (level == ProcessingLevel::BasicInfoOnly) { - return true; // only need basic info already checked + return true; // only need basic info already checked } - return true; // all tests passed + return true; // all tests passed } bool processZIP(DataPack& pack, ProcessingLevel level) @@ -85,15 +90,20 @@ bool processZIP(DataPack& pack, ProcessingLevel level) QuaZip zip(pack.fileinfo().filePath()); if (!zip.open(QuaZip::mdUnzip)) - return false; // can't open zip file + return false; // can't open zip file QuaZipFile file(&zip); + auto mcmeta_invalid = [&pack]() { + qWarning() << "Resource pack at" << pack.fileinfo().filePath() << "does not have a valid pack.mcmeta"; + return false; // the mcmeta is not optional + }; + if (zip.setCurrentFile("pack.mcmeta")) { if (!file.open(QIODevice::ReadOnly)) { qCritical() << "Failed to open file in zip."; zip.close(); - return false; + return mcmeta_invalid(); } auto data = file.readAll(); @@ -102,20 +112,20 @@ bool processZIP(DataPack& pack, ProcessingLevel level) file.close(); if (!mcmeta_result) { - return false; // mcmeta invalid + return mcmeta_invalid(); // mcmeta invalid } } else { - return false; // could not set pack.mcmeta as current file. + return mcmeta_invalid(); // could not set pack.mcmeta as current file. } QuaZipDir zipDir(&zip); if (!zipDir.exists("/data")) { - return false; // data dir does not exists at zip root + return false; // data dir does not exists at zip root } if (level == ProcessingLevel::BasicInfoOnly) { zip.close(); - return true; // only need basic info already checked + return true; // only need basic info already checked } zip.close(); @@ -123,7 +133,7 @@ bool processZIP(DataPack& pack, ProcessingLevel level) return true; } -// https://minecraft.fandom.com/wiki/Tutorials/Creating_a_resource_pack#Formatting_pack.mcmeta +// https://minecraft.fandom.com/wiki/Data_pack#pack.mcmeta bool processMCMeta(DataPack& pack, QByteArray&& raw_data) { try { @@ -147,9 +157,7 @@ bool validate(QFileInfo file) } // namespace DataPackUtils -LocalDataPackParseTask::LocalDataPackParseTask(int token, DataPack& dp) - : Task(nullptr, false), m_token(token), m_resource_pack(dp) -{} +LocalDataPackParseTask::LocalDataPackParseTask(int token, DataPack& dp) : Task(nullptr, false), m_token(token), m_data_pack(dp) {} bool LocalDataPackParseTask::abort() { @@ -159,7 +167,7 @@ bool LocalDataPackParseTask::abort() void LocalDataPackParseTask::executeTask() { - if (!DataPackUtils::process(m_resource_pack)) + if (!DataPackUtils::process(m_data_pack)) return; if (m_aborted) diff --git a/launcher/minecraft/mod/tasks/LocalDataPackParseTask.h b/launcher/minecraft/mod/tasks/LocalDataPackParseTask.h index 54e3d398..12fd8c82 100644 --- a/launcher/minecraft/mod/tasks/LocalDataPackParseTask.h +++ b/launcher/minecraft/mod/tasks/LocalDataPackParseTask.h @@ -59,7 +59,7 @@ class LocalDataPackParseTask : public Task { private: int m_token; - DataPack& m_resource_pack; + DataPack& m_data_pack; bool m_aborted = false; }; diff --git a/launcher/minecraft/mod/tasks/LocalModParseTask.cpp b/launcher/minecraft/mod/tasks/LocalModParseTask.cpp index e8fd39b6..8bfe2c84 100644 --- a/launcher/minecraft/mod/tasks/LocalModParseTask.cpp +++ b/launcher/minecraft/mod/tasks/LocalModParseTask.cpp @@ -284,7 +284,8 @@ ModDetails ReadLiteModInfo(QByteArray contents) return details; } -bool process(Mod& mod, ProcessingLevel level) { +bool process(Mod& mod, ProcessingLevel level) +{ switch (mod.type()) { case ResourceType::FOLDER: return processFolder(mod, level); @@ -293,13 +294,13 @@ bool process(Mod& mod, ProcessingLevel level) { case ResourceType::LITEMOD: return processLitemod(mod); default: - qWarning() << "Invalid type for resource pack parse task!"; + qWarning() << "Invalid type for mod parse task!"; return false; } } -bool processZIP(Mod& mod, ProcessingLevel level) { - +bool processZIP(Mod& mod, ProcessingLevel level) +{ ModDetails details; QuaZip zip(mod.fileinfo().filePath()); @@ -316,7 +317,7 @@ bool processZIP(Mod& mod, ProcessingLevel level) { details = ReadMCModTOML(file.readAll()); file.close(); - + // to replace ${file.jarVersion} with the actual version, as needed if (details.version == "${file.jarVersion}") { if (zip.setCurrentFile("META-INF/MANIFEST.MF")) { @@ -347,7 +348,6 @@ bool processZIP(Mod& mod, ProcessingLevel level) { } } - zip.close(); mod.setDetails(details); @@ -403,11 +403,11 @@ bool processZIP(Mod& mod, ProcessingLevel level) { } zip.close(); - return false; // no valid mod found in archive + return false; // no valid mod found in archive } -bool processFolder(Mod& mod, ProcessingLevel level) { - +bool processFolder(Mod& mod, ProcessingLevel level) +{ ModDetails details; QFileInfo mcmod_info(FS::PathCombine(mod.fileinfo().filePath(), "mcmod.info")); @@ -424,13 +424,13 @@ bool processFolder(Mod& mod, ProcessingLevel level) { return true; } - return false; // no valid mcmod.info file found + return false; // no valid mcmod.info file found } -bool processLitemod(Mod& mod, ProcessingLevel level) { - +bool processLitemod(Mod& mod, ProcessingLevel level) +{ ModDetails details; - + QuaZip zip(mod.fileinfo().filePath()); if (!zip.open(QuaZip::mdUnzip)) return false; @@ -451,24 +451,22 @@ bool processLitemod(Mod& mod, ProcessingLevel level) { } zip.close(); - return false; // no valid litemod.json found in archive + return false; // no valid litemod.json found in archive } /** Checks whether a file is valid as a mod or not. */ -bool validate(QFileInfo file) { - +bool validate(QFileInfo file) +{ Mod mod{ file }; return ModUtils::process(mod, ProcessingLevel::BasicInfoOnly) && mod.valid(); } } // namespace ModUtils - LocalModParseTask::LocalModParseTask(int token, ResourceType type, const QFileInfo& modFile) : Task(nullptr, false), m_token(token), m_type(type), m_modFile(modFile), m_result(new Result()) {} - bool LocalModParseTask::abort() { m_aborted.store(true); @@ -476,7 +474,7 @@ bool LocalModParseTask::abort() } void LocalModParseTask::executeTask() -{ +{ Mod mod{ m_modFile }; ModUtils::process(mod, ModUtils::ProcessingLevel::Full); diff --git a/launcher/minecraft/mod/tasks/LocalModParseTask.h b/launcher/minecraft/mod/tasks/LocalModParseTask.h index c9512166..38dae135 100644 --- a/launcher/minecraft/mod/tasks/LocalModParseTask.h +++ b/launcher/minecraft/mod/tasks/LocalModParseTask.h @@ -27,32 +27,29 @@ bool processLitemod(Mod& mod, ProcessingLevel level = ProcessingLevel::Full); bool validate(QFileInfo file); } // namespace ModUtils -class LocalModParseTask : public Task -{ +class LocalModParseTask : public Task { Q_OBJECT -public: + public: struct Result { ModDetails details; }; using ResultPtr = std::shared_ptr; - ResultPtr result() const { - return m_result; - } + ResultPtr result() const { return m_result; } [[nodiscard]] bool canAbort() const override { return true; } bool abort() override; - LocalModParseTask(int token, ResourceType type, const QFileInfo & modFile); + LocalModParseTask(int token, ResourceType type, const QFileInfo& modFile); void executeTask() override; [[nodiscard]] int token() const { return m_token; } -private: + private: void processAsZip(); void processAsFolder(); void processAsLitemod(); -private: + private: int m_token; ResourceType m_type; QFileInfo m_modFile; diff --git a/launcher/minecraft/mod/tasks/LocalResourcePackParseTask.cpp b/launcher/minecraft/mod/tasks/LocalResourcePackParseTask.cpp index 2c41c9ae..4bf0b80d 100644 --- a/launcher/minecraft/mod/tasks/LocalResourcePackParseTask.cpp +++ b/launcher/minecraft/mod/tasks/LocalResourcePackParseTask.cpp @@ -22,8 +22,8 @@ #include "Json.h" #include -#include #include +#include #include @@ -46,11 +46,16 @@ bool processFolder(ResourcePack& pack, ProcessingLevel level) { Q_ASSERT(pack.type() == ResourceType::FOLDER); + auto mcmeta_invalid = [&pack]() { + qWarning() << "Resource pack at" << pack.fileinfo().filePath() << "does not have a valid pack.mcmeta"; + return false; // the mcmeta is not optional + }; + QFileInfo mcmeta_file_info(FS::PathCombine(pack.fileinfo().filePath(), "pack.mcmeta")); if (mcmeta_file_info.exists() && mcmeta_file_info.isFile()) { QFile mcmeta_file(mcmeta_file_info.filePath()); if (!mcmeta_file.open(QIODevice::ReadOnly)) - return false; // can't open mcmeta file + return mcmeta_invalid(); // can't open mcmeta file auto data = mcmeta_file.readAll(); @@ -58,26 +63,31 @@ bool processFolder(ResourcePack& pack, ProcessingLevel level) mcmeta_file.close(); if (!mcmeta_result) { - return false; // mcmeta invalid + return mcmeta_invalid(); // mcmeta invalid } } else { - return false; // mcmeta file isn't a valid file + return mcmeta_invalid(); // mcmeta file isn't a valid file } QFileInfo assets_dir_info(FS::PathCombine(pack.fileinfo().filePath(), "assets")); if (!assets_dir_info.exists() || !assets_dir_info.isDir()) { - return false; // assets dir does not exists or isn't valid + return false; // assets dir does not exists or isn't valid } if (level == ProcessingLevel::BasicInfoOnly) { - return true; // only need basic info already checked + return true; // only need basic info already checked } - + + auto png_invalid = [&pack]() { + qWarning() << "Resource pack at" << pack.fileinfo().filePath() << "does not have a valid pack.png"; + return true; // the png is optional + }; + QFileInfo image_file_info(FS::PathCombine(pack.fileinfo().filePath(), "pack.png")); if (image_file_info.exists() && image_file_info.isFile()) { QFile pack_png_file(image_file_info.filePath()); if (!pack_png_file.open(QIODevice::ReadOnly)) - return false; // can't open pack.png file + return png_invalid(); // can't open pack.png file auto data = pack_png_file.readAll(); @@ -85,13 +95,13 @@ bool processFolder(ResourcePack& pack, ProcessingLevel level) pack_png_file.close(); if (!pack_png_result) { - return false; // pack.png invalid + return png_invalid(); // pack.png invalid } } else { - return false; // pack.png does not exists or is not a valid file. + return png_invalid(); // pack.png does not exists or is not a valid file. } - return true; // all tests passed + return true; // all tests passed } bool processZIP(ResourcePack& pack, ProcessingLevel level) @@ -100,15 +110,20 @@ bool processZIP(ResourcePack& pack, ProcessingLevel level) QuaZip zip(pack.fileinfo().filePath()); if (!zip.open(QuaZip::mdUnzip)) - return false; // can't open zip file + return false; // can't open zip file QuaZipFile file(&zip); + auto mcmeta_invalid = [&pack]() { + qWarning() << "Resource pack at" << pack.fileinfo().filePath() << "does not have a valid pack.mcmeta"; + return false; // the mcmeta is not optional + }; + if (zip.setCurrentFile("pack.mcmeta")) { if (!file.open(QIODevice::ReadOnly)) { qCritical() << "Failed to open file in zip."; zip.close(); - return false; + return mcmeta_invalid(); } auto data = file.readAll(); @@ -117,27 +132,32 @@ bool processZIP(ResourcePack& pack, ProcessingLevel level) file.close(); if (!mcmeta_result) { - return false; // mcmeta invalid + return mcmeta_invalid(); // mcmeta invalid } } else { - return false; // could not set pack.mcmeta as current file. + return mcmeta_invalid(); // could not set pack.mcmeta as current file. } QuaZipDir zipDir(&zip); if (!zipDir.exists("/assets")) { - return false; // assets dir does not exists at zip root + return false; // assets dir does not exists at zip root } if (level == ProcessingLevel::BasicInfoOnly) { zip.close(); - return true; // only need basic info already checked + return true; // only need basic info already checked } + auto png_invalid = [&pack]() { + qWarning() << "Resource pack at" << pack.fileinfo().filePath() << "does not have a valid pack.png"; + return true; // the png is optional + }; + if (zip.setCurrentFile("pack.png")) { if (!file.open(QIODevice::ReadOnly)) { qCritical() << "Failed to open file in zip."; zip.close(); - return false; + return png_invalid(); } auto data = file.readAll(); @@ -146,10 +166,10 @@ bool processZIP(ResourcePack& pack, ProcessingLevel level) file.close(); if (!pack_png_result) { - return false; // pack.png invalid + return png_invalid(); // pack.png invalid } } else { - return false; // could not set pack.mcmeta as current file. + return png_invalid(); // could not set pack.mcmeta as current file. } zip.close(); diff --git a/launcher/minecraft/mod/tasks/LocalShaderPackParseTask.cpp b/launcher/minecraft/mod/tasks/LocalShaderPackParseTask.cpp index 088853b9..a9949735 100644 --- a/launcher/minecraft/mod/tasks/LocalShaderPackParseTask.cpp +++ b/launcher/minecraft/mod/tasks/LocalShaderPackParseTask.cpp @@ -24,8 +24,8 @@ #include "FileSystem.h" #include -#include #include +#include namespace ShaderPackUtils { @@ -45,18 +45,18 @@ bool process(ShaderPack& pack, ProcessingLevel level) bool processFolder(ShaderPack& pack, ProcessingLevel level) { Q_ASSERT(pack.type() == ResourceType::FOLDER); - + QFileInfo shaders_dir_info(FS::PathCombine(pack.fileinfo().filePath(), "shaders")); if (!shaders_dir_info.exists() || !shaders_dir_info.isDir()) { - return false; // assets dir does not exists or isn't valid + return false; // assets dir does not exists or isn't valid } pack.setPackFormat(ShaderPackFormat::VALID); if (level == ProcessingLevel::BasicInfoOnly) { - return true; // only need basic info already checked + return true; // only need basic info already checked } - - return true; // all tests passed + + return true; // all tests passed } bool processZIP(ShaderPack& pack, ProcessingLevel level) @@ -65,19 +65,19 @@ bool processZIP(ShaderPack& pack, ProcessingLevel level) QuaZip zip(pack.fileinfo().filePath()); if (!zip.open(QuaZip::mdUnzip)) - return false; // can't open zip file + return false; // can't open zip file QuaZipFile file(&zip); QuaZipDir zipDir(&zip); if (!zipDir.exists("/shaders")) { - return false; // assets dir does not exists at zip root + return false; // assets dir does not exists at zip root } pack.setPackFormat(ShaderPackFormat::VALID); if (level == ProcessingLevel::BasicInfoOnly) { zip.close(); - return true; // only need basic info already checked + return true; // only need basic info already checked } zip.close(); @@ -85,7 +85,6 @@ bool processZIP(ShaderPack& pack, ProcessingLevel level) return true; } - bool validate(QFileInfo file) { ShaderPack sp{ file }; @@ -94,9 +93,7 @@ bool validate(QFileInfo file) } // namespace ShaderPackUtils -LocalShaderPackParseTask::LocalShaderPackParseTask(int token, ShaderPack& sp) - : Task(nullptr, false), m_token(token), m_shader_pack(sp) -{} +LocalShaderPackParseTask::LocalShaderPackParseTask(int token, ShaderPack& sp) : Task(nullptr, false), m_token(token), m_shader_pack(sp) {} bool LocalShaderPackParseTask::abort() { diff --git a/launcher/minecraft/mod/tasks/LocalShaderPackParseTask.h b/launcher/minecraft/mod/tasks/LocalShaderPackParseTask.h index 5d113508..6be2183c 100644 --- a/launcher/minecraft/mod/tasks/LocalShaderPackParseTask.h +++ b/launcher/minecraft/mod/tasks/LocalShaderPackParseTask.h @@ -19,7 +19,6 @@ * along with this program. If not, see . */ - #pragma once #include @@ -38,7 +37,7 @@ bool process(ShaderPack& pack, ProcessingLevel level = ProcessingLevel::Full); bool processZIP(ShaderPack& pack, ProcessingLevel level = ProcessingLevel::Full); bool processFolder(ShaderPack& pack, ProcessingLevel level = ProcessingLevel::Full); -/** Checks whether a file is valid as a resource pack or not. */ +/** Checks whether a file is valid as a shader pack or not. */ bool validate(QFileInfo file); } // namespace ShaderPackUtils diff --git a/launcher/minecraft/mod/tasks/LocalWorldSaveParseTask.cpp b/launcher/minecraft/mod/tasks/LocalWorldSaveParseTask.cpp index b7f2420a..cbc8f8ce 100644 --- a/launcher/minecraft/mod/tasks/LocalWorldSaveParseTask.cpp +++ b/launcher/minecraft/mod/tasks/LocalWorldSaveParseTask.cpp @@ -24,12 +24,12 @@ #include "FileSystem.h" -#include -#include #include -#include #include -#include +#include + +#include +#include namespace WorldSaveUtils { @@ -41,15 +41,22 @@ bool process(WorldSave& pack, ProcessingLevel level) case ResourceType::ZIPFILE: return WorldSaveUtils::processZIP(pack, level); default: - qWarning() << "Invalid type for shader pack parse task!"; + qWarning() << "Invalid type for world save parse task!"; return false; } } - +/// @brief checks a folder structure to see if it contains a level.dat +/// @param dir the path to check +/// @param saves used in recursive call if a "saves" dir was found +/// @return std::tuple of ( +/// bool , +/// QString , +/// bool +/// ) static std::tuple contains_level_dat(QDir dir, bool saves = false) { - for(auto const& entry : dir.entryInfoList()) { + for (auto const& entry : dir.entryInfoList()) { if (!entry.isDir()) { continue; } @@ -64,12 +71,11 @@ static std::tuple contains_level_dat(QDir dir, bool saves = return std::make_tuple(false, "", saves); } - bool processFolder(WorldSave& save, ProcessingLevel level) { Q_ASSERT(save.type() == ResourceType::FOLDER); - auto [ found, save_dir_name, found_saves_dir ] = contains_level_dat(QDir(save.fileinfo().filePath())); + auto [found, save_dir_name, found_saves_dir] = contains_level_dat(QDir(save.fileinfo().filePath())); if (!found) { return false; @@ -84,14 +90,21 @@ bool processFolder(WorldSave& save, ProcessingLevel level) } if (level == ProcessingLevel::BasicInfoOnly) { - return true; // only need basic info already checked + return true; // only need basic info already checked } - // resurved for more intensive processing - - return true; // all tests passed + // reserved for more intensive processing + + return true; // all tests passed } +/// @brief checks a folder structure to see if it contains a level.dat +/// @param zip the zip file to check +/// @return std::tuple of ( +/// bool , +/// QString , +/// bool +/// ) static std::tuple contains_level_dat(QuaZip& zip) { bool saves = false; @@ -100,7 +113,7 @@ static std::tuple contains_level_dat(QuaZip& zip) saves = true; zipDir.cd("/saves"); } - + for (auto const& entry : zipDir.entryList()) { zipDir.cd(entry); if (zipDir.exists("level.dat")) { @@ -117,14 +130,14 @@ bool processZIP(WorldSave& save, ProcessingLevel level) QuaZip zip(save.fileinfo().filePath()); if (!zip.open(QuaZip::mdUnzip)) - return false; // can't open zip file + return false; // can't open zip file - auto [ found, save_dir_name, found_saves_dir ] = contains_level_dat(zip); + auto [found, save_dir_name, found_saves_dir] = contains_level_dat(zip); if (save_dir_name.endsWith("/")) { save_dir_name.chop(1); } - + if (!found) { return false; } @@ -139,17 +152,16 @@ bool processZIP(WorldSave& save, ProcessingLevel level) if (level == ProcessingLevel::BasicInfoOnly) { zip.close(); - return true; // only need basic info already checked + return true; // only need basic info already checked } - // resurved for more intensive processing + // reserved for more intensive processing zip.close(); return true; } - bool validate(QFileInfo file) { WorldSave sp{ file }; @@ -158,9 +170,7 @@ bool validate(QFileInfo file) } // namespace WorldSaveUtils -LocalWorldSaveParseTask::LocalWorldSaveParseTask(int token, WorldSave& save) - : Task(nullptr, false), m_token(token), m_save(save) -{} +LocalWorldSaveParseTask::LocalWorldSaveParseTask(int token, WorldSave& save) : Task(nullptr, false), m_token(token), m_save(save) {} bool LocalWorldSaveParseTask::abort() { diff --git a/launcher/minecraft/mod/tasks/LocalWorldSaveParseTask.h b/launcher/minecraft/mod/tasks/LocalWorldSaveParseTask.h index aa5db0c2..9dcdca2b 100644 --- a/launcher/minecraft/mod/tasks/LocalWorldSaveParseTask.h +++ b/launcher/minecraft/mod/tasks/LocalWorldSaveParseTask.h @@ -59,4 +59,4 @@ class LocalWorldSaveParseTask : public Task { WorldSave& m_save; bool m_aborted = false; -}; \ No newline at end of file +}; diff --git a/launcher/modplatform/flame/FlameInstanceCreationTask.cpp b/launcher/modplatform/flame/FlameInstanceCreationTask.cpp index b62d05ab..79104e17 100644 --- a/launcher/modplatform/flame/FlameInstanceCreationTask.cpp +++ b/launcher/modplatform/flame/FlameInstanceCreationTask.cpp @@ -53,15 +53,16 @@ #include "ui/dialogs/BlockedModsDialog.h" #include "ui/dialogs/CustomMessageBox.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include +#include +#include + +#include "minecraft/World.h" +#include "minecraft/mod/tasks/LocalDataPackParseTask.h" +#include "minecraft/mod/tasks/LocalModParseTask.h" +#include "minecraft/mod/tasks/LocalResourcePackParseTask.h" +#include "minecraft/mod/tasks/LocalShaderPackParseTask.h" +#include "minecraft/mod/tasks/LocalTexturePackParseTask.h" +#include "minecraft/mod/tasks/LocalWorldSaveParseTask.h" const static QMap forgemap = { { "1.2.5", "3.4.9.171" }, { "1.4.2", "6.0.1.355" }, @@ -411,8 +412,7 @@ void FlameCreationTask::idResolverSucceeded(QEventLoop& loop) QList blocked_mods; auto anyBlocked = false; for (const auto& result : results.files.values()) { - - if(result.fileName.endsWith(".zip")) { + if (result.fileName.endsWith(".zip")) { m_ZIP_resources.append(std::make_pair(result.fileName, result.targetFolder)); } @@ -454,7 +454,6 @@ void FlameCreationTask::idResolverSucceeded(QEventLoop& loop) } } - void FlameCreationTask::setupDownloadJob(QEventLoop& loop) { m_files_job = new NetJob(tr("Mod download"), APPLICATION->network()); @@ -493,8 +492,8 @@ void FlameCreationTask::setupDownloadJob(QEventLoop& loop) } m_mod_id_resolver.reset(); - connect(m_files_job.get(), &NetJob::succeeded, this, [&]() { - m_files_job.reset(); + connect(m_files_job.get(), &NetJob::succeeded, this, [&]() { + m_files_job.reset(); validateZIPResouces(); }); connect(m_files_job.get(), &NetJob::failed, [&](QString reason) { @@ -543,26 +542,26 @@ void FlameCreationTask::copyBlockedMods(QList const& blocked_mods) bool moveFile(QString src, QString dst) { if (!FS::copy(src, dst)()) { // copy - qDebug() << "Copy of" << src << "to" << dst << "Failed!"; + qDebug() << "Copy of" << src << "to" << dst << "failed!"; return false; } else { if (!FS::deletePath(src)) { // remove original - qDebug() << "Deleation of" << src << "Failed!"; + qDebug() << "Deletion of" << src << "failed!"; return false; }; } return true; } - void FlameCreationTask::validateZIPResouces() { - qDebug() << "Validating resoucres stored as .zip are in the right place"; + qDebug() << "Validating whether resources stored as .zip are in the right place"; for (auto [fileName, targetFolder] : m_ZIP_resources) { - qDebug() << "Checking" << fileName << "..."; auto localPath = FS::PathCombine(m_stagingPath, "minecraft", targetFolder, fileName); + /// @brief check the target and move the the file + /// @return path where file can now be found auto validatePath = [&localPath, this](QString fileName, QString targetFolder, QString realTarget) { if (targetFolder != realTarget) { qDebug() << "Target folder of" << fileName << "is incorrect, it belongs in" << realTarget; @@ -589,7 +588,7 @@ void FlameCreationTask::validateZIPResouces() } else if (ModUtils::validate(localFileInfo)) { qDebug() << fileName << "is a mod"; validatePath(fileName, targetFolder, "mods"); - } else if (WorldSaveUtils::validate(localFileInfo)) { + } else if (WorldSaveUtils::validate(localFileInfo)) { qDebug() << fileName << "is a world save"; QString worldPath = validatePath(fileName, targetFolder, "saves"); @@ -600,7 +599,7 @@ void FlameCreationTask::validateZIPResouces() qDebug() << "World at" << worldPath << "is not valid, skipping install."; } else { w.install(FS::PathCombine(m_stagingPath, "minecraft", "saves")); - } + } } else if (ShaderPackUtils::validate(localFileInfo)) { // in theroy flame API can't do this but who knows, that *may* change ? // better to handle it if it *does* occure in the future @@ -610,7 +609,7 @@ void FlameCreationTask::validateZIPResouces() qDebug() << "Can't Identify" << fileName << "at" << localPath << ", leaving it where it is."; } } else { - qDebug() << "Can't find" << localPath << "to validate it, ignoreing"; + qDebug() << "Can't find" << localPath << "to validate it, ignoring"; } } } diff --git a/tests/ResourcePackParse_test.cpp b/tests/ResourcePackParse_test.cpp index 4192da31..7f2f86bf 100644 --- a/tests/ResourcePackParse_test.cpp +++ b/tests/ResourcePackParse_test.cpp @@ -67,7 +67,7 @@ class ResourcePackParseTest : public QObject { QVERIFY(pack.packFormat() == 6); QVERIFY(pack.description() == "o quartel pegou fogo, policia deu sinal, acode acode acode a bandeira nacional"); - QVERIFY(valid == false); + QVERIFY(valid == false); // no assets dir } }; From 58d3779efb3d7517a62345ea58d31748753890c6 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 27 Dec 2022 12:20:21 +0000 Subject: [PATCH 032/152] chore(deps): update actions/cache action to v3.2.2 --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f415741d..14c5b5e5 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -165,7 +165,7 @@ jobs: - name: Retrieve ccache cache (Windows MinGW-w64) if: runner.os == 'Windows' && matrix.msystem != '' && inputs.build_type == 'Debug' - uses: actions/cache@v3.2.1 + uses: actions/cache@v3.2.2 with: path: '${{ github.workspace }}\.ccache' key: ${{ matrix.os }}-mingw-w64 From c8d8046412467d10abd439bf2066b2304122d7c6 Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Tue, 27 Dec 2022 17:04:42 +0100 Subject: [PATCH 033/152] refactor: add logging category for credentials Signed-off-by: Sefa Eyeoglu --- launcher/CMakeLists.txt | 2 ++ launcher/Logging.cpp | 22 ++++++++++++++++ launcher/Logging.h | 24 ++++++++++++++++++ launcher/minecraft/auth/Parsers.cpp | 25 ++++++------------- .../minecraft/auth/steps/EntitlementsStep.cpp | 5 ++-- .../auth/steps/LauncherLoginStep.cpp | 15 ++++------- launcher/minecraft/auth/steps/MSAStep.cpp | 7 +++--- .../auth/steps/MinecraftProfileStep.cpp | 5 ++-- .../auth/steps/MinecraftProfileStepMojang.cpp | 5 ++-- .../auth/steps/XboxAuthorizationStep.cpp | 5 ++-- .../minecraft/auth/steps/XboxProfileStep.cpp | 10 +++----- .../katabasis/include/katabasis/DeviceFlow.h | 3 +++ libraries/katabasis/src/DeviceFlow.cpp | 7 +++--- 13 files changed, 81 insertions(+), 54 deletions(-) create mode 100644 launcher/Logging.cpp create mode 100644 launcher/Logging.h diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index a0d92b6e..21597081 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -27,6 +27,8 @@ set(CORE_SOURCES StringUtils.h StringUtils.cpp RuntimeContext.h + Logging.h + Logging.cpp # Basic instance manipulation tasks (derived from InstanceTask) InstanceCreationTask.h diff --git a/launcher/Logging.cpp b/launcher/Logging.cpp new file mode 100644 index 00000000..d0e30473 --- /dev/null +++ b/launcher/Logging.cpp @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - 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 . + * + */ + +#include "Logging.h" + +Q_LOGGING_CATEGORY(authCredentials, "launcher.auth.credentials", QtWarningMsg) diff --git a/launcher/Logging.h b/launcher/Logging.h new file mode 100644 index 00000000..0fcb30b7 --- /dev/null +++ b/launcher/Logging.h @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - 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 . + * + */ + +#pragma once + +#include + +Q_DECLARE_LOGGING_CATEGORY(authCredentials) diff --git a/launcher/minecraft/auth/Parsers.cpp b/launcher/minecraft/auth/Parsers.cpp index 47473899..f3d9ad56 100644 --- a/launcher/minecraft/auth/Parsers.cpp +++ b/launcher/minecraft/auth/Parsers.cpp @@ -1,5 +1,6 @@ #include "Parsers.h" #include "Json.h" +#include "Logging.h" #include #include @@ -75,9 +76,7 @@ bool getBool(QJsonValue value, bool & out) { bool parseXTokenResponse(QByteArray & data, Katabasis::Token &output, QString name) { qDebug() << "Parsing" << name <<":"; -#ifndef NDEBUG - qDebug() << data; -#endif + qCDebug(authCredentials()) << data; QJsonParseError jsonError; QJsonDocument doc = QJsonDocument::fromJson(data, &jsonError); if(jsonError.error) { @@ -137,9 +136,7 @@ bool parseXTokenResponse(QByteArray & data, Katabasis::Token &output, QString na bool parseMinecraftProfile(QByteArray & data, MinecraftProfile &output) { qDebug() << "Parsing Minecraft profile..."; -#ifndef NDEBUG - qDebug() << data; -#endif + qCDebug(authCredentials()) << data; QJsonParseError jsonError; QJsonDocument doc = QJsonDocument::fromJson(data, &jsonError); @@ -275,9 +272,7 @@ decoded base64 "value": bool parseMinecraftProfileMojang(QByteArray & data, MinecraftProfile &output) { qDebug() << "Parsing Minecraft profile..."; -#ifndef NDEBUG - qDebug() << data; -#endif + qCDebug(authCredentials()) << data; QJsonParseError jsonError; QJsonDocument doc = QJsonDocument::fromJson(data, &jsonError); @@ -389,9 +384,7 @@ bool parseMinecraftProfileMojang(QByteArray & data, MinecraftProfile &output) { bool parseMinecraftEntitlements(QByteArray & data, MinecraftEntitlement &output) { qDebug() << "Parsing Minecraft entitlements..."; -#ifndef NDEBUG - qDebug() << data; -#endif + qCDebug(authCredentials()) << data; QJsonParseError jsonError; QJsonDocument doc = QJsonDocument::fromJson(data, &jsonError); @@ -424,9 +417,7 @@ bool parseMinecraftEntitlements(QByteArray & data, MinecraftEntitlement &output) bool parseRolloutResponse(QByteArray & data, bool& result) { qDebug() << "Parsing Rollout response..."; -#ifndef NDEBUG - qDebug() << data; -#endif + qCDebug(authCredentials()) << data; QJsonParseError jsonError; QJsonDocument doc = QJsonDocument::fromJson(data, &jsonError); @@ -455,9 +446,7 @@ bool parseRolloutResponse(QByteArray & data, bool& result) { bool parseMojangResponse(QByteArray & data, Katabasis::Token &output) { QJsonParseError jsonError; qDebug() << "Parsing Mojang response..."; -#ifndef NDEBUG - qDebug() << data; -#endif + qCDebug(authCredentials()) << data; QJsonDocument doc = QJsonDocument::fromJson(data, &jsonError); if(jsonError.error) { qWarning() << "Failed to parse response from api.minecraftservices.com/launcher/login as JSON: " << jsonError.errorString(); diff --git a/launcher/minecraft/auth/steps/EntitlementsStep.cpp b/launcher/minecraft/auth/steps/EntitlementsStep.cpp index f726244f..bd604292 100644 --- a/launcher/minecraft/auth/steps/EntitlementsStep.cpp +++ b/launcher/minecraft/auth/steps/EntitlementsStep.cpp @@ -3,6 +3,7 @@ #include #include +#include "Logging.h" #include "minecraft/auth/AuthRequest.h" #include "minecraft/auth/Parsers.h" @@ -41,9 +42,7 @@ void EntitlementsStep::onRequestDone( auto requestor = qobject_cast(QObject::sender()); requestor->deleteLater(); -#ifndef NDEBUG - qDebug() << data; -#endif + qCDebug(authCredentials()) << data; // TODO: check presence of same entitlementsRequestId? // TODO: validate JWTs? diff --git a/launcher/minecraft/auth/steps/LauncherLoginStep.cpp b/launcher/minecraft/auth/steps/LauncherLoginStep.cpp index 8c53f037..8a26cbe7 100644 --- a/launcher/minecraft/auth/steps/LauncherLoginStep.cpp +++ b/launcher/minecraft/auth/steps/LauncherLoginStep.cpp @@ -2,9 +2,10 @@ #include +#include "Logging.h" +#include "minecraft/auth/AccountTask.h" #include "minecraft/auth/AuthRequest.h" #include "minecraft/auth/Parsers.h" -#include "minecraft/auth/AccountTask.h" #include "net/NetUtils.h" LauncherLoginStep::LauncherLoginStep(AccountData* data) : AuthStep(data) { @@ -51,14 +52,10 @@ void LauncherLoginStep::onRequestDone( auto requestor = qobject_cast(QObject::sender()); requestor->deleteLater(); -#ifndef NDEBUG - qDebug() << data; -#endif + qCDebug(authCredentials()) << data; if (error != QNetworkReply::NoError) { qWarning() << "Reply error:" << error; -#ifndef NDEBUG - qDebug() << data; -#endif + qCDebug(authCredentials()) << data; if (Net::isApplicationError(error)) { emit finished( AccountTaskState::STATE_FAILED_SOFT, @@ -76,9 +73,7 @@ void LauncherLoginStep::onRequestDone( if(!Parsers::parseMojangResponse(data, m_data->yggdrasilToken)) { qWarning() << "Could not parse login_with_xbox response..."; -#ifndef NDEBUG - qDebug() << data; -#endif + qCDebug(authCredentials()) << data; emit finished( AccountTaskState::STATE_FAILED_SOFT, tr("Failed to parse the Minecraft access token response.") diff --git a/launcher/minecraft/auth/steps/MSAStep.cpp b/launcher/minecraft/auth/steps/MSAStep.cpp index 16afcb42..6fc8d468 100644 --- a/launcher/minecraft/auth/steps/MSAStep.cpp +++ b/launcher/minecraft/auth/steps/MSAStep.cpp @@ -42,6 +42,7 @@ #include "minecraft/auth/Parsers.h" #include "Application.h" +#include "Logging.h" using OAuth2 = Katabasis::DeviceFlow; using Activity = Katabasis::Activity; @@ -117,14 +118,12 @@ void MSAStep::onOAuthActivityChanged(Katabasis::Activity activity) { // Succeeded or did not invalidate tokens emit hideVerificationUriAndCode(); QVariantMap extraTokens = m_oauth2->extraTokens(); -#ifndef NDEBUG if (!extraTokens.isEmpty()) { - qDebug() << "Extra tokens in response:"; + qCDebug(authCredentials()) << "Extra tokens in response:"; foreach (QString key, extraTokens.keys()) { - qDebug() << "\t" << key << ":" << extraTokens.value(key); + qCDebug(authCredentials()) << "\t" << key << ":" << extraTokens.value(key); } } -#endif emit finished(AccountTaskState::STATE_WORKING, tr("Got ")); return; } diff --git a/launcher/minecraft/auth/steps/MinecraftProfileStep.cpp b/launcher/minecraft/auth/steps/MinecraftProfileStep.cpp index b39b9326..6cfa7c1c 100644 --- a/launcher/minecraft/auth/steps/MinecraftProfileStep.cpp +++ b/launcher/minecraft/auth/steps/MinecraftProfileStep.cpp @@ -2,6 +2,7 @@ #include +#include "Logging.h" #include "minecraft/auth/AuthRequest.h" #include "minecraft/auth/Parsers.h" #include "net/NetUtils.h" @@ -40,9 +41,7 @@ void MinecraftProfileStep::onRequestDone( auto requestor = qobject_cast(QObject::sender()); requestor->deleteLater(); -#ifndef NDEBUG - qDebug() << data; -#endif + qCDebug(authCredentials()) << data; if (error == QNetworkReply::ContentNotFoundError) { // NOTE: Succeed even if we do not have a profile. This is a valid account state. if(m_data->type == AccountType::Mojang) { diff --git a/launcher/minecraft/auth/steps/MinecraftProfileStepMojang.cpp b/launcher/minecraft/auth/steps/MinecraftProfileStepMojang.cpp index 6a1eb7a0..8c378588 100644 --- a/launcher/minecraft/auth/steps/MinecraftProfileStepMojang.cpp +++ b/launcher/minecraft/auth/steps/MinecraftProfileStepMojang.cpp @@ -2,6 +2,7 @@ #include +#include "Logging.h" #include "minecraft/auth/AuthRequest.h" #include "minecraft/auth/Parsers.h" #include "net/NetUtils.h" @@ -43,9 +44,7 @@ void MinecraftProfileStepMojang::onRequestDone( auto requestor = qobject_cast(QObject::sender()); requestor->deleteLater(); -#ifndef NDEBUG - qDebug() << data; -#endif + qCDebug(authCredentials()) << data; if (error == QNetworkReply::ContentNotFoundError) { // NOTE: Succeed even if we do not have a profile. This is a valid account state. if(m_data->type == AccountType::Mojang) { diff --git a/launcher/minecraft/auth/steps/XboxAuthorizationStep.cpp b/launcher/minecraft/auth/steps/XboxAuthorizationStep.cpp index 14bde47e..b397b734 100644 --- a/launcher/minecraft/auth/steps/XboxAuthorizationStep.cpp +++ b/launcher/minecraft/auth/steps/XboxAuthorizationStep.cpp @@ -4,6 +4,7 @@ #include #include +#include "Logging.h" #include "minecraft/auth/AuthRequest.h" #include "minecraft/auth/Parsers.h" #include "net/NetUtils.h" @@ -58,9 +59,7 @@ void XboxAuthorizationStep::onRequestDone( auto requestor = qobject_cast(QObject::sender()); requestor->deleteLater(); -#ifndef NDEBUG - qDebug() << data; -#endif + qCDebug(authCredentials()) << data; if (error != QNetworkReply::NoError) { qWarning() << "Reply error:" << error; if (Net::isApplicationError(error)) { diff --git a/launcher/minecraft/auth/steps/XboxProfileStep.cpp b/launcher/minecraft/auth/steps/XboxProfileStep.cpp index 738fe1db..644c419b 100644 --- a/launcher/minecraft/auth/steps/XboxProfileStep.cpp +++ b/launcher/minecraft/auth/steps/XboxProfileStep.cpp @@ -3,7 +3,7 @@ #include #include - +#include "Logging.h" #include "minecraft/auth/AuthRequest.h" #include "minecraft/auth/Parsers.h" #include "net/NetUtils.h" @@ -56,9 +56,7 @@ void XboxProfileStep::onRequestDone( if (error != QNetworkReply::NoError) { qWarning() << "Reply error:" << error; -#ifndef NDEBUG - qDebug() << data; -#endif + qCDebug(authCredentials()) << data; if (Net::isApplicationError(error)) { emit finished( AccountTaskState::STATE_FAILED_SOFT, @@ -74,9 +72,7 @@ void XboxProfileStep::onRequestDone( return; } -#ifndef NDEBUG - qDebug() << "XBox profile: " << data; -#endif + qCDebug(authCredentials()) << "XBox profile: " << data; emit finished(AccountTaskState::STATE_WORKING, tr("Got Xbox profile")); } diff --git a/libraries/katabasis/include/katabasis/DeviceFlow.h b/libraries/katabasis/include/katabasis/DeviceFlow.h index b68c92e0..a5bfbbf3 100644 --- a/libraries/katabasis/include/katabasis/DeviceFlow.h +++ b/libraries/katabasis/include/katabasis/DeviceFlow.h @@ -1,5 +1,6 @@ #pragma once +#include #include #include #include @@ -11,6 +12,8 @@ namespace Katabasis { +Q_DECLARE_LOGGING_CATEGORY(katabasisCredentials) + class ReplyServer; class PollServer; diff --git a/libraries/katabasis/src/DeviceFlow.cpp b/libraries/katabasis/src/DeviceFlow.cpp index f78fd620..17ee379b 100644 --- a/libraries/katabasis/src/DeviceFlow.cpp +++ b/libraries/katabasis/src/DeviceFlow.cpp @@ -22,6 +22,7 @@ #include "JsonResponse.h" namespace { + // ref: https://tools.ietf.org/html/rfc8628#section-3.2 // Exception: Google sign-in uses "verification_url" instead of "*_uri" - we'll accept both. bool hasMandatoryDeviceAuthParams(const QVariantMap& params) @@ -58,6 +59,8 @@ QByteArray createQueryParameters(const QList ¶m namespace Katabasis { +Q_LOGGING_CATEGORY(katabasisCredentials, "katabasis.credentials", QtWarningMsg) + DeviceFlow::DeviceFlow(Options & opts, Token & token, QObject *parent, QNetworkAccessManager *manager) : QObject(parent), token_(token) { manager_ = manager ? manager : new QNetworkAccessManager(this); qRegisterMetaType("QNetworkReply::NetworkError"); @@ -333,9 +336,7 @@ QString DeviceFlow::refreshToken() { } void DeviceFlow::setRefreshToken(const QString &v) { -#ifndef NDEBUG - qDebug() << "DeviceFlow::setRefreshToken" << v << "..."; -#endif + qCDebug(katabasisCredentials) << "new refresh token:" << v; token_.refresh_token = v; } From f33f596584bf88df36175516764d5d7977d98b98 Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Tue, 27 Dec 2022 17:23:44 +0100 Subject: [PATCH 034/152] refactor: use ECM logging categories instead Co-authored-by: flow Signed-off-by: Sefa Eyeoglu --- CMakeLists.txt | 2 ++ launcher/CMakeLists.txt | 13 ++++++++-- launcher/Logging.cpp | 22 ----------------- launcher/Logging.h | 24 ------------------- libraries/katabasis/CMakeLists.txt | 9 +++++++ .../katabasis/include/katabasis/DeviceFlow.h | 2 -- libraries/katabasis/src/DeviceFlow.cpp | 3 +-- 7 files changed, 23 insertions(+), 52 deletions(-) delete mode 100644 launcher/Logging.cpp delete mode 100644 launcher/Logging.h diff --git a/CMakeLists.txt b/CMakeLists.txt index de9b6fe1..c7ba9e9f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -268,6 +268,8 @@ if(NOT Launcher_FORCE_BUNDLED_LIBS) find_package(ghc_filesystem QUIET) endif() +include(ECMQtDeclareLoggingCategory) + ####################################### Program Info ####################################### set(Launcher_APP_BINARY_NAME "prismlauncher" CACHE STRING "Name of the Launcher binary") diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index 21597081..4057c876 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -27,8 +27,6 @@ set(CORE_SOURCES StringUtils.h StringUtils.cpp RuntimeContext.h - Logging.h - Logging.cpp # Basic instance manipulation tasks (derived from InstanceTask) InstanceCreationTask.h @@ -553,6 +551,17 @@ set(ATLAUNCHER_SOURCES modplatform/atlauncher/ATLShareCode.h ) +######## Logging categories ######## + +ecm_qt_declare_logging_category(CORE_SOURCES + HEADER Logging.h + IDENTIFIER authCredentials + CATEGORY_NAME "launcher.auth.credentials" + DEFAULT_SEVERITY Warning + DESCRIPTION "Secrets and credentials for debugging purposes" + EXPORT "${Launcher_Name}" +) + ################################ COMPILE ################################ set(LOGIC_SOURCES diff --git a/launcher/Logging.cpp b/launcher/Logging.cpp deleted file mode 100644 index d0e30473..00000000 --- a/launcher/Logging.cpp +++ /dev/null @@ -1,22 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-only -/* - * PolyMC - 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 . - * - */ - -#include "Logging.h" - -Q_LOGGING_CATEGORY(authCredentials, "launcher.auth.credentials", QtWarningMsg) diff --git a/launcher/Logging.h b/launcher/Logging.h deleted file mode 100644 index 0fcb30b7..00000000 --- a/launcher/Logging.h +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-only -/* - * PolyMC - 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 . - * - */ - -#pragma once - -#include - -Q_DECLARE_LOGGING_CATEGORY(authCredentials) diff --git a/libraries/katabasis/CMakeLists.txt b/libraries/katabasis/CMakeLists.txt index f764feb6..643244ed 100644 --- a/libraries/katabasis/CMakeLists.txt +++ b/libraries/katabasis/CMakeLists.txt @@ -38,6 +38,15 @@ set( katabasis_PUBLIC include/katabasis/RequestParameter.h ) +ecm_qt_declare_logging_category(katabasis_PRIVATE + HEADER KatabasisLogging.h # NOTE: this won't be in src/, but CMAKE_BINARY_DIR/src isn't included by default so this should be fine + IDENTIFIER katabasisCredentials + CATEGORY_NAME "katabasis.credentials" + DEFAULT_SEVERITY Warning + DESCRIPTION "Secrets and credentials from Katabasis" + EXPORT "Katabasis" +) + add_library( Katabasis STATIC ${katabasis_PRIVATE} ${katabasis_PUBLIC} ) target_link_libraries(Katabasis Qt${QT_VERSION_MAJOR}::Core Qt${QT_VERSION_MAJOR}::Network) diff --git a/libraries/katabasis/include/katabasis/DeviceFlow.h b/libraries/katabasis/include/katabasis/DeviceFlow.h index a5bfbbf3..0401df3c 100644 --- a/libraries/katabasis/include/katabasis/DeviceFlow.h +++ b/libraries/katabasis/include/katabasis/DeviceFlow.h @@ -12,8 +12,6 @@ namespace Katabasis { -Q_DECLARE_LOGGING_CATEGORY(katabasisCredentials) - class ReplyServer; class PollServer; diff --git a/libraries/katabasis/src/DeviceFlow.cpp b/libraries/katabasis/src/DeviceFlow.cpp index 17ee379b..f49fcb7d 100644 --- a/libraries/katabasis/src/DeviceFlow.cpp +++ b/libraries/katabasis/src/DeviceFlow.cpp @@ -19,6 +19,7 @@ #include "katabasis/PollServer.h" #include "katabasis/Globals.h" +#include "KatabasisLogging.h" #include "JsonResponse.h" namespace { @@ -59,8 +60,6 @@ QByteArray createQueryParameters(const QList ¶m namespace Katabasis { -Q_LOGGING_CATEGORY(katabasisCredentials, "katabasis.credentials", QtWarningMsg) - DeviceFlow::DeviceFlow(Options & opts, Token & token, QObject *parent, QNetworkAccessManager *manager) : QObject(parent), token_(token) { manager_ = manager ? manager : new QNetworkAccessManager(this); qRegisterMetaType("QNetworkReply::NetworkError"); From 7a651bdc5310dffd19148e5f2046126324e2f53f Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Tue, 27 Dec 2022 17:31:56 +0100 Subject: [PATCH 035/152] feat: install launcher logging categories Signed-off-by: Sefa Eyeoglu --- launcher/CMakeLists.txt | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index 4057c876..6ca88ec6 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -562,6 +562,13 @@ ecm_qt_declare_logging_category(CORE_SOURCES EXPORT "${Launcher_Name}" ) +if(KDE_INSTALL_LOGGINGCATEGORIESDIR) # only install if there is a standard path for this + ecm_qt_install_logging_categories( + EXPORT "${Launcher_Name}" + DESTINATION "${KDE_INSTALL_LOGGINGCATEGORIESDIR}" + ) +endif() + ################################ COMPILE ################################ set(LOGIC_SOURCES From 257970c27d262bd4b4dec4632f6370c5e04bc61b Mon Sep 17 00:00:00 2001 From: flow Date: Thu, 29 Dec 2022 12:39:20 -0300 Subject: [PATCH 036/152] refactor(Mods): make provider() return a std::optional This makes it easier to check if a mod has a provider or not, without having to do a string comparison. Signed-off-by: flow --- launcher/minecraft/mod/Mod.cpp | 13 ++++++------- launcher/minecraft/mod/Mod.h | 4 +++- launcher/minecraft/mod/ModFolderModel.cpp | 11 +++++++++-- 3 files changed, 18 insertions(+), 10 deletions(-) diff --git a/launcher/minecraft/mod/Mod.cpp b/launcher/minecraft/mod/Mod.cpp index 1be8e7e3..9cd0056c 100644 --- a/launcher/minecraft/mod/Mod.cpp +++ b/launcher/minecraft/mod/Mod.cpp @@ -93,10 +93,11 @@ std::pair Mod::compare(const Resource& other, SortType type) const if (this_ver < other_ver) return { -1, type == SortType::VERSION }; } - case SortType::PROVIDER: - auto compare_result = QString::compare(provider(), cast_other->provider(), Qt::CaseInsensitive); + 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 { 0, false }; } @@ -197,11 +198,9 @@ void Mod::finishResolvingWithDetails(ModDetails&& details) setMetadata(std::move(metadata)); }; -auto Mod::provider() const -> QString +auto Mod::provider() const -> std::optional { - if (metadata()) { + if (metadata()) return ProviderCaps.readableName(metadata()->provider); - } - //: Unknown mod provider (i.e. not Modrinth, CurseForge, etc...) - return tr("Unknown"); + return {}; } diff --git a/launcher/minecraft/mod/Mod.h b/launcher/minecraft/mod/Mod.h index 16d2bb32..8185c8fc 100644 --- a/launcher/minecraft/mod/Mod.h +++ b/launcher/minecraft/mod/Mod.h @@ -39,6 +39,8 @@ #include #include +#include + #include "Resource.h" #include "ModDetails.h" @@ -61,7 +63,7 @@ public: auto description() const -> QString; auto authors() const -> QStringList; auto status() const -> ModStatus; - auto provider() const -> QString; + auto provider() const -> std::optional; auto metadata() -> std::shared_ptr; auto metadata() const -> const std::shared_ptr; diff --git a/launcher/minecraft/mod/ModFolderModel.cpp b/launcher/minecraft/mod/ModFolderModel.cpp index 5aadc2f1..f258ad69 100644 --- a/launcher/minecraft/mod/ModFolderModel.cpp +++ b/launcher/minecraft/mod/ModFolderModel.cpp @@ -83,8 +83,15 @@ QVariant ModFolderModel::data(const QModelIndex &index, int role) const } case DateColumn: return m_resources[row]->dateTimeChanged(); - case ProviderColumn: - return at(row)->provider(); + case ProviderColumn: { + auto provider = at(row)->provider(); + if (!provider.has_value()) { + //: Unknown mod provider (i.e. not Modrinth, CurseForge, etc...) + return tr("Unknown"); + } + + return provider.value(); + } default: return QVariant(); } From 141e94369ed88e7099b46b45a0ed3683cada6329 Mon Sep 17 00:00:00 2001 From: flow Date: Thu, 29 Dec 2022 13:04:38 -0300 Subject: [PATCH 037/152] feat(Mods): hide 'Provider' column when no mods have providers This makes the mod list look a bit less polluted in the common case of mods having no provider whatsoever. Signed-off-by: flow --- launcher/ui/widgets/ModListView.cpp | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/launcher/ui/widgets/ModListView.cpp b/launcher/ui/widgets/ModListView.cpp index c8ccd292..40d23f45 100644 --- a/launcher/ui/widgets/ModListView.cpp +++ b/launcher/ui/widgets/ModListView.cpp @@ -14,6 +14,9 @@ */ #include "ModListView.h" + +#include "minecraft/mod/ModFolderModel.h" + #include #include #include @@ -63,4 +66,17 @@ void ModListView::setModel ( QAbstractItemModel* model ) for(int i = 1; i < head->count(); i++) head->setSectionResizeMode(i, QHeaderView::ResizeToContents); } + + auto real_model = model; + if (auto proxy_model = dynamic_cast(model); proxy_model) + real_model = proxy_model->sourceModel(); + + if (auto mod_model = dynamic_cast(real_model); mod_model) { + connect(mod_model, &ModFolderModel::updateFinished, this, [this, mod_model]{ + auto mods = mod_model->allMods(); + // Hide the 'Provider' column if no mod has a defined provider! + setColumnHidden(ModFolderModel::Columns::ProviderColumn, + std::none_of(mods.constBegin(), mods.constEnd(), [](auto const mod){ return mod->provider().has_value(); })); + }); + } } From c470f05abf090232b27faac6014f9e1cbe9dab9b Mon Sep 17 00:00:00 2001 From: Rachel Powers <508861+Ryex@users.noreply.github.com> Date: Thu, 29 Dec 2022 17:21:54 -0700 Subject: [PATCH 038/152] refactor: use std::filesystem::rename insted of copy and then moving. Signed-off-by: Rachel Powers <508861+Ryex@users.noreply.github.com> --- launcher/FileSystem.cpp | 16 ++++++++++++++++ launcher/FileSystem.h | 8 ++++++++ .../flame/FlameInstanceCreationTask.cpp | 15 +-------------- 3 files changed, 25 insertions(+), 14 deletions(-) diff --git a/launcher/FileSystem.cpp b/launcher/FileSystem.cpp index 3e8e10a5..4390eed9 100644 --- a/launcher/FileSystem.cpp +++ b/launcher/FileSystem.cpp @@ -213,6 +213,22 @@ bool copy::operator()(const QString& offset, bool dryRun) return err.value() == 0; } +bool move(const QString& source, const QString& dest) +{ + std::error_code err; + + ensureFilePathExists(dest); + fs::rename(StringUtils::toStdString(source), StringUtils::toStdString(dest), err); + + if (err) { + qWarning() << "Failed to move file:" << QString::fromStdString(err.message()); + qDebug() << "Source file:" << source; + qDebug() << "Destination file:" << dest; + } + + return err.value() == 0; +} + bool deletePath(QString path) { std::error_code err; diff --git a/launcher/FileSystem.h b/launcher/FileSystem.h index ac893725..1e3a60d9 100644 --- a/launcher/FileSystem.h +++ b/launcher/FileSystem.h @@ -121,6 +121,14 @@ class copy : public QObject { int m_copied; }; +/** + * @brief moves a file by renaming it + * @param source source file path + * @param dest destination filepath + * + */ +bool move(const QString& source, const QString& dest); + /** * Delete a folder recursively */ diff --git a/launcher/modplatform/flame/FlameInstanceCreationTask.cpp b/launcher/modplatform/flame/FlameInstanceCreationTask.cpp index 79104e17..0a91879d 100644 --- a/launcher/modplatform/flame/FlameInstanceCreationTask.cpp +++ b/launcher/modplatform/flame/FlameInstanceCreationTask.cpp @@ -539,19 +539,6 @@ void FlameCreationTask::copyBlockedMods(QList const& blocked_mods) setAbortable(true); } -bool moveFile(QString src, QString dst) -{ - if (!FS::copy(src, dst)()) { // copy - qDebug() << "Copy of" << src << "to" << dst << "failed!"; - return false; - } else { - if (!FS::deletePath(src)) { // remove original - qDebug() << "Deletion of" << src << "failed!"; - return false; - }; - } - return true; -} void FlameCreationTask::validateZIPResouces() { @@ -567,7 +554,7 @@ void FlameCreationTask::validateZIPResouces() qDebug() << "Target folder of" << fileName << "is incorrect, it belongs in" << realTarget; auto destPath = FS::PathCombine(m_stagingPath, "minecraft", realTarget, fileName); qDebug() << "Moving" << localPath << "to" << destPath; - if (moveFile(localPath, destPath)) { + if (FS::move(localPath, destPath)) { return destPath; } } From 7f438425aa84db51211123b47622a828be0aeb96 Mon Sep 17 00:00:00 2001 From: Rachel Powers <508861+Ryex@users.noreply.github.com> Date: Thu, 29 Dec 2022 19:47:19 -0700 Subject: [PATCH 039/152] refactor: add an `identify` function to make easy to reuse Signed-off-by: Rachel Powers <508861+Ryex@users.noreply.github.com> --- launcher/CMakeLists.txt | 2 + .../mod/tasks/LocalResourceParse.cpp | 60 +++++++++++++++ .../minecraft/mod/tasks/LocalResourceParse.h | 31 ++++++++ .../flame/FlameInstanceCreationTask.cpp | 76 ++++++++++--------- 4 files changed, 132 insertions(+), 37 deletions(-) create mode 100644 launcher/minecraft/mod/tasks/LocalResourceParse.cpp create mode 100644 launcher/minecraft/mod/tasks/LocalResourceParse.h diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index 853e1c03..9826d543 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -363,6 +363,8 @@ set(MINECRAFT_SOURCES minecraft/mod/tasks/LocalShaderPackParseTask.cpp minecraft/mod/tasks/LocalWorldSaveParseTask.h minecraft/mod/tasks/LocalWorldSaveParseTask.cpp + minecraft/mod/tasks/LocalResourceParse.h + minecraft/mod/tasks/LocalResourceParse.cpp # Assets minecraft/AssetsUtils.h diff --git a/launcher/minecraft/mod/tasks/LocalResourceParse.cpp b/launcher/minecraft/mod/tasks/LocalResourceParse.cpp new file mode 100644 index 00000000..244b2f54 --- /dev/null +++ b/launcher/minecraft/mod/tasks/LocalResourceParse.cpp @@ -0,0 +1,60 @@ +// SPDX-FileCopyrightText: 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> +// +// SPDX-License-Identifier: GPL-3.0-only + +/* + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "LocalResourceParse.h" + +#include "LocalDataPackParseTask.h" +#include "LocalModParseTask.h" +#include "LocalResourcePackParseTask.h" +#include "LocalShaderPackParseTask.h" +#include "LocalTexturePackParseTask.h" +#include "LocalWorldSaveParseTask.h" + +namespace ResourceUtils { +PackedResourceType identify(QFileInfo file){ + if (file.exists() && file.isFile()) { + if (ResourcePackUtils::validate(file)) { + qDebug() << file.fileName() << "is a resource pack"; + return PackedResourceType::ResourcePack; + } else if (TexturePackUtils::validate(file)) { + qDebug() << file.fileName() << "is a pre 1.6 texture pack"; + return PackedResourceType::TexturePack; + } else if (DataPackUtils::validate(file)) { + qDebug() << file.fileName() << "is a data pack"; + return PackedResourceType::DataPack; + } else if (ModUtils::validate(file)) { + qDebug() << file.fileName() << "is a mod"; + return PackedResourceType::Mod; + } else if (WorldSaveUtils::validate(file)) { + qDebug() << file.fileName() << "is a world save"; + return PackedResourceType::WorldSave; + } else if (ShaderPackUtils::validate(file)) { + qDebug() << file.fileName() << "is a shader pack"; + return PackedResourceType::ShaderPack; + } else { + qDebug() << "Can't Identify" << file.fileName() ; + } + } else { + qDebug() << "Can't find" << file.absolutePath(); + } + return PackedResourceType::UNKNOWN; +} +} \ No newline at end of file diff --git a/launcher/minecraft/mod/tasks/LocalResourceParse.h b/launcher/minecraft/mod/tasks/LocalResourceParse.h new file mode 100644 index 00000000..b3e2829d --- /dev/null +++ b/launcher/minecraft/mod/tasks/LocalResourceParse.h @@ -0,0 +1,31 @@ +// SPDX-FileCopyrightText: 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> +// +// SPDX-License-Identifier: GPL-3.0-only + +/* + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include +#include +#include + +enum class PackedResourceType { DataPack, ResourcePack, TexturePack, ShaderPack, WorldSave, Mod, UNKNOWN }; +namespace ResourceUtils { +PackedResourceType identify(QFileInfo file); +} // namespace ResourceUtils \ No newline at end of file diff --git a/launcher/modplatform/flame/FlameInstanceCreationTask.cpp b/launcher/modplatform/flame/FlameInstanceCreationTask.cpp index 0a91879d..dc69769a 100644 --- a/launcher/modplatform/flame/FlameInstanceCreationTask.cpp +++ b/launcher/modplatform/flame/FlameInstanceCreationTask.cpp @@ -57,12 +57,8 @@ #include #include "minecraft/World.h" -#include "minecraft/mod/tasks/LocalDataPackParseTask.h" -#include "minecraft/mod/tasks/LocalModParseTask.h" -#include "minecraft/mod/tasks/LocalResourcePackParseTask.h" -#include "minecraft/mod/tasks/LocalShaderPackParseTask.h" -#include "minecraft/mod/tasks/LocalTexturePackParseTask.h" -#include "minecraft/mod/tasks/LocalWorldSaveParseTask.h" +#include "minecraft/mod/tasks/LocalResourceParse.h" + const static QMap forgemap = { { "1.2.5", "3.4.9.171" }, { "1.4.2", "6.0.1.355" }, @@ -561,42 +557,48 @@ void FlameCreationTask::validateZIPResouces() return localPath; }; - QFileInfo localFileInfo(localPath); - if (localFileInfo.exists() && localFileInfo.isFile()) { - if (ResourcePackUtils::validate(localFileInfo)) { - qDebug() << fileName << "is a resource pack"; - validatePath(fileName, targetFolder, "resourcepacks"); - } else if (TexturePackUtils::validate(localFileInfo)) { - qDebug() << fileName << "is a pre 1.6 texture pack"; - validatePath(fileName, targetFolder, "texturepacks"); - } else if (DataPackUtils::validate(localFileInfo)) { - qDebug() << fileName << "is a data pack"; - validatePath(fileName, targetFolder, "datapacks"); - } else if (ModUtils::validate(localFileInfo)) { - qDebug() << fileName << "is a mod"; - validatePath(fileName, targetFolder, "mods"); - } else if (WorldSaveUtils::validate(localFileInfo)) { - qDebug() << fileName << "is a world save"; - QString worldPath = validatePath(fileName, targetFolder, "saves"); + auto installWorld = [this](QString worldPath){ + qDebug() << "Installing World from" << worldPath; + QFileInfo worldFileInfo(worldPath); + World w(worldFileInfo); + if (!w.isValid()) { + qDebug() << "World at" << worldPath << "is not valid, skipping install."; + } else { + w.install(FS::PathCombine(m_stagingPath, "minecraft", "saves")); + } + }; - qDebug() << "Installing World from" << worldPath; - QFileInfo worldFileInfo(worldPath); - World w(worldFileInfo); - if (!w.isValid()) { - qDebug() << "World at" << worldPath << "is not valid, skipping install."; - } else { - w.install(FS::PathCombine(m_stagingPath, "minecraft", "saves")); - } - } else if (ShaderPackUtils::validate(localFileInfo)) { + QFileInfo localFileInfo(localPath); + auto type = ResourceUtils::identify(localFileInfo); + + QString worldPath; + + switch (type) { + case PackedResourceType::ResourcePack : + validatePath(fileName, targetFolder, "resourcepacks"); + break; + case PackedResourceType::TexturePack : + validatePath(fileName, targetFolder, "texturepacks"); + break; + case PackedResourceType::DataPack : + validatePath(fileName, targetFolder, "datapacks"); + break; + case PackedResourceType::Mod : + validatePath(fileName, targetFolder, "mods"); + break; + case PackedResourceType::ShaderPack : // in theroy flame API can't do this but who knows, that *may* change ? // better to handle it if it *does* occure in the future - qDebug() << fileName << "is a shader pack"; validatePath(fileName, targetFolder, "shaderpacks"); - } else { + break; + case PackedResourceType::WorldSave : + worldPath = validatePath(fileName, targetFolder, "saves"); + installWorld(worldPath); + break; + case PackedResourceType::UNKNOWN : + default : qDebug() << "Can't Identify" << fileName << "at" << localPath << ", leaving it where it is."; - } - } else { - qDebug() << "Can't find" << localPath << "to validate it, ignoring"; + break; } } } From 11df4845b7ce916cf2e34bf2fa3afbbd61735999 Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Fri, 30 Dec 2022 15:36:35 +0100 Subject: [PATCH 040/152] fix: remove Flatpak cache key workaround Signed-off-by: Sefa Eyeoglu --- .github/workflows/build.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index dd27ba30..9f286014 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -550,7 +550,6 @@ jobs: with: bundle: "Prism Launcher.flatpak" manifest-path: flatpak/org.prismlauncher.PrismLauncher.yml - cache-key: flatpak-${{ github.sha }}-x86_64 nix: runs-on: ubuntu-latest From 0ebf04a021c633cd6a3cdd76514aa728dc253714 Mon Sep 17 00:00:00 2001 From: Rachel Powers <508861+Ryex@users.noreply.github.com> Date: Fri, 30 Dec 2022 10:21:49 -0700 Subject: [PATCH 041/152] fix newlines Co-authored-by: flow Signed-off-by: Rachel Powers <508861+Ryex@users.noreply.github.com> --- launcher/minecraft/mod/tasks/LocalResourceParse.cpp | 2 +- launcher/minecraft/mod/tasks/LocalResourceParse.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/launcher/minecraft/mod/tasks/LocalResourceParse.cpp b/launcher/minecraft/mod/tasks/LocalResourceParse.cpp index 244b2f54..19ddc899 100644 --- a/launcher/minecraft/mod/tasks/LocalResourceParse.cpp +++ b/launcher/minecraft/mod/tasks/LocalResourceParse.cpp @@ -57,4 +57,4 @@ PackedResourceType identify(QFileInfo file){ } return PackedResourceType::UNKNOWN; } -} \ No newline at end of file +} diff --git a/launcher/minecraft/mod/tasks/LocalResourceParse.h b/launcher/minecraft/mod/tasks/LocalResourceParse.h index b3e2829d..b07a874c 100644 --- a/launcher/minecraft/mod/tasks/LocalResourceParse.h +++ b/launcher/minecraft/mod/tasks/LocalResourceParse.h @@ -28,4 +28,4 @@ enum class PackedResourceType { DataPack, ResourcePack, TexturePack, ShaderPack, WorldSave, Mod, UNKNOWN }; namespace ResourceUtils { PackedResourceType identify(QFileInfo file); -} // namespace ResourceUtils \ No newline at end of file +} // namespace ResourceUtils From 7e2d78bab555ac17ff79711d41d59b14b226998f Mon Sep 17 00:00:00 2001 From: Aaron <10217842+byteduck@users.noreply.github.com> Date: Tue, 27 Dec 2022 15:00:15 -0700 Subject: [PATCH 042/152] Allow selecting a default account to use with an instance Signed-off-by: Aaron <10217842+byteduck@users.noreply.github.com> --- launcher/LaunchController.cpp | 14 +++- launcher/minecraft/MinecraftInstance.cpp | 4 + .../pages/instance/InstanceSettingsPage.cpp | 83 ++++++++++++++++++- .../ui/pages/instance/InstanceSettingsPage.h | 13 ++- .../ui/pages/instance/InstanceSettingsPage.ui | 42 ++++++++++ 5 files changed, 150 insertions(+), 6 deletions(-) diff --git a/launcher/LaunchController.cpp b/launcher/LaunchController.cpp index 11e3de15..5dd551ee 100644 --- a/launcher/LaunchController.cpp +++ b/launcher/LaunchController.cpp @@ -112,7 +112,19 @@ void LaunchController::decideAccount() } } - m_accountToUse = accounts->defaultAccount(); + // Select the account to use. If the instance has a specific account set, that will be used. Otherwise, the default account will be used + auto instanceAccountId = m_instance->settings()->get("InstanceAccountId").toString(); + auto instanceAccountIndex = accounts->findAccountByProfileId(instanceAccountId); + if (instanceAccountIndex == -1) + { + m_accountToUse = accounts->defaultAccount(); + } + else + { + m_accountToUse = accounts->at(instanceAccountIndex); + } + + if (!m_accountToUse) { // If no default account is set, ask the user which one to use. diff --git a/launcher/minecraft/MinecraftInstance.cpp b/launcher/minecraft/MinecraftInstance.cpp index 1d37224a..d0a5ed31 100644 --- a/launcher/minecraft/MinecraftInstance.cpp +++ b/launcher/minecraft/MinecraftInstance.cpp @@ -192,6 +192,10 @@ void MinecraftInstance::loadSpecificSettings() m_settings->registerSetting("JoinServerOnLaunch", false); m_settings->registerSetting("JoinServerOnLaunchAddress", ""); + // Use account for instance, this does not have a global override + m_settings->registerSetting("UseAccountForInstance", false); + m_settings->registerSetting("InstanceAccountId", ""); + qDebug() << "Instance-type specific settings were loaded!"; setSpecificSettingsLoaded(true); diff --git a/launcher/ui/pages/instance/InstanceSettingsPage.cpp b/launcher/ui/pages/instance/InstanceSettingsPage.cpp index af2ba7c8..18f5f2ac 100644 --- a/launcher/ui/pages/instance/InstanceSettingsPage.cpp +++ b/launcher/ui/pages/instance/InstanceSettingsPage.cpp @@ -48,18 +48,23 @@ #include "JavaCommon.h" #include "Application.h" +#include "minecraft/auth/AccountList.h" #include "java/JavaInstallList.h" #include "java/JavaUtils.h" #include "FileSystem.h" - InstanceSettingsPage::InstanceSettingsPage(BaseInstance *inst, QWidget *parent) : QWidget(parent), ui(new Ui::InstanceSettingsPage), m_instance(inst) { m_settings = inst->settings(); ui->setupUi(this); + accountMenu = new QMenu(this); + // Use undocumented property... https://stackoverflow.com/questions/7121718/create-a-scrollbar-in-a-submenu-qt + accountMenu->setStyleSheet("QMenu { menu-scrollable: 1; }"); + ui->instanceAccountSelector->setMenu(accountMenu); + connect(ui->openGlobalJavaSettingsButton, &QCommandLinkButton::clicked, this, &InstanceSettingsPage::globalSettingsButtonClicked); connect(APPLICATION, &Application::globalSettingsAboutToOpen, this, &InstanceSettingsPage::applySettings); connect(APPLICATION, &Application::globalSettingsClosed, this, &InstanceSettingsPage::loadSettings); @@ -75,6 +80,7 @@ bool InstanceSettingsPage::shouldDisplay() const InstanceSettingsPage::~InstanceSettingsPage() { delete ui; + delete accountMenu; } void InstanceSettingsPage::globalSettingsButtonClicked(bool) @@ -275,6 +281,14 @@ void InstanceSettingsPage::applySettings() m_settings->reset("JoinServerOnLaunchAddress"); } + // Use an account for this instance + bool useAccountForInstance = ui->instanceAccountGroupBox->isChecked(); + m_settings->set("UseAccountForInstance", useAccountForInstance); + if (!useAccountForInstance) + { + m_settings->reset("InstanceAccountId"); + } + // FIXME: This should probably be called by a signal instead m_instance->updateRuntimeContext(); } @@ -372,6 +386,9 @@ void InstanceSettingsPage::loadSettings() ui->serverJoinGroupBox->setChecked(m_settings->get("JoinServerOnLaunch").toBool()); ui->serverJoinAddress->setText(m_settings->get("JoinServerOnLaunchAddress").toString()); + + ui->instanceAccountGroupBox->setChecked(m_settings->get("UseAccountForInstance").toBool()); + updateAccountsMenu(); } void InstanceSettingsPage::on_javaDetectBtn_clicked() @@ -437,6 +454,70 @@ void InstanceSettingsPage::on_javaTestBtn_clicked() checker->run(); } +void InstanceSettingsPage::updateAccountsMenu() +{ + accountMenu->clear(); + + auto accounts = APPLICATION->accounts(); + int accountIndex = accounts->findAccountByProfileId(m_settings->get("InstanceAccountId").toString()); + + if (accountIndex != -1) + { + auto account = accounts->at(accountIndex); + ui->instanceAccountSelector->setText(account->profileName()); + ui->instanceAccountSelector->setIcon(account->getFace()); + } else { + ui->instanceAccountSelector->setText(tr("No default account")); + ui->instanceAccountSelector->setIcon(APPLICATION->getThemedIcon("noaccount")); + } + + for (int i = 0; i < accounts->count(); i++) + { + MinecraftAccountPtr account = accounts->at(i); + QAction *action = new QAction(account->profileName(), this); + action->setData(i); + action->setCheckable(true); + if (accountIndex == i) + { + action->setChecked(true); + } + + auto face = account->getFace(); + if(!face.isNull()) { + action->setIcon(face); + } + else { + action->setIcon(APPLICATION->getThemedIcon("noaccount")); + } + + accountMenu->addAction(action); + connect(action, SIGNAL(triggered(bool)), SLOT(changeInstanceAccount())); + } +} + +void InstanceSettingsPage::changeInstanceAccount() +{ + QAction *sAction = (QAction *)sender(); + + // Profile's associated Mojang username + if (sAction->data().type() != QVariant::Type::Int) + return; + + QVariant data = sAction->data(); + bool valid = false; + int index = data.toInt(&valid); + if(!valid) { + index = -1; + } + auto accounts = APPLICATION->accounts(); + auto account = accounts->at(index); + + m_settings->set("InstanceAccountId", account->profileId()); + + ui->instanceAccountSelector->setText(account->profileName()); + ui->instanceAccountSelector->setIcon(account->getFace()); +} + void InstanceSettingsPage::on_maxMemSpinBox_valueChanged(int i) { updateThresholds(); diff --git a/launcher/ui/pages/instance/InstanceSettingsPage.h b/launcher/ui/pages/instance/InstanceSettingsPage.h index 7450188d..b80db99a 100644 --- a/launcher/ui/pages/instance/InstanceSettingsPage.h +++ b/launcher/ui/pages/instance/InstanceSettingsPage.h @@ -37,12 +37,13 @@ #include -#include "java/JavaChecker.h" -#include "BaseInstance.h" #include -#include "ui/pages/BasePage.h" -#include "JavaCommon.h" +#include #include "Application.h" +#include "BaseInstance.h" +#include "JavaCommon.h" +#include "java/JavaChecker.h" +#include "ui/pages/BasePage.h" class JavaChecker; namespace Ui @@ -92,9 +93,13 @@ private slots: void globalSettingsButtonClicked(bool checked); + void updateAccountsMenu(); + void changeInstanceAccount(); + private: Ui::InstanceSettingsPage *ui; BaseInstance *m_instance; SettingsObjectPtr m_settings; unique_qobject_ptr checker; + QMenu *accountMenu = nullptr; }; diff --git a/launcher/ui/pages/instance/InstanceSettingsPage.ui b/launcher/ui/pages/instance/InstanceSettingsPage.ui index b064367d..ef86ed7e 100644 --- a/launcher/ui/pages/instance/InstanceSettingsPage.ui +++ b/launcher/ui/pages/instance/InstanceSettingsPage.ui @@ -608,6 +608,48 @@ + + + + Set a default account to use with this instance + + + true + + + false + + + + + + + + + 0 + 0 + + + + Account: + + + + + + + QToolButton::InstantPopup + + + Qt::ToolButtonTextBesideIcon + + + + + + + + From cba3d68063bea28acb1eae870aee7e0b2a57b2be Mon Sep 17 00:00:00 2001 From: Aaron <10217842+byteduck@users.noreply.github.com> Date: Tue, 27 Dec 2022 15:39:35 -0700 Subject: [PATCH 043/152] Fix conflicting layout name in InstanceSettingsPage Signed-off-by: Aaron <10217842+byteduck@users.noreply.github.com> --- launcher/ui/pages/instance/InstanceSettingsPage.ui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/launcher/ui/pages/instance/InstanceSettingsPage.ui b/launcher/ui/pages/instance/InstanceSettingsPage.ui index ef86ed7e..a88fdb54 100644 --- a/launcher/ui/pages/instance/InstanceSettingsPage.ui +++ b/launcher/ui/pages/instance/InstanceSettingsPage.ui @@ -619,7 +619,7 @@ false - + From 021e6c02d781706da82ca8ee5c77f716b5c210b9 Mon Sep 17 00:00:00 2001 From: Aaron Sonin <10217842+byteduck@users.noreply.github.com> Date: Mon, 2 Jan 2023 10:49:16 -0700 Subject: [PATCH 044/152] Replace unecessary type check with assertion in InstanceSettingsPage Co-authored-by: flow Signed-off-by: Aaron Sonin <10217842+byteduck@users.noreply.github.com> Signed-off-by: Aaron <10217842+byteduck@users.noreply.github.com> --- launcher/ui/pages/instance/InstanceSettingsPage.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/launcher/ui/pages/instance/InstanceSettingsPage.cpp b/launcher/ui/pages/instance/InstanceSettingsPage.cpp index 18f5f2ac..1c3989f6 100644 --- a/launcher/ui/pages/instance/InstanceSettingsPage.cpp +++ b/launcher/ui/pages/instance/InstanceSettingsPage.cpp @@ -500,8 +500,7 @@ void InstanceSettingsPage::changeInstanceAccount() QAction *sAction = (QAction *)sender(); // Profile's associated Mojang username - if (sAction->data().type() != QVariant::Type::Int) - return; + Q_ASSERT(sAction->data().type() == QVariant::Type::Int); QVariant data = sAction->data(); bool valid = false; From e18652387835e61d6b006d88fe856c63e24a098f Mon Sep 17 00:00:00 2001 From: Aaron Sonin <10217842+byteduck@users.noreply.github.com> Date: Mon, 2 Jan 2023 10:50:24 -0700 Subject: [PATCH 045/152] Add null check for face in instance account settings selector Co-authored-by: flow Signed-off-by: Aaron Sonin <10217842+byteduck@users.noreply.github.com> Signed-off-by: Aaron <10217842+byteduck@users.noreply.github.com> --- launcher/ui/pages/instance/InstanceSettingsPage.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/launcher/ui/pages/instance/InstanceSettingsPage.cpp b/launcher/ui/pages/instance/InstanceSettingsPage.cpp index 1c3989f6..a870c01b 100644 --- a/launcher/ui/pages/instance/InstanceSettingsPage.cpp +++ b/launcher/ui/pages/instance/InstanceSettingsPage.cpp @@ -514,7 +514,11 @@ void InstanceSettingsPage::changeInstanceAccount() m_settings->set("InstanceAccountId", account->profileId()); ui->instanceAccountSelector->setText(account->profileName()); - ui->instanceAccountSelector->setIcon(account->getFace()); + if (auto face = account->getFace(); !face.isNull()) { + ui->instanceAccountSelector->setIcon(face); + } else { + ui->instanceAccountSelector->setIcon(APPLICATION->getThemedIcon("noaccount")); + } } void InstanceSettingsPage::on_maxMemSpinBox_valueChanged(int i) From 9b8add196123f13b18b6a8c878da95921103b6f7 Mon Sep 17 00:00:00 2001 From: Aaron Sonin <10217842+byteduck@users.noreply.github.com> Date: Mon, 2 Jan 2023 10:50:59 -0700 Subject: [PATCH 046/152] Properly connect signal in instance settings for account selector Co-authored-by: flow Signed-off-by: Aaron Sonin <10217842+byteduck@users.noreply.github.com> Signed-off-by: Aaron <10217842+byteduck@users.noreply.github.com> --- launcher/ui/pages/instance/InstanceSettingsPage.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/launcher/ui/pages/instance/InstanceSettingsPage.cpp b/launcher/ui/pages/instance/InstanceSettingsPage.cpp index a870c01b..6a823d5f 100644 --- a/launcher/ui/pages/instance/InstanceSettingsPage.cpp +++ b/launcher/ui/pages/instance/InstanceSettingsPage.cpp @@ -491,7 +491,7 @@ void InstanceSettingsPage::updateAccountsMenu() } accountMenu->addAction(action); - connect(action, SIGNAL(triggered(bool)), SLOT(changeInstanceAccount())); + connect(action, SIGNAL(triggered(bool)), this, SLOT(changeInstanceAccount())); } } From eefb259ddff3de641457b6312bc125796e7661c9 Mon Sep 17 00:00:00 2001 From: Aaron Sonin <10217842+byteduck@users.noreply.github.com> Date: Mon, 2 Jan 2023 10:51:17 -0700 Subject: [PATCH 047/152] Remove unecessary delete in InstanceSettingsPage destructor Co-authored-by: flow Signed-off-by: Aaron Sonin <10217842+byteduck@users.noreply.github.com> Signed-off-by: Aaron <10217842+byteduck@users.noreply.github.com> --- launcher/ui/pages/instance/InstanceSettingsPage.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/launcher/ui/pages/instance/InstanceSettingsPage.cpp b/launcher/ui/pages/instance/InstanceSettingsPage.cpp index 6a823d5f..4b7c0f83 100644 --- a/launcher/ui/pages/instance/InstanceSettingsPage.cpp +++ b/launcher/ui/pages/instance/InstanceSettingsPage.cpp @@ -80,7 +80,6 @@ bool InstanceSettingsPage::shouldDisplay() const InstanceSettingsPage::~InstanceSettingsPage() { delete ui; - delete accountMenu; } void InstanceSettingsPage::globalSettingsButtonClicked(bool) From ba81ad1ac3cff48b973ee167802a5d6398eac990 Mon Sep 17 00:00:00 2001 From: Aaron <10217842+byteduck@users.noreply.github.com> Date: Mon, 2 Jan 2023 11:16:09 -0700 Subject: [PATCH 048/152] Reword instance-specific account settings, apply clang-format Signed-off-by: Aaron <10217842+byteduck@users.noreply.github.com> --- launcher/LaunchController.cpp | 8 +--- .../pages/instance/InstanceSettingsPage.cpp | 39 ++++++++----------- .../ui/pages/instance/InstanceSettingsPage.ui | 2 +- 3 files changed, 19 insertions(+), 30 deletions(-) diff --git a/launcher/LaunchController.cpp b/launcher/LaunchController.cpp index 5dd551ee..9741fd95 100644 --- a/launcher/LaunchController.cpp +++ b/launcher/LaunchController.cpp @@ -115,16 +115,12 @@ void LaunchController::decideAccount() // Select the account to use. If the instance has a specific account set, that will be used. Otherwise, the default account will be used auto instanceAccountId = m_instance->settings()->get("InstanceAccountId").toString(); auto instanceAccountIndex = accounts->findAccountByProfileId(instanceAccountId); - if (instanceAccountIndex == -1) - { + if (instanceAccountIndex == -1) { m_accountToUse = accounts->defaultAccount(); - } - else - { + } else { m_accountToUse = accounts->at(instanceAccountIndex); } - if (!m_accountToUse) { // If no default account is set, ask the user which one to use. diff --git a/launcher/ui/pages/instance/InstanceSettingsPage.cpp b/launcher/ui/pages/instance/InstanceSettingsPage.cpp index 4b7c0f83..24b261ba 100644 --- a/launcher/ui/pages/instance/InstanceSettingsPage.cpp +++ b/launcher/ui/pages/instance/InstanceSettingsPage.cpp @@ -283,8 +283,7 @@ void InstanceSettingsPage::applySettings() // Use an account for this instance bool useAccountForInstance = ui->instanceAccountGroupBox->isChecked(); m_settings->set("UseAccountForInstance", useAccountForInstance); - if (!useAccountForInstance) - { + if (!useAccountForInstance) { m_settings->reset("InstanceAccountId"); } @@ -459,33 +458,33 @@ void InstanceSettingsPage::updateAccountsMenu() auto accounts = APPLICATION->accounts(); int accountIndex = accounts->findAccountByProfileId(m_settings->get("InstanceAccountId").toString()); + MinecraftAccountPtr defaultAccount = accounts->defaultAccount(); - if (accountIndex != -1) - { - auto account = accounts->at(accountIndex); - ui->instanceAccountSelector->setText(account->profileName()); - ui->instanceAccountSelector->setIcon(account->getFace()); + if (accountIndex != -1 && accounts->at(accountIndex)) { + defaultAccount = accounts->at(accountIndex); + } + + if (defaultAccount) { + ui->instanceAccountSelector->setText(defaultAccount->profileName()); + ui->instanceAccountSelector->setIcon(defaultAccount->getFace()); } else { ui->instanceAccountSelector->setText(tr("No default account")); ui->instanceAccountSelector->setIcon(APPLICATION->getThemedIcon("noaccount")); } - for (int i = 0; i < accounts->count(); i++) - { + for (int i = 0; i < accounts->count(); i++) { MinecraftAccountPtr account = accounts->at(i); - QAction *action = new QAction(account->profileName(), this); + QAction* action = new QAction(account->profileName(), this); action->setData(i); action->setCheckable(true); - if (accountIndex == i) - { + if (accountIndex == i) { action->setChecked(true); } auto face = account->getFace(); - if(!face.isNull()) { + if (!face.isNull()) { action->setIcon(face); - } - else { + } else { action->setIcon(APPLICATION->getThemedIcon("noaccount")); } @@ -496,20 +495,14 @@ void InstanceSettingsPage::updateAccountsMenu() void InstanceSettingsPage::changeInstanceAccount() { - QAction *sAction = (QAction *)sender(); + QAction* sAction = (QAction*)sender(); - // Profile's associated Mojang username Q_ASSERT(sAction->data().type() == QVariant::Type::Int); QVariant data = sAction->data(); - bool valid = false; - int index = data.toInt(&valid); - if(!valid) { - index = -1; - } + int index = data.toInt(); auto accounts = APPLICATION->accounts(); auto account = accounts->at(index); - m_settings->set("InstanceAccountId", account->profileId()); ui->instanceAccountSelector->setText(account->profileName()); diff --git a/launcher/ui/pages/instance/InstanceSettingsPage.ui b/launcher/ui/pages/instance/InstanceSettingsPage.ui index a88fdb54..1b986184 100644 --- a/launcher/ui/pages/instance/InstanceSettingsPage.ui +++ b/launcher/ui/pages/instance/InstanceSettingsPage.ui @@ -611,7 +611,7 @@ - Set a default account to use with this instance + Override default account true From 2faf8332ee8523a5c097c744dc8e12e86d304230 Mon Sep 17 00:00:00 2001 From: byquanton <32410361+byquanton@users.noreply.github.com> Date: Thu, 5 Jan 2023 17:08:41 +0100 Subject: [PATCH 049/152] fix: Add 1.16+ Forge library prefix in TechnicPackProcessor.cpp Signed-off-by: byquanton <32410361+byquanton@users.noreply.github.com> --- launcher/modplatform/technic/TechnicPackProcessor.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/launcher/modplatform/technic/TechnicPackProcessor.cpp b/launcher/modplatform/technic/TechnicPackProcessor.cpp index 95feb4b2..df713a72 100644 --- a/launcher/modplatform/technic/TechnicPackProcessor.cpp +++ b/launcher/modplatform/technic/TechnicPackProcessor.cpp @@ -172,7 +172,7 @@ void Technic::TechnicPackProcessor::run(SettingsObjectPtr globalSettings, const auto libraryObject = Json::ensureObject(library, {}, ""); auto libraryName = Json::ensureString(libraryObject, "name", "", ""); - if (libraryName.startsWith("net.minecraftforge:forge:") && libraryName.contains('-')) + if ((libraryName.startsWith("net.minecraftforge:forge:") || libraryName.startsWith("net.minecraftforge:fmlloader:")) && libraryName.contains('-')) { QString libraryVersion = libraryName.section(':', 2); if (!libraryVersion.startsWith("1.7.10-")) From f04703f09b35fa7449fe368b04565016e6482786 Mon Sep 17 00:00:00 2001 From: Joshua Goins Date: Fri, 6 Jan 2023 15:05:19 -0500 Subject: [PATCH 050/152] Strip certain HTML tags when rendering mod pages Some mod pages use certain tags for centering purposes, but trips up hoedown. Signed-off-by: Joshua Goins --- launcher/ui/pages/modplatform/ModPage.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/launcher/ui/pages/modplatform/ModPage.cpp b/launcher/ui/pages/modplatform/ModPage.cpp index 677bc4d6..75be25b2 100644 --- a/launcher/ui/pages/modplatform/ModPage.cpp +++ b/launcher/ui/pages/modplatform/ModPage.cpp @@ -428,6 +428,10 @@ void ModPage::updateUi() text += "
"; HoeDown h; + + // hoedown bug: it doesn't handle markdown surrounded by block tags (like center, div) so strip them + current.extraData.body.remove(QRegularExpression("<[^>]*(?:center|div)\\W*>")); + ui->packDescription->setHtml(text + (current.extraData.body.isEmpty() ? current.description : h.process(current.extraData.body.toUtf8()))); ui->packDescription->flush(); } From 8140f5136d7389ea1c134f7eb84caaefe037a3d9 Mon Sep 17 00:00:00 2001 From: seth Date: Fri, 6 Jan 2023 22:28:15 -0500 Subject: [PATCH 051/152] feat: add teawie drawn by sympathytea (https://github.com/SympathyTea) Signed-off-by: seth --- launcher/resources/backgrounds/backgrounds.qrc | 1 + launcher/resources/backgrounds/teawie.png | Bin 0 -> 187972 bytes launcher/ui/pages/global/LauncherPage.cpp | 5 +++++ launcher/ui/pages/global/LauncherPage.ui | 5 +++++ 4 files changed, 11 insertions(+) create mode 100644 launcher/resources/backgrounds/teawie.png diff --git a/launcher/resources/backgrounds/backgrounds.qrc b/launcher/resources/backgrounds/backgrounds.qrc index e55faf15..7ed9410b 100644 --- a/launcher/resources/backgrounds/backgrounds.qrc +++ b/launcher/resources/backgrounds/backgrounds.qrc @@ -13,5 +13,6 @@ rory-flat-xmas.png rory-flat-bday.png rory-flat-spooky.png + teawie.png diff --git a/launcher/resources/backgrounds/teawie.png b/launcher/resources/backgrounds/teawie.png new file mode 100644 index 0000000000000000000000000000000000000000..dc32c51f9faf5a9c03a037831e770c336816bd06 GIT binary patch literal 187972 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�xow}-ktZ}U)<%u1Jjf; z5X7!06x%^#hnI_T!6|n~<<&@td}h-hy7&kIYyCq-Q4|9_Mc{C5V8B`CMY#eC$scph z|9r6$Rx1_0?(5#bSaX!${ml<^@%i(ZEXBr_w41Q`)H+ueF0%UQGW*|qn2CF)kXq&J zBb&bsZ=Dm3M%*@Xj4aDY(gY(ilv0E$@-UbHh4-4X!eWK@H49xxDpGGxlS5+}8CgEi z7Y;%b9ac3lT|5XQSl_0g->3L4AzIo+Bt#>36Blg4l+g-#bV0$aN|vB*_w_ zP*@=tIes_$-uBJdG~tWC^e@;t`$eKkowhSL6>#sj{WC`QAEitVE_WWq%!CrX+3a#K zFX+a_VqSDU<#~)_pm{+KE`zdCA3NM36+vdYxD0Jc)F}o!AG0etw9x)&F-%G*EO2%g zx$@kLMWw`a0oJ8;n)X$nESu|pVFK=*w2I)yDiR3 zJwZ9DKnPH^5w_P>P#ZUCt*ar97fXrQ8zs4?axzQ z!6d8f>0X-SxZJ{5xWNL09L#3=Nm~Ux<`{k*yDfnG4~wCnl}a%?H^Y&mhe^_u&UPD< zWdtgq7Su?(DeD(jng8NVwpVs&&W#c@LyX8kIB)Tj&xwpMXr&RMcb%@N3Qk$9%)`@! z@C!T^Cwu`7U5FJnFY+#5;Qae@&KGQH{cES%Pd_rUGPe`VjngKx}Cx@w}87S1{<^$KAn^m#V< zEM@S?`FT>7FI5_ia=QOk1A?3-NB?=?!?l$2Q2>VA(#HAsqntzug_VxXWL!RfiB`LX zk{TfeA`Gca&QYD2^+b*YdM~cB_*-9K=Myg=c6+uKOLlg47TWEdUqbM&KeM*D@wL9b-6}R;LC1;dJ>Ne$J@tY1POH)D zc9qEt22C(F!-4mF5BGoXk1~DY9-Q#nhVpRLm7odZhHu2gB8%q@pUM zD=}MV*J;nUAO$@V64fdQCD3T^@|@F*6A|ZL**WZ%S8dNEb2)3xpiq5tQ*iy_VuZ;H z4@f8bU#^vo&FxLDzi<_^ZIF?~N{6o1iDu`}qfOG4Hrt=S%*L-hPWIFyDiwrbM4V=b zFrs&Y> z8pHDvS&C~O%5K|`)TfhA1)>`k@>UOKx ze8n7Z7@Mx9CjCGVhNs#)+f|%H=!pIA{a#M~ogd`To4$#tIhHSCgPYRsg2{H@ROOqu zoN}mifQl-tJoPz96K-C;PHnWop(BR~qbTn>c4JRv$ZIzyvAf3_;1@1bxKR6FuKm{5 zDTxcU|Gz*LCZoFu2ci!BFLBE|I94oC-m2#%qxwivP?-qrT?yCi{*LZ)V> zIdbY4Q+sCU_IkL?66z2o6-hUts;aO7Th}(&xV*v6{5DD{WL2UfMSw;i(F7YXX5n5;)y2J}w`K7-?C)w9MM#Dp{72l46xX=?1ll zgLE&fvi?VBXno{7+0|`aVrX?*q?v=L#?0%#fz#jikC`}iKQ|uxGcNqT`$t3%_;4X_&>=t;BFQvAgjt6!yjr$DEfDR=Z36Z{08yEK0EgMKnQq zzfp8iFo=8J-TfDHBvmRLWs4chFrm@y>Z}B~Zv+jpvHT=`htt{AEtJ|$&^A&Ym?{%`t%-+%K_KpQXFkPut89Q*C)8GB0 zj2}6LP`Zyhm)m|_wtL@o1G@4+RSe(UQVJSVbF|i%Nfxe>#W9yIUSi+TgX}+a04e1V z)KT2=PAi`(S#Ujt?BBVf6vvFri%FpScd6)S)A*tzgvbS+0;i=vx0e3=MPbs)7bmU# zzl$zde`4|g03ZNKL_t)C5>`{ZD_iP*#Gq>`!m30LxDP>a#SYiG!S=XtHqesV=m-an z9%S!r`MXSnwV{|{!4-`N*l z@(TNpp@o^@P_Z(}q#T`GMos%(W^xc%R{mAxPN{&%iv2{O201u=pE8EeV@d*2Q4)+6 z?n#0GAGP8GvuvVed>0qt(A!AAr$_VWOlJ$hm7uUG{$W=NQ z+w|7DWH!ZR&WEchixC+P=R@goxXjz%c$iE$K}L$jq4Ez&2tV(!(qWV(5CJANTzcUp zwimW=^M?3r7oIZ&=b&;mqq?oAx`0qAgbHx=37W_6X8&8?&w;Q1X6kc?SULYJPyL6V z#BMI5v?d5Em@wk5fAEv+x$^-p+%0u0W$394Pwfs0Sa)Vfy8 z<%ASzEv#PCO8-I-gg^eZP6W7BZ2kruGg2)9_X{bHDXl6=lCrb1#DO(irxO&;7v4g=4rq z5W|&C>>2`>7cB8g9I;(bDlKj=FGzAQZO#j#$lI5q0227ZN8lkm*ZCVF)>)KPbUR(rG$l=a^SA zN#|Od)?$n8)h%pn(V4;-pN6Wf@{ZQl5}TOBq-dwH&U%7EB1qdQE_SqTwrHJevGT++ zm;UG+$)i2gO9HMs`#JcozsJnoZy;<=5N|H?)W82x zS{I&13Q4s#0?u*zhku4W_dbY{awu4A$jMj?Lttfl{{m`L^5vaRxj+~}GfBaYM85kxd7R4-7gHCAIrP03(rAf@{mp-xhrC)ti6uku<%9 zU)1gMJ-qaSva?jna+uv-juw{Pr){ z*ZaC=t}|!OoH=9FWHiQtKolCZy~kSgo$U7@sfcICQps~w+P0^us@o9@j~El$E`wQ* z;HukvE6`)s^!^vzdfjv)_2+dBkHgWm7}$>vN_MyIVbyjn`|?c8FOeoLC@-^2n=>dJ zs+PAW3+_-x5IGLE8 zY3X`5gc*xFUc9V{LGi$qf_89ht4i1Ns=CABXmP5&+ib)9I_uB#vB0RIg%MI>a?yHv zd0{H)o08sVYiuG0f@&H=J$j9jQqG9{jWQ!ZWYQ#x@+CwKZFur#MF@d}Gfdsi!>p%0 zPIh8FgUiN%*TdKgj>xN7&wXSWK>B;=PeXu>N7UOuhy#g!#k7-rgXwGI3M+!;(UF7I z!D(uOLN>iM4?A9asfUx6+uF6EQE_UI^1jfUXF2u2i<_4b8=5Y{A0Y6Lkta672Z73e z9HIx=AIEIwKKB0HaC7#xV%wDqr7|43gnk*O{E`~rrINU{f^%4t7Q*^T1DtK>M=PWH z@ciRuRV!oFK70M*g~iVnYXk2}6KWFUDoZ27MjMlP1S*un`7|CI+S$pMWfhW-7O+0@ z!=7N6_N9^GJpECZSVTTA)P^yS^P7$B>xQ4NjElnG=(X#4Z)FC(d;YiCrpK8jx(aBo zR;Sf7Fj&#`4j#X0J-Lt^zy29~yAhD7{%q0F$FXD-zco*Z8}O+ruJxs{=qPD&zRpcPDBI<2rU?!y zb;7hD+=>lhOryYU4*mPD#P7*B_|?WD2G0;rn=k(Jp+OMJTF^~Es*zgzyru{z*(cVL z{+}u=J>+CRH{DE(5XL`IbgrY;&aMT2S)ii1;eUipHv6|J4n$9!}jvsx0ere;h=0oi|P@#c9;bcYhg zie3jMA(3a524ggFIuy~RtC2V}x0w4<6TJF`!$fkI&jrMdfEB`uB_*(Qs!D|FLeSh= z@N|URUTho^8CDq)rq)}3nr!~J{pfdl^Q0uQ9A;1<+e`n|?H8r+^L%yZcU?UCPwuAa z9Hu;t2i=h9cCJx7AI4@;F>qz;kkl)_H$8*WB^+s7+-4aU&O@^OKfnsvdbaC2qiogDaH+0qVa#GMjn<(H3Tx z`VQX)hOI8tEhoj?6K4SvK~%Y|Rnk8rl7}r%q?!<=DIl2Kdu0?qS~KBC_~BcV&!P4Z zA-u4mLrG<{Z&bk~iipzyG}C53qNesoq53itXg()D4#U=-7+8!0O>CrX$ie_eoN@c@ z_fG@4AIGvLRewLc=7;)9`7kbYv$AJfq`M7U3QS0jbJJtE>tI&zTL)R87|@Ew$j#9p zyoJ|7+M=t&b{3M2nb3SoisXNozUzqsT5BrMGoru+M0R3&Kr}a+9$yKI0MQu5*fx3c zgof^xuL*DIH$|ePK(dff9$dQgh%%CguTHf$W;d2k5^G7bN3;MF5nS_%P`FAN43O_x z?tXp~^lFio4s=cUFVOJf-eW82fx+=2;388dcUg_$d4<;ifSt2=l6J28iMLt+W}4qm zDmh${L06(LvK3drN@Oi$*Pl7cS|@D)9qQO)>O#TUO*2gimhJwLBW)Ys;opCWd%YX2u@mmbOx6`ag!)(}sztqruB)L#YS2 zrC#r%!g;TCGPDk?$hNQgXa08FNkwr#^#@b?WOC1z>UzUp+sAipk^)1MW%Ho0RZ4}) z>hY2B;G<7R-ofvU-!3`?pB(5f{pzdIeIA}38EKH=u~{Tk>$sq4-P(J?%Q6^#CF!j8 zT6*xy78kYetY8T#RoP{fKIVM8ALjUT*KvMD0FA7(xFgpYw}h(W*nY z&rxZDVu(z2v|=4LVVFb!S?7mM>C>Hnju2sUDKt$S*MO`yK4Px-@HItSJ6_t1-`cye z8N6f9R~GA2dr}w-P!VPU*5%fWn|Ke|% z^um!Wn~vs1#Mh50R}>kvYsmR5u#Ob$&iiJsaM!+QSZN-h6TCv75=~kiJpUo8IcT4# zg|jQ-P9{EblpU^*BMV_IS|`A2LAd=PpqI}H+n=Y)>#FiQ#gg-TMUdyogXrXl%2aQS zsSdskns&kiIflM}aTTt#Az-hlYrZgU`p)wH5X=#HB}ISef~FWQ3q8*G<^HG?j9EW5 z@SF8FZ8~P>5N_ykDg`eWH;snhKoG(3da-Idlr>cOwwvWyI6g;D{;wY=^bnu+Domk5 zFE7wd(sM|1mbgdNej@O%$4tvN{zn(@fQDPzsPHbc(S?O2%29pJSXqw1V}kX0uJ6y^lVnVU za8=H=hdvx;L%I6CTp^k_iZn}pz2GowU4Cx7DHi$_f;23b8>nLeFrFUDX@rU4OWv*p z^JQx7COG%#WH{h3GEN0#_5XDpCx?YE_TU z(Vzk-1JnGRwTP}iNRqQI`}`A5h&E2Y4`8WKyKdCEE^XT8=9T<-(Y8oqo zzt4cQJg?j`BfM{F&ebNvFsCQJdcwY&B#s$p@O(7^jCUHjDuGw`jr2AAeqx8GK9xcW zO>}apdy$`jPQ=r+TI#vxPgND`c;!zm5Fh+COkbr}6wXXKnd5OZDnk+^gcC0HL8fny zj`{2E;K(km183Yo0a}X`?7* ztXJz*N)kA^)X=|*e42o|XOXhJ_T#>&yj6&5`FPO@mWa;^niphd=WJAH#_{ z9U@<>{bSF&J*2g)X~4-vS?40{*n2r2Jf|d0PMso9POF^yFElltJl*2>kRh_%fS01r zC;0Z}`$)u)*$@$F81=uL=Re+qLy0Nff^-3I`J!AATvk7>JqXd~f4WoTa!Vz|72Fg2 zykE}Tfp2MK9nHKVjV$6UEwuvC#WHF_5}bY=i6karwLeeno}at<=o>sADliJOlut%1 zJcM)m+U*xT>D%j3c>A5SKMI?W?mY%kRzKdJ(7}k6Iu5W<5Jb2EHI?4(!{l1GHd`3# zFT`@sU1c&Pvqm4&F;TNkqvr$gH=}4DnQ$@9F3mquJz2(dwBN+lP`{vkuAo(L`YQUK z$@QFBuS-_Z%ZbsY!Q(mn+X`P}njB)eWZ0$ZtObW&%sAXOqn7=VT;;0wST2vlZZW>D zL?AW?xeQl40ZZA!uQ>HK5iDv!8IQ*Y{DH7Mh0JaY1 z-oU&Z*~^rtyg7?IN{+y%=LqP+7Y&Ub2fUzv`pc(9v~L+oRIy>q7}ymkW{Yy*mxg)~ z-x+ig4r-~XIGLLJ0CMkl)39~ zU3}us`m;Ig=yV%K8amD>RRbjE*WT`w$J?pbkkP=4?%8Wdy%Ec}IQ^ZnmYpa4((Zy& zN3{oKwthJ)#ea$5p^3c23Eh&E=})T<>XERylJ|H}Z$4g(43=vHW;Z9R-205QPuPB7 z)346Z9*A?fv*rM6s9phLd9tavt~PA7MaSKYz+NLCaT}NE{+OVohG4&JH-pu0`& zQ?StN81An~haHBomI$ZHtm0?L4d9pYbo(KUNXXYtsi4BmsENGBtjj~!eG!Kb{FqS~ zMeGVJFliIj$a~2PQB&~%8VYn7Q0GzrFk12-V+~uvgFPPFz(BuK@D(lZ$1PjXtF8tR z#fy87^^#|*D!KmmKHUyhQxq%!SymKIw^<#&UEBz3M4HJINZJXH9M@7F7jA=tjyXig zBWqbVHJ6FbfzKXww-9KPl#q}eN~yy`MK4&N3i_7UkEk4n{tX>pKPP(zKAmwxA7{4y zeB+vQ=ULyFV2Nk`?3ric?mPzyqf0E|SKPO!Xh5_j+CHyB^RKp4Jw*Oyzs(wLav(a- z>?SZAUpLs-sdD=Is=8RN0N3IJ#rWr7JpTUG*h;rnym{lW?8P`liitEUt!U?urvRg$ zZ;o?v%{=T3Qu0uapL&|N?1S*uPVfJS-xG?I``tach497}-0xU)jvS7RF=dMv1}zg- zdA-u`>5f7<|47Eaio^eFV}z^8a^MTARW!->Z4;?(wgL5}kd;kHe-nun0`WGIVblgK zHl=y5xJfq)kUfbc{C-9;No#9cew_(B@CRWMnO4W%ZO*u}HB4cCMAf8ZX-9c(u%~CB z9gR&GaIZ1xczMyZDmZ1X$9${Biz9*^P!yji8k2dgKg2ab{(HbI2aM5RmgOEIawvl- z2eM-+MlUH$k4WJKZiNt^+L3Jt*<0d<(vu7&3s}19o@R0+JA&YaTY#0TVPK>1gxEcC zw=jPEz)yn&TQAuC)(%PUH!(e% z-}x-bO+&vY^x*pdT!{L0Hr;zW3pM=+tExFVm;rU1JY)2w1HXk5j>3$6o;WF_>)S%> zEFJ~!j%=r?DD##qFQVN?eV=)pm&USP%5+DVdT`gv40~VulU*&$_h{RB_1d-IqoWv# zbh)ay@d5O$qY(k*53#)R^KP%(kg5{3=Tj&WF%t#|$*yfqNCJYGVO1F~?#g$r!T zkS%dbVr$@c^%BbW$3&ZX2?@{aPVwnz6GKO0IwFrmco3kWEStRhMN1uw4OJ~daamx? zT-6-6L_>?~x-SdXD7rH<20+948wEgP&8XnXh6nWYjb!tFe-yE*U)fhm7;`Vt`Y%1e zqihvj%KhbTVt(Ir3N%aX+QofehtW!3eDr=`Owj1~SCWbzf9jODJ7;3xq5auV!IOz| z!3Uy@o%OEqK#WXobHw=b`S{j=w-Tg90KVlQUnGdYReTsh*uAmYYoW%Z*XQD9#F6DN zZy+13VD>R+Nm=P4z^GJq?>TH|%rQDY%zAYsM6&48%qK+6UHhGVEa^v!4kAho8&jqG z8m8^{c*XDc)T`p`QvY(wW})Q^Lk}xv!u5-!o(?aHWK{J&*;4TJ%2nH!@x#GjDssj_ zJKF4E8csBuzx{@*mAr6KJjxS&)b&O*{yH*sUrLj5jr9;pE31x#iy+*(AZ(9c2|?8a z;8!23ct1XV_|~j9GHcpPY?&Cn5EXvRBi~eqoolk53}jl5mhcju?E*)i5Xb;`v}0v4 zvRFa5^zzvB#jLo(>ALm}`s@I6x`>a7dTjQIzD>A7;e#7|*$ZlpO+Tryjxby$zC^5R zfyN9R1;@Q5STcBPn-fG%i-aZ)acD3yzVo3A|aNmOpP?u{fGJgExSxbi&m zzPIA$zn;0>H->{_+upt~2IFD{vVwV+_cymaS=ML1&e!=`+5CIs|2c6~3`mXV&s{k( zpdJN{@6sWV`n2|F@Jvb5@l4cuH{{g3zyIhUa8MYZer!i3IOqBul`#m@rZweeZEC-@ z`{Q7%s0#y>PPEvMJx{Mmj9UC^{`bEsi~Acx$v~nAO+R&vNv|^35CD=EIsjLs_Xy?{2?nb-cTsHk6IuS2Cmj=z`O?|X_o z&l}XV)rQFgyn1sfLyoLXL)x7 zQF#F>X)ty$4*-a=M}-`G`ga#-f$-@_3cSB57M-COtXtTyyX8#RG^}o&W}Bfv;M>8Ym7<_N+WmFWQv$ei=f0AlhoI9Y2Ys+M?rs z66L$DSgBvf08mz)ZGOr;&F#28IfFl6iaLLHk?tjOUJ9MY!3pvGr%2^Lhb0Y))~3iT zXr*P>_(;x%)AWKfg{qglZ)uW(Ipe zZP6j^t~zf^z8&@S%7o))K?L_CL+jyXp2_{PvA3(gkI(E64pkhPyEd+`sYQ>S!Cmi3 z!iZ7z3uHXktVU=gF|EV^b;KVW+afKY*poZ_nnAQ}3AW87HE)3>Oku9^3Fc%l&*k$m zu}O3Dv!=N@EbW00bCsLf)e^CzaKVp%uq2T>j7NOG^r7Rpf-R!ob}Sd51~{)}-i93Q zJ&*s2*sV@EOc0F!em**t*Lgg$>DhS1{3he$b402Z6;~X_{Jdl8RD~LO$?<@gHUrO{ z;0IlSt(s4mdyQ7{zsuX7)-$(Q{ik{+BVKK7 zl%eo2A)Mz)Z%wURDd>tzIGknhRS@~4EMeLi^`2b-KGQ{9$_#)t{dfIDbYr5@daO}} z((df0Uoad3ns4-fowJ7Y@BusQ18sLCU>AXaGR`qJEs{n2ts2u8nCKbUMFtwscYnQc zF#}oArgMKg%J{P5!s%febN==J0bA zSCct{Xt}4&-{V^e1{z!Nq>q4`hmw=NnP`HrsagfK@JUOd@JaS3B7^=-OXs%@r_kra zmsy|l5eAZhwANB#gs9_Emn4!q^Z&}Fo^>1=jYZkwUGL(>UL^|`T*Pzx98&~p663~y zB9ABkeXwgYX3A?PDt0Ekld&hQC)}?x~Zdq}^8@2hJF)zKx z*KdXBDSM>T{tF-sadMHaUnOC1y?^5Q(cDH4_Y?nfCCrv?{OjaKy`8T3lj(>5&L+4- zz4wVCq@hWVV!V>>Ne6m~8gk-mcB0O-?Y-P+W=moZ(<%|PGR|f_ZBYPz(N2C8LMIAA z+0A)7-xw~E2tWgZ0jOL0m9ZW|S!RaKUU!#&f0LO@USW+Qrn3hK#z8JYoflu5h#|&m zp5o^V_a0~Ip8t-MR>GUst`e0tPERqxqrp1K!=*|pbe&1xSG?bcwRbZJGEPcu7$ zAGcEaPdoQIViP=73*+hai9JZ@V;K)Sc9=_~xO&Cp0YF-g%5Sg}dVQ4j3Uw1SLyhNF^!0Y%SxSx=S(^Sy@eu!t zoi!AAB|6XeKeOx(pA{7o=l@RPx%Bf@0DZ{szXeJsUTxVtK-cuPRS87a5^G5v><1VS zC6*y3!Ee8i*dw}?rww_N?;?xM`{P$t!;}?CBpPIat>`tVQ86Q1)B=gNft0u(q-hdyPxZ(JIQ`|mYvl+ZpaA<2u{rS_4P2kP8eyM&MLoshFqAOFy<=@{ojE{J6kv834yyYNUsu?~&T= z-GqqYA3qF8{0r6ih>D!R>fXzj>OZTN0@AP(CduIanqcU@-p83$7mpIbu@vLV+ok&c z*j^B}GZX7@YNkxBx&kOnO)@h_FTpl*OiE~Z^E(3Qn9-d4pUZ zPz@NEF%u6q+h`x~55@dmwO?kBLw6EX9jd=0bJ&=r%ez+w7M@?`TS8T)-4LdsCRxHo z)WI*nKd06%4kPk8#%S;6o!vGfUvh7@OWoowk{L*gD1k7)Oz+olzQ}jQQt#{eb#hB6 z`EV8-=9IVsSWBffO3wEdF~_@_#r+y^4SgyTE5sfx zprnN|e1&a(9Zp>O`=FR^?7E-|qx2}|31EUs>on#~GRRgFeA$@DfT(Y#{r63S|6F}M z^hNPPX-x$BnKXEChp_Esw0S(qGR6cV-83Ri+*Y^ek4fzxBKW?#)iWp68*JyUTtg4o zDLS`KfR9J4mr~NA&+2oP%ah-P%L)ZC+fdq1?F)J;>@9kaPJW@e1@^ljv$cw@lO4+> z$O_09dJ~zHWoy#BkwIB&f5MQ#ZH~D|mF|cUXTW(Zk8;7BGPUrxITzlgOqO)G8n!fQ zHhsAKykNN0S%+@o?eQF5a~rQ;c~Y}(OnjmO24+AzNSHkbC9j1lkxMTsSDB2YQA@C z{QKpX{8PNjr^zg>32kQb8k-QtZh*0q*DvaMtt1eXl;xxq38)>l*I@p|ONpyO9jAV+ z4S@?AV@<<`%T=O*v_)QSC9{PQ05bD8aopd0PZ8|)r%wTrA#!uns9U%Z6Zuu;)I&8Z z(&6!AiX;E_`hDCH67SVi7=(YIrG*Z-dSLwu!RO_{{NwDlwFzeue_Dt7{i$w`y`I7W z(${ccd3wG`)~1U=Wo4>;E`$2bNzis5`L2dmewU0nz9!|eXbv$ikNeHtfm?z%Rv-~x zJXf?5({5@m36Z=J)3MZC{##cwV>n=dz= z+*gp_!M%-blQnD#=P`(Exz=M&se>Ty{@QwNqo>9UtKfOW!gA-_mZQUirgCppdj2Hb z0d2|061eWq#jzB1^fP7)*iI`=*;#4wr!5E-6Mgir=)C!>1}E20VZmR}EH{Xy)@tP- zI*Pz(!r+n<)ML!(5I-6H)V4aAjmeuBAfy^P>|+HBo6O-EwvHflIzP@rMh5fM+v;fH zjBXS5D=d_YZ(4VgGwE;w(Zq|Rs@>^^jJ>e|!%3x+V9f^ zQK3M5o_=m~B&!q+A8+iCs2f|0F95mFxH627k(B3CWFK07!wlkA)w}cg{V91?p8vZT zCM*x_m=m{&Z3a@`tv!YsR-V-aBq{J7PxkqPIHm2ogOn{kZhmNenqv1G$Lc^k@w>^N z53rj4jgAm<>aGd8!@i0Y4k(LI zF#Y<4Wkc^z{=JfyQ$f_K&iS1j#C0;e%a@sJK+!V7XgvdCYo@UNN4oY4*%)?b`nQfq zXp}Q8Tts8qZV7N+@%-~gsJ`Vj^mYKTxY+M9{>X*KkNg>=6wz5cUhbVOL&w)MmeyjC z=n|i*{TRrxgiIFkwY5GJU5U$K9*wEAEXa5g^7yoSF-7xJYm0o9VwMHHG>$^lWNF14<{&L6uneN&Lv z{EC7`7b^=uhrHvU5s6rcQ~=`C@7`GYpO}lxP_QL1FGDa6$i|>+DLu`n`;~=?A;hsQ zKVQt4_8uPj_EZR-k7Q4eE&%9=KkbiLE67%IZMZ3J|9Do-=c7L)!PqCh)%mGezQZYu zsyRl4CFekdCPa_E{Y5KBZ58ijP4;x@6Kyqj``K5Ni4QlMPfGG6?W}OWR`aSgB;IR| zTlyGGrDWsOl8}}p9<8HM(7OShTc5a6s&SuTxTcwp_AkL-^6uosmjwb@<0t2N-N=m^ zsZ=e7tugCWmP|3*)Cr8)zg4@gpHC617x?3Giw9B#l@kyl=pu^e4rJv~Lr;2GF_wIY zF|xFiYeNqFzxchv4eR|feP-GTNQvnJV=>|e@kBtyk|D*5d5WGv1j`kqRC7JuPvKw2 z$9ue;r_0KbhWfK_-r2vylO^^XLyT}VI^nr5MM+Ufha<@d2XCANC)X&H$mtRnekgI_ zpf=ZV&ja7IojMu?Tz9no#}^NwF@MdGNQVl%av5o0q>C6`Ogc$e4t51n)qzYVzEvj@ zelScc9_l_KEr_`sc3at|o_t@W@uSt+5pu>@zeIu6wjH{p#s*H;mKd+Xz3cg0-`vpE z;;kaotOW^t%k}|riKUuCM7y`VNT$Ra)k9CUgt*=%H4nPR5HQ9Y;X0P_{EA(z-?b`t z&)Z~=-{6nexYhYX#Ca~%)<}h<7B&G1HKBt2cpXG6-LcfOKSa^kB<4`vG8XEstjgn$##E%@e;xaGqrILS;!=r#RGFi4)(Gkuute;1z%+#O_2~XC1SH)T+&)OBwn^OBp zuUgn1N)$>I71R{&0^XR%-%lWolAt|cr_9R9MuIwmpeHA6u`jPJ(Qq^st?Fe!5j<0! zpai;0s|BBZCT99gP@5~62Piz1LGZV?sn<6b5QsIUCd05dS$o%$q#N`#fjNG19oAK% zrwm4c7S@N&4w_;CIOZZ}94t=+=d7Lu=e8=UgHRmjqYa!apSu{FpURp-%5SBx3rlY~ z8uP`Ot7O2D+Jh}>?@|xYKn`%8fOWuFhZpgzJ0O?$bXWY)m>PBw76|(pC)>lC=D_tM zSpcs335S)*yyRc&Yzh6BGLq(~sWtvdfQp-iqXQ~h$!kjzYvcl45?%z7zxrPwuePr8 z|8gKgJVhf8M_2&UZ03EgO>+`7CV3&6L!fSvZe3*4E_H-RR)tT%0O=9ca&P0 zlSj|uK)#{_&Jj5%Mf9vt(48E}#gdmY_MI|)YO^hbsE=3If66#9P6Sw(5Y@;rO-kxM zv)P1P)I$_$nFod5@|6;58i%{%+bw0~TJ%moh})6f6s_ijxhx?ux>AW~^fyh#ki^)c1`>RsGWi~td$xyO}gJ4v&mWV-8C>Jg4A6;>XfaeE?VL74AWU&w(lPFs<~H8I723>V~! zQAP3~LmTfgpNOVT-n?bGJ0RG6mvbaol#(XJ>qt}a>43*)6QF>#UC3bE=S z05e6zclj0n{6S5YVcX@&j(i$BOI!F!qFtNG$;?GUS|uD!j4ma^8GI&5fM=t`AM z8$(S?lRt(=%`y|fRS)mU@z=awbej{XR(IeG2{_!N9>U3Fx*s*uO)QsMTMgu-Egiy) zK7m=Ny@BiCT%x}~4tq%uzYoa7fQ@~AN{EUceI(l)y+WfaCD4zi@>{1LUtmyL&VQLz zP}|L;a$0#%NO;y1@;7PDxT@0lH~%rmS~zPgabL>h1Vn}?OwkEv8pH$9aueh=sq8Ao zy|Q~@-iCre_EkGuw#Gx9@QPktoBB%ZkAiW-=s``Il{3gMJdu|#2cL{M`l+eiNk~Ub zRLmn^-WJ+*O)474U# zaCKenH*fWN;3 z6;6c!X+kz;og4&m2y4SbPoSP3uy=P&RMjWYRE53JacRoZ5#z6 zW$*H8=^!uudLi=oF#KcndcW&#=sI=^@%*`gg3(#%XhN_z)jI98(*&{jrA8}JE{-u?sg4*$PaI6l$0N>% zg%SNu>QBED1fqN1Uz5eMzvh3D@dLj7HJc6?=k!GkDF<@EM5 z$9UC*3P}xLI};cAo@){LGDkVCq_e+rToNPod#|wZZxQ$H#fZ1860y7yr5D7VSvJgR zH3X<|z2X=~bO?*&BTO3zrh}swQRFd=cU9@u)_V~#BzCq$?!@iUM*_C=akvJT6)E%i za^yMf>q8aKvebUU!Sd)3%9_eo9LE9d&U1ZnNs{Q)k$)Uwcb}Z34>3S`BMz)6T0a5N zxB`ATwrNMzh_9AxjNR?l3ZHBWsMdz9n0y#Z-qJowdI$Bs}4p{AmAG9X_2P2h-1Fc__2`+^N6OgG0Fm-ti* z9l>McB3l~WaPR@Jr`dzD$s=z+q1p*c75J5~T~&v^&Wg(^9ZiUXASM)4eo_a-h$vF^ zyR+*dW8P91>VEzDXQo^K!=<=@L;Fq6sO=ZeVy9G@Mj3r*!G4?}4TE9_Cgp#&nX;zW8diTj)1eF`%s_1QofOEo zn)jn9HJt=vD_!U$vg&$xtcVvugx%+IBy_-VfCmXfBGeDbo8nG|v$u6?ETAKGoo|dN z!D)bS07OOJ10Bp%no1~37u(GMr;{K|;1!KQ0W>)a@nf>a)4 zSka)QUHz%T@Me+9U>TQAMTLio*Pxd4yVsnPj<51OaHG&jqfl5P)W$ylkDB;pAaMC9 z`3wt|dR(|B*EH9cLg^Y|YqXvh>iHS%Ey6W^@~`esOA%%XLzm$&ZJZt%+S~(nU>Kd2 z1^nD1)turbAfD;7HGoiwNbLt()&p@GE{Ft{nk-8P47>a)(UO4c;NZ`YDiGDOV%{^W zPx<|*(;8~myD;$?1{en^)4zAh`L+U22-W7ZclluQ_EoG%30|u)wN3!(`^Lt$AMs;$ zVps<@t0h)OK9Zx{yNVbGgXR{5ss7)Y)Do6Rsm8kQ5-GrNtM0W{@_ZSW@$@v-RVdMgdLr08;Z*>LP(dl*^mesq{87m@qekl!AXy! zd}H1AS+ozvf2jcP*BJ+)?o`4U1@SHwM1%v)ir2nlMysCZUmK{XnliadmX5Yv zx)1L;mN2)(g=tl3qPaei0^=tKpWePBU-L1ofiR_nu$Rz2b!YmU4DWqz0IRu*LKQt- zeBRy-&SP`tq4H@6+A1QqG_ohRFMlxM@*uPf+3Bj7mI3OKHX#cI^;OOPp);TAF83O# z_#TBd+ViTaw>8GEbUZIwriL8S&_40L@s(6pWEzxF(~hEphe?4tt&zS-c>EVHcz#S1 zGhpX#Ei3ja=Pc*d{9A1M-rG%kw@(IpG*tQk9ayTCMJnAXiqXWuaomP8YWE^>tJt_o zCdx|k#u66BmlGU}u=}xR0BBLkE*?U83&wC=$Xck2@%fc-HDfz_L-#!7=!18)cF_og!ywC61`^pxK$p&H2n6RE<$U}r>y*!fS+(I$gRDu!t5a1Y zvFYEbI*3nmhyh+^lzI<2!yx!(a8JUiHxk=$j?l=#J#N^mi>#_K(z{33ZsJW5teo@5 zMANgEsAEQKgvx#R3l{=l(0=L*Rin;i^)1TZ{K%RqBBrPwvA7e*IG$O0-IM_4wdE^8L6@U&Dvc$pxoT4qPvE8csjE-US(EBRC%@a)QY@f{7H76IEh>IZAw@P3hQ-j!zDGV5cl!t~Po?>ujf=%DXFH&nC&Ee0y$ymY1UHfviUvz^!K@%m zGByI#uX%=~rHL!awG*k8L%MD17+vVnhft7=!k7s1Z~E&YMH9ipQCl$BLzmi6mb}cz zDU7iQkF1B5;mejkx^yYx`1$o^94};nmsk?4;M$f()L@>0C9hBZ+$Ni9o{mFR5)h@- zmm;ddpeeF39|@jr9?&J>Ke;&Gpg*2cSD~v_0Ru^b&d%B-!&%wFIYWEw7=RtMIFiEL zQJ43mrE#+q3RW&@p+w0KYd#X_RzQH>E5$rMA^s0aZ~p^li&{-@M+xaSeLaUa(b2;< z0OoDHKa_ezqbDh}nUNgVBqvPYXrH+VJ#ow0&Jh!t5WXLDHGcc}CsCI#pVQP%8eR|C z9*hXFjTHneektwi69E?8w#wVOlwStfzR?^v?6I7PhVg>x=(bq1)8bn&Z9K~&$c?bs zRZ$d^P*f&y7;kwBrZ~b`TBxhFBLy(y`9LK|s{|gZ%ZoM&Pze8v+tMhK?kKfUgD+Ph ztclu3GUpVYWAx-*?mV6DBI_wJxf|~82PV6N6U9KF3dcKEa=i>&fVEn85KP_IWeP}_ z&d0$2xe+-OmO~O^Gnx8Ah9~6Yp3nmq4kOYgl$dhLoBm&cRljX%=s{i0%P&SC3v6r> zmTAGL`W;SCsu_&(HxUo{3Zy~8hpQU@wtSFBiajt4joTyIrA7!G!hWmg^aT|?$ct%>BopG4vzV!TXxrXQ480@W~?X*ccp zzsh3f)#IgvD*en#wTd#R>aNfyLYG4iO*+=6M2eHQtpU)^AB~jSXEHckEs*gn-cK#y zQJ4t6si)a>TfE1vwybL?Z$l9e9`9k#nrZkcmZo;X;%p%y>VO$KN*T|_NJ%lZenjO? zd=g2lFepDpJe88pylwRqtwZR8L7>x2uZ*R3a1$RQ4blip{cgL2>?!E{=6}Z=_`aD9 z-UZ^M3U5H6XYQCPhwT{bMGykP>-|KV8-V4_`DMp){SnnITZoT8Dc!A`xHbsnxieOwJp~q=Lsyk8?2P8sl@6Adf$~leSox>h%Y-v>YB}SHrQ@ zu>nCZ8$&jnWtfrBa*Kg1s~udB2^smgQ>ihflm1{DyChW4koqv>0~q$vHsVKarjwSp zE`6unnEIi_7DoUIP$Z|CVJoT56%@8A|rMnY^?n8mE}0Z{6obuld}>W9pmk4^2iYe_DWnY&=C@=TdS!^krBvDj z?x7Q?%^{Md8`|kZANir_qHg8rkNJJCi;2X81nwfR%^!NmT*vuj&1w-5vvV(0c1kk5 zE4d)4z6;Ly@C{`EkEg}9YnO_xA=4^;_a>84E2Hs-d{5=PbdoM2tn8P>Aj9jyCI8#9 z;HO7y2UizYy2Lafx*~Zu&j5TS0>EcbPVe?Ib=r?3V6@K1qERVcJOb zk0SWW2ISvl-t=$N%j-6}wTj;xhgdn{_OvZZ zofTBF`N`8E#VU6ewIoJ_D5$8z-bCnw2DDlZ&g%vukM?d#`L_ z31OGl0)l#^2b@VFl~C5#9uGVG&U#;JJp7abxe8Of9Z}P;`hMK`oXfq@xAuVXP2z?S z7W1MDfP)gsR!v{D{VAhQ<6v_xWO7^MdDE$oZ>T_QD~|`UIr!`U`W}g2{a&Gk2bHYy z#e4WQP&m)AA5FYQyMO-*7wVI=`qDKLA+gn;mxmPkkyK2e`&9N(feFqM5!v&-c4WT!YIPMPckJyaO$?=Q1vTJxI*U-_M)5BL*Jm3c9)ET%-$CX zfq~gX;X0j#GdZ$Vt0T*e3oI{*ENcMYB2MuNrtwVjZhTK5K-s4n+O#*`W=A zG?|iDl_EEP*Q9HLu#?Y#&4uVZjOr+$(H})S_2UytRhv6i3PYe;m;UV5%Jv4wDdGI- zwF;QFzFWYsCIk-{v*qt@%JL4boDEW^&>0d}A*23%tG3!cv}i*2MTo-T?RcRd)dG9tv`q3;Xb5TLPmWF$uc25! z3F$~&TGhOyJ!`-|!9%?1*nqmrlTUGVdXB;55nY5M-ZgD)*mGACE6d6eSbravGSDC( zR+SL8mkJ~<0y?R0On8_*y2s|HryppL_k$>IT5qWiS8y&?WEZg6FGe|e)G&K;dEb_n zaqytuHYpAPc_2}089UY48Tb0(i=@KqF#_nu3az5IE3Ndq_UJThRAq8gW!@g}$^Lu; zW-*WSYgEGwc*d%TR7|nE=lS>lOSnqp;iAJn&R`584_wVuZslvNM=xph^T45xw}dtk zZwtKyO)F-3B$}|UjH&DRJ?RjZ69>D|U%;Zu_Q;JH`3W4s#HcxZAwL8RVTbn0t69T_ z%6xKP1Why{OxjziGir^cD&&J?O2bT%11KT!O9$JtyaHHXa1Z&6E-shFcXw~PJ*Yqc zt#wA6@DwxhjGqpf5|ezsVOmM`VSSVJSDBF=wuPYK`6;B=(0qyX=ZKqi}( z$|v9^vZs(*DaFGXE3t2VkZZQ%^xp5-Kq?kq-X7N6k>4r=f6N)F)Xns9=c@&1uZQkP zD~4)8isz0LL!}~k{jCl7|4Si`nHD4nwigCwdUe#fe(8|U;uQ^1Cj>g#o(o^gM&71w zedfo^h$^!E|9JY!u(rA;+Tg*1l@ym!oI;BPDFlbmQoK09io3hJJH=f~@#0Q#cXuuB zrP$5)KKI^V`A0ZAb7s%1wPsDv9W39E28SllmzYZ+{lrl?7O{PmE&|cYvk%Kdr5OnK zlms;Y@eEqh*V-|Q1S=Ke_5l>ZzFg>~Zv?c+IS(BM0CXN$8vkH#K?C3TigxTXz4Z3J z#d>8bVJQu);QE35HQSIX)#To626i62q_^GS{9+TV?3t5iT&2+c!=3py-vikf>02xc zv%ZY4C{A8PBGYh@kSU7+LC`=Thp`8=lW4zYWp=UXfkx%a-Px%p_+<1Q0nvFwp~rgfw>EXB@!Lo#wn z9$l=6?>ffS2W2joz#+*l;A!rm&EpgU1*b+9hjuf~XyO+r9Y1HEn0$MvWG;NB%p373 zz`CDpdunq`Dte29DsLG{FwDR9Z1arVey#V!-}e1Q6ip%v>^p&wsj|Q_m!_80M|J;*fW^&C?}acAm^qnr^{Pf(Q9P>$?hms_ z*Rp_w$?*A-OB3xJP2C-tX$C0Mr|r(`XTyTaN{mDm{F3V9ihA?N+Ez$cy3HS_{9!4X zMO#Cn`PJ=#8VQh;HEF*zm*CYHOY3KTX>+yZsYU|wjHRJwa>OEL^1}M?PZm*C)4z}5 z)ER>O5W|HIFQUiqF0;PVfYTyV-^NCZX@zdLdDLYqQ_v7dic5?D85NixO5(;5$t4I_ zYkb*%NWTi$re1XWb9TJ8?B(Wsb?}Ap>G>|Ecv_b6Fjy;H%cx#6ukl`$qW#JE_+{89 z@cG^)$77Qcf*1^)SQNsflCUu3@nN($&csq{nxXtD3qtJ%6D5~fURi2ZnUz=AOf7KD;s_(oPR|#Pf<&fToZ9@J`Z{H9T}JrobAQKzH^$(* zYBEwcN}wNMas(Z}--bUj$Uox4I*uh^!^i8%7;VELMe;gn*7CNa@alK=A6#_mgBYo8 zrtmE$99bIfy1|_FM~YW`x?TI;H>49@f6aP(n18)%Bh5Vd5GthwS5#VMGp)#t+*E?? zn8E5M>8D`XTK}DjTEuL|U7jo-P%N#8x!~4ivh4DztT_@ryIaUb`J|yL*zo;t+xL<# zoPoAeOFeBZ_zN|zF#!BmOM*bSPKZTlDIi=)Q})M4ra7N<76Sn-_O90V|=P)ct3jGpnD(LHal)ABL~DrTX{D!W2Q_7N>|y^e#DK(&n!A+5WpR;Q9Bo=q3Mm zQtHKywugOzfUbm|3)4iCrktNVy8`_RChDHgFWvg3w%epH!tTo^6Cy8fdi$6ywY`z@ z1SRY|0SJy=5|b7g1{E{>gbJT*ul+Al9>Pl;d50q=2SC0?8fuh3GO$}3&FbibSLLyR zpLo7sfVy8h#J$OM=~sN8s6YvdXJTkl zWS}#EzV3{+(Z4SgRLEI)*nN487Sojn`n^eMie+kOZ}>@y*=juN?AM{p17Y2bU?~kS z=C$#mC)bDCd#-4_+p?Z9a!Yrmbw;%%uIc8qPcTd!f@}1)Nz^7K^+HyVShbL_KO2L2 zNA1m|;xyYr%jfdo9&X>ErM}N&RDq*qwOb299(d)B#-ot>ZhP;Qze0B|JvvL&rz1|3 zb5_0|JLxCU^|sw8MUrdjg`zKRF=aITThx#5Ckck~9M{L4f+Iz2Pt=H^lZnQB{O)4F zfB6K?972{Yyd0o@N{W?chk&HC-$FDU)7cL%7fUIlmDFn=Pd`a@pvH<2gPaU5oFKO{6`Z`aEa1JO2hSRSEt3cb6sfLY9Ag#_{dS z1bW^I>Pk>b-p9McXN-(jOnsxDylVgf+gI>>h50+k*G-0-+vE0WqnJsXvMO-*KynS`T(RHiRMbD?vd*$WMi!fB}5jtbWtV`)8v9C z%;zOG{~nY21}db96g3M9;^~m+43ha;9>Rm;Kzi#@=Hq?Ej=KTuEEa*R-jje)eHUTV zv-w!A7f?GIgEnHaRFOWMcSB5*rR!#vr8+w6PauMG#vT^P&rvc|{QC3Pt%=gF7Kdskg|BLPy zn?Y46m#V!I6K{?rhUDp&xQZ9^2nZc#h*6M^Vi%%H4-$a{+wNwKKKxiwI~p~%u*u? zR;f|p@YQ+*7v5OKr=@<=hxQ-Shjhd5*f)&}J*?8SY3?#F%?R}u2 zINgSyaj@51zd>4(+55FLR#ag#p7w5k$@Hl(kLm2jz3tSVi}9$}d@3I+c>LQ9?<{3Er^w zh#qaT1~9=9CC-;oX2Zuv$SiC*#nK8vel(K05>ggHj%&|K4YU5Awbs37k8W@5xn6E| zCns-CDk_L3Im59^?dn|cx0Q?M3Vz?QHa-4o`BD_f<-9oUBpmrcLY6fD0#!C1Uj2zn zqz0AO7^f_;(P{THu~~S!O-IuC$3P zU`Zz4&%uvw94dDzR?YArl>D01=F@=tkbp%)*$6 z;-bugtteC^2q|I><}X^6oS_^hEVg|$=Kg$;5HPkE6IjS(jH$6Rp-Fez2!G$;A^y4+ z8C!}#iVK(vwzi~^H<+Tvd*EC5eo!t;d_nFUz2$4EcxAct?^n4}GNstzNx?M9p?T2# zF&M_us<;Djx+m@Ns_#PG|3a|v$e8Oj;j86~B-!!%47{t7OP6%WQi7f&<}V;hexVl6l%bc+*3 zMOJz>Fil*;#F;)GyU!en6x>5lz(&x=Ri^}wW=xgm&8d@8G)gl5q_{InHECXtN{Xxx z5+Ft1%fotaT4D7QPa*0hoUJlo4sM7fkjZf=)MMWS*r%|IrvZxn)gc#SN*)d((X&PB zW2$jf9jp%maRS_*keF!I+4eZem@aXYWKs2T7;xe*i1)KRkoL1&G4QOl`=ct`BORM4 z!VqHgjeNkf?t|9qe-X4iYhT|;6uR-xQ+T;J(D6z&Txb|XMP<<5fI+svA-cyOsk9L& zHiR5$$5f$^ELqQ2Q(Yaz6qd0RuECR35Gt6QLyafRDYbn*aY>{ki@;oitj@s@2A6;eZkTpS1eNHbgAD0w7Df)6eRv4(QCL>@YCxbSOh}bQdu0cnJ4uZal z6djFE(2t3_ofAkGO_*q`AB3X0{Mi-Z$rH6}fP|PT!0*Hn1xqsF(rkfez~C_uf?!I( zOr-_iJ%EP^1>^|&D6i4IGQ9V6z%j)%k-1D*IQ`0;)(wApI1%H~oPc;;pC}uwZV(KH zpdkrRlU9AwiW(jEUrB(VW38G%Vvr2z4Y@9mM31Jb!yg|G$wo!zeHE zsgO+sQ|j5?2ePC)_H9=GRS=iV_W;{qM10VV>*I87P9j~3t zejqFYU`EF|6Z+GV;`(s(CSdcKk_=`>Lb}S4t7(bV9B^X^49sYgH&Y8Y9Y?RUV9)k*0T_ZWR#<4|0^~c*eZK4%< z++1gqU{%2nSS|CF`p&NKe#2gWog{2jK(8-BxKim7R=;`-&%??w4|8eGHw;iw;0Cg^ zbcoC}$->GmxgjR}Suxn@etsar{o`n`SlsjnE2>10Mr9_WaI`MZ$ni5}qiWz`Z za%aU?(IBLt&t=E>($SDgq)tI*^3}+spQDWRylZNcA3=%o&-?w;RSeYqSlA*3^;k}wUt*csgE4E#lf_1JAUC=qF@k^o?TOTv zT0g6ZRrujmjXiv@SDMsUC5F@P z>thX6pPKvQSk@#qIBAR#0Y6!G-?KQ+Cq|1k6K_@n_kKVt&-j zc(sOSvR)(|iJNPTtk`){?Me_B@jCbkDftQw$mo0*0`SLHiCaB&QqkPpS-jeiMb=C{ zaBNoW5b@kP*w}ce5_rZqvGLSeA2L5NU;0;cGdaCaMTRH!Vpcit6KA(Xj~bc})X&(s z|MCRl{gTyv(y%76`KCD#Z9}m?x+kL$i`G{KF~*_3>W$q7lKCn}ak?ImXl$&t9mUI@ zVo>G^fQ5a$#w@doy%Vf8WS^DUnwbTTMfWk~F_j(bcsA_x8`n30bv_){d1AM{Q|tVV zY3fofT-+a3>v;3#Q)ZCwLybcT+DJ=FgRQN}yZ{vwT2e($q8Kv?5HMz?RHAb0hB?-S zR_kr$>t&_$16hL-srkpw;*#l(q~E;(QXfClHph>#MpN{tF}QE-myZEL(S}3bLou`pelYG$}9Lo>`5v{{)lO} z-!N6bUCX$?x`+=;<=L5O4v&0Ar+$=G7sEhvQ!tD*I+u?ugF+)iPnD*CWrb6asJ6R{ zD_L#|(A2TZBsR}jS-#}4@xC!05E2Ik1Bi0$8tl7nQ->S19xWsHTdC9R#>ETecINf` z)UdAZag>r}YG=aH6IFA5E8>hvGGt9G`PxNarEt(k8iJ4uG+?+R%9)KUmoK)?A6~<85TS08?aeqQ{QL&I4_Ro7TIE{@pjB!9=nMe& zQl7#VkgnbfexzbuBc*9|1@vkHl(Cd;N{yWHQt6R>-u_Q`4gTbDolUt2&3R zkRUR!>w4$4_VY%FPHJl3WTuXU;??hT+W)5o2;77RTHA!4&b^kg^6R5Mwhc>kug3q0 z-2!H22;G2Z2vT)5_R0xO1iA9tf9NP2lGcgGlP=s;JvYtrax68Bg;*jUq{SYMRv;Dc%;`-b|_RCgXr2c6m#lqEpx zc@>oAoB8q;bsyf>u~frSl8L6LbW#8gSHFZ+lAwXO{trGQRR&eX8_~(&iT|9O~|AyZwtk8V1XGl@9Y)CxPNC>t_)2p2927zWaCde zCfmvX%jPz+ZN-Ae*S{Kk-1m1%Uufi}8BvKQZ}%{9FvPFjQ9gYU3B-Ro_yXTQb_1Jo z_Hiz+qp&i_XF;N9BCB#}O;_rD;W*(+0)y|6LIOF|7BW7G0uVjfi8B78c$@6p171N_ zLe5ZhwQ6DzE_;Bg%Y9|>D1j!Ad7gD^@E`m_;$G&dlI2_JfZ~ggmh~93w~ol@b{Q%d z?c>K6^krBpEL7pV_Kpz5wRW@+V{HZys7 z&YOHiKa%o+*cgcg-2LX45J;ieQ(!He>(qDT@{=(3{tD3|9Nx%cm^F8A9)ngZ*oE%4?hLnQm>I7a zVkP~^ZCazBJX~zgPT5EHu^qFX1LfjpK>iw~KhI=pa7e#=>G~Qn08t<&q{BJ>CB!hw ztcpfe#5HsY-+HU#M7`}sI02(uNrWkjbuv{BxU8GG?S99gKn#|Pgt!`~ZTm->7a7!G znN|Twy`>p&KoNo#AY-!wfu? z=<7dUbFNfLD%u?OXcdgBG&wjC$rnxkh78DoXh&Z~xaDKi)qCyH22SG|dAJ~M*W7T*r&O zf#k*VE$K#B$o~JU!~5j2QT#;XNTCp4u=Uq`n(+@{LpYG;YrpH!4WF%RmCc|9Gl66^ zC6O8HpejtX-2JStnYm>QHR>Yy$kW`rpDMFpuXGs_?8#JAVS4KRcMg zS+5g$rYUq`eP@vBA+)rBIh8aFWteuJcRd;9v$fq$Q!Y-T(Z^u>RJPBU6s$7DkdS6B zTi>*gm`q9z*|?|hM*x*v;Lj7;mUN?t*zh}a#1Tm)w=)TI4^&0E-F40yKw0KWyRNNJZIGN*iE{4T%+pd zL^i}(^`@SL2a*ckh=XmcM9oyGe;NDDx1S?cu1=oZRyVmJub$Ly*o|MH@cK8!zaECaCc;0@)QC!{Rb=cm|HyBfqpi#ae1f#Xjik(onl>%I$G`p9UK@!rh-30LT2R^O*jOwn#1X z+4`H$%EStWTH?S}VodOh(1DjVa^D5FlMDsR&rn_g2A$G_)7AE{^vbqBQVzsv((rsr z@B~bz6YM00f<2R=Y}@ze3>O6kpjbtPuZX>^vO7iQ5VFH;)1~g&1TUFVP#tUJ`W+pE z_YdxZY2+Joo~GFH5vQ{JMQF3e6<81-$#okhH2kjNEcq9Lv^=^~84o>hBM9xNu5Nn@ z$bP9N~-&j9*2dVa@r0 zEmh$v$%Jl-YRk9g^_^QARLQmCjF>McR)@QvcI@mh&_h*9lPcJU-w~IcJ<=}dx z-ayQA_Jsae_b*Mv4yjkj?r0(F@^lWMe2-UalcG4==BMnduw!D$mw)$lE{T{N9RVP+ z?+iNh;mg6D+or%lW zFJT8yf~4=BZ#L)V?pmLRzDgf%5+u$!j}S7dml2wXoC|;Yl36xF-*!~Pi{Ag>0r}gP z4yi_zCt8dXC)ZtkW6(e(h@rw%kxH;%(ju%-^a_oxivSqB36B)@3DmVwxS;Rb@Uu%i zGcBQCuIIHF``yd_d5IES%PVljF%k$u6;+Q}DKnNkQ=a(5F^KS73+ z#8`qhO(}r1x-*5XGwlxlUAF5Qd>_CGa%_M7#!Uat3uf+ow-go;CmDcqcH2VGjd8G5 zw=SulP~|f^4lKVOBQX^Qa-sX%t$1$Z8r6RPypqW9s!3vTz09j&C6^-!$swOnwE%o8 zhDz7z=^U9iK+rGC^Xd}W_m2YFM2@|A^Lo^ep*(eRs|vY<>bWQ&t@-osM!WBf{j4H9 zGX=3+IZ^$n3X7+p{S;+8%DeQyepkk8XbAWMMzPv0btSaFbzR+*LKWU`RiBB&DRCW#RZT zMOqcqiT0Fg25?1RebGg%ILQeVv+cqxGyxlm+&sd`>d*523z`OY#juz_q*4_H}bx%(o>btCtjLNO&{oXj!M8#!M9#iL3)0&1n>6;rvBp`!Y?Y&`=gKKV6 z6Ckt2TCdA1m_-Slw18k|*XlAaoLwHlN8>Oi!1j(wdFowI)XDR6zbUWWBpOcwu#}(3 z)m@^0wY+iSh{o_+G8u_~%$PQpkY?8LNPTM~UJ}K2{#r1@Ay9we{B!y4QsNAHW*_)i zw3J||l6`-lc|sJk((0@;a)+;w%&-ON>YKy+x6QnEKlnwzrcH;^ z9X5-Rp(nG__K$bZ<<$2>F!$%UZ~Oy&;)roI97v;JBho^l1gL~AWRP^iIGS6Bc3erK zjp|?i1&VBHmf;<$$5XK|(}{eT?vcEw<~J*|ZJKcI#*Ds(YN@Oufwd$eoCU5lly`>y#!qUOU-zYAoMu?*hZ-wqU)MuAP?`st zbjq>fz!^yZ{=Q8alhL==*Q>e0MjDDCPEM@$-I|vIg@QETp_$yqwe+U>OPXp%HEhdL zq&WG*a^hICTz;Zvi?{V%kYmqt9||xx3QcKhp~3FYJLbY&e&2d{L(xMmhMAbJg-vTP z0##aNQpoq&;!YMo#Uz@imw^)<`sJK?kpNM1e)c^5S;}GNrL3{8MZvlw)}7 zPOhHkTP;QD1zslq1(Rx9vBMb-cE3IJMZi`1_4&{GM%5C7H5b za;duxBVu^BqDPmKg$!|IGvH@)p9dBx#DErMW=sbMZ?LRHiE#mvAHu)AIw2d!~l#@G!qr&mKm|T$7*Pkl$CG~w5d4Z9|IYK4Gp1LIp ziG)M!&P5t#gYIUAxpC|S-T#Ux$D^xZUk_}o@&&;;0}gB{4uP`BEs5zppqzmzjN6pQ z1nTRM(7eW6UT#iJM6;jaevHYtH3N@~D_G{IcRjzzQLu8Ep6gJmcjoa4{j<7?WN3HY zv&{JAkx?20BO!g=6&neHrIZtkX0PF!A295*6JzkO(ge%h74JCq`!l0W*K1zgIWK(m zvXHtDL5%wM$D@#&=tF4=KOVGA(i{Hx6R#4tb?%Cvx{hYzJj1)ThU8*BE*wRKO7rcb zTBnOpd--`az%6c-5xrBBZ&K*`WIkQM-gluuep(%UD`Pfp1~uxq_D25qTRlW`sS`cw zwcc^>xnjt+-{1K3XC8o<>_;tSejiVB98E?eNm=y=b}tR>WCDn>>IRLRA}=CmI4~La zOgN9}N$9@f45ZPjK$Ij3BQXS$TvF-FoMp)t5A);P?g`e#A#8Hx7ui0mRPKKjwO73{ z4>yGo)$M+3hugmykK4Puj6d&)-efuhw4FY%d0m z^zN}tAmK#BS5Tsf70sv*pu3TrVXEW_MCwnZF=XRSgJKUp&1g zRxiefF+qxf$as5fMv)0e2<|dX(I>Jin0=7-u2>CiH^? zkn)(MD1PTe&b#r90+E!Lr@8x$+AVx7+8_HPo6Z0?f-Ikx$)<5WI1r#k6V+_wu@)x3 zEJH4QML%qHVqsqjIiP3NRiA58PmLZ8)Z(nW-pfr4MRhUtKZ^xi7XigKePf{fT4edS zLsJ5xtQF*$38LSBLWgk_!D1Fs+;UtQ)&{NhD5Y+JOPOD4o+W!qGIc@W`&Uu8%ur0`A9%{!Z{|!BFHrYYk$$Zih(YS32q` zd4cGnf7@m@7)>xj5D>Hn?%nCJm~4=Iae#aSHfL~%o-2|`mgi-=_@W)%PjO&CmB5a! z$9+WO<U$W?#>A}4; zw3x5IKD#*E-cC(}WB51^a6!^sMv77???j&3R=r=IsQDd!l3*j+(oq-XwidwxEtB!* zD!c_B@J&4as{6yq*y91v57>!}^x0RVxjU>ltGl#68<2~0b79U>?r8`o{RauT>g<0` zVg>m@b$3$V?LkIxIakotD{=^t#3|4=@P3s2-Psk69E$zMD)=*(*sdXB%@#Oypme;3 zWrxM9NqHVsDz7>h^O;xJVB(Wh`H8tql4MU>3`WC(tSo&7&e5A@QVedpA7sUGLp+OG zBiw@JZx2>myh&smBACLvUiajuWJ85geHE4u-CR(`0AtMHhD9E=sWxFe6^wb>E1W)4 zTMjn$;LS9Qj48rEb%igrNocyNG~-hOs|kp*-v|_Z;Osto7T`lBhT3?3UCWf{4@CYk zMe?)#0#`;uSU|Y?iK|JyUfbq>Z(zfWLl35`R1dZ1<7EG{N8XK~OQjBJ8QbL1^~9mG zD6^>Ms|D|V!SeyZ^%I##P4nZ=uiJ3Fh^00X5h}dRo0N{g*)>YXJXED*U;>B#GpbS5 zZagE2QE5(rpITq^SYnb$ijtohZWqeVB4H5u**OG#yY*JwVfnr&u3*ve@`t_Yir+iq zracWQV8stBmiC@^ri54=5+wd=8o6$gYKBXD(w}rw6`Z(DL(#RDij=0v43z$OPL`li zY33Wo6++s*Q34eZoS_Y*SA2iW9~Cp>pz{4Hs%2TJtp|^9#H=$tG_x2q>{U_&P1D@> zn&(}|@#@l=+8kI_z0;JA9PZYV>2(oNpceVS zFkyxrxT4DE&f4{KQ(72pY;C?BW!b!IuO+AbuGh_ZYh;o7qsQ&z_&8q7TZzN596o>v zMH3rMJz@*m{`wP@w)Z*)ZTH0JpX#~ypoO4r2n-7Bf^&-f(%!n!dmup-@^#EFH|3Pz zT!O|rlFRweG3O*sY3v-C?v{zEZhef=WV*yz$LOq7VSIxL}Nb>m(#7&3i4Tt^2GgFs!!A>H|Q23C>1=wLWs*f zb0{amr{&;{&hcs^8f?%IU$6OkIve6tjo``t>8LK_`C!bk=y>Y+Z)RsxQfce5r$|^d zFvh((2u6BC>-zWpZu4=sn5ERtiKeB(qY-U{GZM=5+ZG~9!SFZ9=U$ff8#vpErfa(DUewJf*i)jlG$t8BOPo zLrS4&eeb+@KUkc)g#CJnanQeW^L==4GC-#O8my4Aee75Ik4z~*V>Z)HAD1IZN|zW{ z(YFnzk=*FE%g(mf56Tc{4q`}wHek?-DfB5M6H3yq-nQ$gu@@pc2&L{T*UjbhyG$O% zbQ%StRaY|XB4>70*;8{$1%%Mz{CE&o+!;_~?L~~rh*>q7J!n(+fg=@@T3e@2HeH<$ zPzi4!R#!V_f!mlPp27J6C?FJy)3d8C*X!%A_;GuFhFVr8S0Pnd#7JHOp(2Ht z&#j7=r3OiFC6;>DKyXen{rwRQq5Eh2&F(@nQ_tiGu4esu{|~ z)iN1}QPp(8YiQZLH$O_39x;GI6R5gQt*-a@L$qWQsGw0EHk}(HuA#(5rxbzY+^@GFMGxWPD?Jvh}`!`TRu!j zcAUZgaORBO(@|Aj|J2H2WFwrepY${K+8r!k#FVS(Oqk*RXs0knrp8tft?rzk%^3+o zB`8YxjPp<2wBmes{E-YH0pcyvlG#E9BJZS$yYRt1*I@K~!OeK3ynWo9u&uGta@XzI z%I(uN9Uv`2ZO=s!)mAEJ{2cl)hmwx!)^=pj|Is<}cJ$ctPgRYk+DTqfy;-V!T3UW- zvIu!;9^Uz5>V&j{^1IC2$~UE5g-aE7!d5NwYd^EI=b$qfl$HVKB}6A)inT&g)RAt1-&bV;G>MP zrnJF>Hq{s!;;}gU=9@*uFnQ&(MNNc$--ab1nolX_f(T>B=dGT1I2hab4QC?=Na9{x zB}Ia%d3tahKRDn8a@u>AJ{m39AA*6f`%__vj={HU%`p6Tp5OTRjIT-8E*wT4{>EeC ztt>{SJpT0ITlY3$fWHOeXKB&%Kx?zccgvz5{?MCmbzFoj@OlpR0XMPm$MHAI`K~#E zC|y3$V#D8P!@Go$A3imFQF%5DoB^muU8nVYXdpEt!%l$>I_-k1zI~ZaAx057LbUg}0hI%_Zc1%z z6kL?RZfRp9-gNZsPMQ%k<;rpEL>~1Pj3bo!V zXHN8A+dyAKNvEvRTUv;vI+=qKmz;!%BMF!QoXNY<`Cv(nt-t3$p(@WB#_sZne4b7f z%26+6F0mhGMoHfm7rvKD;yZEIu6Db?7XWbr!fBn{r**A-@^QgSaCriZkYtw*C47#x+{zf4k5M=+SPFvAm--#vEcwE@vnj z112h>TUj=cI<*|&x(}p$P=F~C&&6K-UDJ`#5NphKF#Pap0s*figZy*m}{2iW6 zwwRg=!sE@&QqEF0Xd=bB;qIII+V1NQh?D%KCHm3dABAd+KaZ)~l{*L|S~%r?6OrBj zJj)s@i6nKhYA1^79D|+es&3pAE0t*9+4RwtSyY3?#7Yc$NrJi$)i4lBGvLmG*R`RH zNutp#IkC3~lzPI;T5MbTyO}Cxl--imR(&Lg+dGqHn36_;7WN1E-SZWlPL}vtY-yoA zV2!s({ryRjewIx?h@D602VShSQ?^}_APtXcU)j~3Z2~tQmB)Rt{0{e$on#l*`%h1_ zGJ?ADLqZ&_Msc@pQX!1Mpp0^CO*o92lszVfLSdHr&23-JyG$1cW=rta(eZuSwk z4Wjdoi9`-l{{y;$wvc|Uu71}NhSEd}9ymRKV~WlNnWZdRZwudT{XOL{(h$ggk4EIA zACI(PiczRC5K;5q@>-CtUC#DLK)rV^YRyQTF{^DCLPWP$YlxbO$CQtYO{UD;-Rkna zO}ruIFwE=4#sg^a!zAr{_%kQ8hZmirFhK|ae>PmJ?~EBWZ}HJ&PtS`twn(h8RkHiT zc5fl6^_{fwGASi@n6QSeOA{J&qH*Y9 z(V-2cyAGxY63pZr>e7ob$6Jr5<-HHCVarLZd$xexlMXCyaO;Y6vUktHg)%Ma!s(V( zd9Bg=mIWsi1C?N)A$Vlp4eM7B&^sRU;^J0k86mk@*T3nuU84M4B(hA7>yrVpSCo}S zBROz%ha*X^92wMAhh2yxYMz!gOpd^gb-iK73zy2s_itoLrw8Bu`sHk{+YNBBS?#sR zev16)Qzu2p2yuEw=J@zP>}W$?SnaYR+F5QaZHQAaBhUYvEffOJ@5`Yk%nHm;DbFjHJ=c(PxEusRdS_n&7g@*CG@O20x`w!VQ6F^Q7N3yQW zhyPqtmER6a`nIBP)9>bDquamAr|Z%hDXN>uIS910UlJe!C@jse@VOj3d}L5|f;Ddi zp&D+mNoc@ENF5Z$pqRh+_L3dsX~y(^o@$zcXtazKVEWwP{PjcH!#A@Iyr=hnNk`!y zEBk$P!BW3kXh$Qd$b-BtGSB)dmKy-ysNafY1rWJkUbwVfJyC;gMCRH({@1(+7G&WH z5>ZLM-2Z0G`g_OA*YJ+4Ff3CB;BOUIsyVzYK+Fz88B|gGL#S^o0>n?e}D%? ze44m*qNaCpLa|Jx<$qCrAeL}F6pt6$y(q%8zit6W7O%e2TEMp9E~JuaqR9+HbRl@n zK)!J?$A1$`P?)ivjk2H-t8v*yW{2!~u0fVQ0L84cY>|_LKFc@r6?b5 zq(s3e`1HLTP%CH8NV<1NjEAaWx?y_()2$;dlRS7B+x03c_%d21RNaG&>wYwmT|vI_ zgf<;Fua;Mqyxr|@uVXrjMXb@J{|D2Y=$;+pH1h6^e=Fa;)AKqSjmqyqp^G34=?_UQ zb|{bx7L$D=$awMBBMZYp*CgxVJcPk1{SaYe)^_;Q>6wQ^(hf2wYN zi~tg}NS9x<{;2a`Y1K1)?KB6|qpB~p|42bHJQC!Tt|XRdh=~PK78ZrW5*WYauaoQ7 z)SuwZ2Pf1#Xvwy)ijlK$50UAlk;lF6-()H*7x_#-@=Zmoa=wx(2K2*8FRQ$Sq9|;A zcrr=ZZ*D0ZrA{>2_~*87(kqA4_BFV3RZO{_=invBN>N-|2|gFU2J?t%kIQGdErN<<)%xz9wA4h$u?lScheZ>$ya@rgUg&RJg_AP6Hvp&WQpFIHlIc*DgEgW$Q z;*l`uBr5qj9SzC#iN(u~0l8AUDWLVkujK1M zl~rGnji)XS%8O$+5}$<>Nj%kL^`FTIxi#-k9WQMCscMX72Yecf6{NBeFyNO*y!*)w zvz&&L_~+s8dw-Ap>OKXy+fA)DO`E5;kNwn$prMKSi?UzX2WgPU5GI4dQi^2lo-FiG zh6sE7>Nid3kjX9mf+=msC?;m$6;Xcn>nVz?*?TLO5`{-(QF9woO-4#b4n=U#Tgr19 zRD5L84js)+WE{x$0bVd%sB`mz7Kb*p`|peK+2;TTx#`QQ{pGP_#*}oIj{2`oH*2b^ zvmSRpexUSl(tv)A0R20q3YG&z#Ju`XP)qd4&IQ!8ApKXkBhc~d z_p2Ynb{Hul5_-8-UTAi&mqhj1IG^@( zrQ}U_IdJ;gA{{>X*G5|Q@k{d8uQry(?71vqldhpKwF3_J{{vt_pT42Lx`Ouclep{s zA4HU-jxH;qwF>$6w%FUXRGPN1vb=^g$!fnVcN&*fg-1U3X$-Dz0>)r#>l((RArQyd zIw&zr*1@ARG#IZ}p^OC391YiK#*6p120pQ-;%WQ zf10)P8~1j0#KkvW$8m|dS}k!rhD!KWgH=8%&nB{dc?h$jG~q0SX^92 zl4kYE-_&carylt;JocGSLKah0vjS2|v=(~kt{lPDF@H5u6Kj@(!{jX=LZv=A@(wbm*{0B=fkHU3qdWEv|x678<)T20V&z%l!{wCMBw> zLc7~TBqGSNTn8nm0UWN<4zIuPIt29o8n5rzICX>4>K`txuC33e6Ar1NV}tI(0+u$` zP)LdHiBmXs^DSVEhYvhuwty(58>dr>EX&d9_R#J2ToEyI!0&jlkLMrx5|}Z724WFI zCK9J_za1s(fQutAj6o_r$LLb7cqXw8?46oaNyf{V5ec|9;;{pA~yC2-YKkH@)) zbAAr=Z1-nXRbeVH?s00R@$%!3p_)#?)67mz?G^B~8=*i!8n257|Cz^)g2u+>?=Y9a zLtVrGV-R@QqyC};7t*nK1o z4xw?D0B@fcf*UwvEcn(fEcCH|u!GTP*hoT!bu8G}z?J8pg>eT8rFGox_cwO0UAyai z(t7%-*;pNZ!Vj;*$>tq!-QVt?1|h$=ytZ~ z2SavBy)yE)D>V3AMNy1?e+m7C2wk%64RvaqIPC(rfyoAwXX4(#=43&Q$YjE%r=nVSm3@#2!=R{z=a4j zZ>1z&e)>u5UAY8f42oigX`vuiPlIOdP_Q{?b57G9ztN5c?eQ?>!so*DAWKMDA{KE7 zZ5s{uA!Uj2Xb_^C?P{)Gp~h*3d}SHK(Ew#pU^pD$rI%j7WICCH$802aoy=+Gnj)RU z!3d$~FD!?Hj{P@FB3L_p23fC5 zz{??CeEwxzyLtt?JKL5e6~|%a<~_;$l;Kb5#1+B71Z&_>T5A+#0p+lhFmcs8GI(2+ zaOx)}L_gc-)7ToJ_Ec*UAOhB7YWo_G_jo!vAr$3wh8Mp2=a>zLP)cGtn}KCrh?SF; zIN-o{)_BgoQ2VbDA7V!9-|wRi%E1<96YIV(nT*2fs?%GrBml-5l;m1h#Dze+yMVmY zgEj_(;Q?mTsdI%ep|Llca!o-;L+4bl8v%kO$y~~7-GC^iSX|#gtJg!T-9fLvh&WCl zrR-A1?j*o%hoADp>u`X-w@$S?-CvC2_1X8|T~^LyTp z-pcA*a0&b2x3$#)@Qk6=%CWq(iahVkC2@v^KnON3zJbR+`$u^F`KK`*k0ECTcqFj8 zyoCL!g6W@zNHYsi>sr(pXu@v1(MAVA+5_(Xq^=Fn%VMl|R&li$0LoynwTrEbZ{p2Y zUPiaS2;vx6Q4~W&5xC%Ct>%k+)1BECKWE9p_?+<4H_`BaP-}}QsT(w*1k8P(_aqCL z5#X3=WPUy60R6Hkt+RqR%A^LnS2v-{3R#+hae?7z0@+@LSifTt{@DnJ=WRZN4(lQV%Rj$jN8hYhW@bE0Op5fg%?l~o8XkhgMh&cQ)2+}?8O zrOY{5*SaBV&tqyc>c)G`A0m<@L8skCRh2>KWQ+mj9IH3nfET~?C#Xha5K53HsUU=I z4EOiG=X~D%@>8P&bNGorybhbqU&D2d$44opzepH;|9CR)a7vNHDOe=1dg2(sBHZxc zL+CEA%tZqP_7Q4U6d^zyr&wKCN0znfLq0KcIL=^y7hn8?{{wHl_zZTgU&U-Pg%AQO z3rBD;EimRSh|Y=y@12s|H!Yu@hKWTn0-L#BmG}#b|eXh~pUJ;Q;F=Pb2U4ToS0lWH`WdGz4`5 zMi2z&gxWpXP9AX-g9{N<|6Vj|w1I9kDnecUawxB+#%6+zj;D9Fy>U|Z^2avsWa`BGEhdtfZ)I+cF1&MKzBJXz5b*rO;;Q%CxKm|ipRY;t0v(@dQy|e^X zRY+THWJv}kEoj|b)%i6Uqm6t6ue)(8wZM?01jHaDbo&2)>E&y4ca%kg|l<2E&5^){mbC z7oy>CZC#@|=QjAPQfPO3813((ED8(`_A#D}YALUquCx!K)4C>ixUV%`pnaS}94A=p zui(n&8$mcr2m!dj(X%(=%Cpaa7>z7xp{z=g<@wFq*Eav#-TmFgJ4F1uU#|*zSOE^N z!)EhWa6Qytro++T??q{Rzbvbk;DU%K0#6dGA3F}x>EP@`4N$SXFvgH)ZA4K7r8TZ@Z9;Hi zY`8pC`G-F)HR#W|IiSa3JU2OcJ0OhFaik9vA4emB`Z)6*VuF;skFA~ zsl(wYDmeof3xmsaGP2%orlxu`$A0r>r}Kvy`yf1K6HG^q!NNixJG<8*rF4(O{@=xo zBUr!r7F>PdIe^+pay%L=q z;)DU`0udKroP&}nl#&>aN8xddrxS3_G1=Qg5+%?|;kCVefKgB(AVh@m^{WtZigve) zOV2-xI8MMtgi=;$EiU4QThF1@ZX?bz#94+UYavNf_}FKQ=Mal^P@7J=cWo29SFXTR z5>XsuJf6bv6lr2T&=mBXfhXXTO&j=^%fa!eJP%+$oE47(RRx4#(1L@Q7|;gO$r%0h zHOTQKyyA8$GGI)AQU;|eB&{6nA3%u{v>rVO*`1e3`aQ5@S0Bg-DC9RtYQ{o)8E z!Oa^$&-95$Q7>o4JYPk)u|?eG5R^3v+ZSJyXA4@LvVC9~~GQ!gy0zzDSkhmfF!@r6-vW~U&hn^A6kq_nb%`8WeRdJCX6 z^5f@WiZRq+2YR>-Rn5T4F*ppkQXr}XF&a`<(1t@B3PUKSvl%ASDdH$b6mg6vV^Bg+ zRTUWH=(f9-!R4Bqh$1k?A*&MGmoFnpGVETygvoS*$|$s2ZELM#3|@WwF)XgEqS6ZB z3_MM-ar_ic-+DXpZqEu8ot?hc8b(RH@%(cbY+VOs6jfCLB1L}sE)a(uNmWA=oUyQC z1hIOUvXV|)Q;Sk5C3fmDb%Qg6gNq1hZvlH-+ZauU0H@fzcnKtHTl%;sOSn}Z=M0>2 z=d!>M34tU{VYETZf&1gh7`?7=`XkJ*YW;*m>lWkR7rs$ytr5pb!0mja(pn?UbKL%( z_u-8vzXqu)I5r7j3`?`tF{#vt%IS3YaCiCVzkIN@dsrC`o6X_%ANU#!_J63AxBr16 z`ZNf{^3t*`nsE%)TfmL)eJ_$UtD||FvBaJwW$bCY-(SYs+7Y_~3O-;pKu%mC{?e!a z4Ziw`KLFJl)vUmHI)*X|dB2ZgNkLDZ2Rf^B(-UK44kOZ9!&C;0+0`0zv}dme5u7>v z+d#WDo>$;|EQfk$;wXg{2}r9CtevqdA*CS+V3dTG*5AuW2{j#{k|ie96l}5!%G($Y zc98Q3Jk3!R1q`D|@-Co(NGQz85{ZZq$8JqXtmcR_1`v*Zl45_bhpo-akV=AcfpN5l z!R{8iogQ}gc2QIXUVHKhJpIMbq0?W4i6Sf=J%(1VhsCvZRI?d2UwIjo3viPfU`rcF zSB}C^5hguP%l2MmfvtyzJ}7v%nLy_7g`IN-leJM^zXYjfm=!Z9B|}_v*6rEGZmOWO zF)m4zF>ae67Z5_AEK5{Xg?xG0k{hZ5MOh$mG1Ug9#`pY0CRMZd_zi7%>{-@Anzb+- z9t6CJxMsR?`~*}{KvM#9=}j9uZGemmn#9S*_V)FUO=r{oog)4DC#&hpht=V**&JT~ zfv)o+IV(i;Q|)&5lrcsC231v|)#;&g@&sF(`bkejRUpG+*%`Vb+Ed!j@V_% zD|d=+3KMs(UBzd9@3*n};`11f4p2;{SnMq#O;SKG9E?j~^)yI#6_MZ}656{H`{_v= z1HpOFWT4@z<%^JPWXlN75Je*BL`Vbs(AsHUh{q%tBF-D$WihnQpdE-#7gxc3JhN5+ z0phAer5#=|9U#*sM#T&qfZTZ#I?@=dDiHk_3~%0fZ39vKFQ$2G(yvWhmfi z$Hp#n%9#3=iPrKOeI2qlqm`|~Kvv3&DQICJ3wDC40x`%o=7`;poym>^A4tgjzM z6vsg=9}*Ou$JXBVb^Pl8_zT#(_&TyA12HzrcxQhHX)8l3Tf!h-f>=KbWdL3IW<>%{ zp|!48L6kIt&T0=c&R7t2a%b`4DbW6y>kF?xvHOA;nv-BC3K;~Dh8akSU}qCN&OAvW z=AhIvxsc?j-0G$x1X#Nf#2BEO;$S)iiZPH$4671+vV&2@5c3SZei!|c+jMvp&}!unQRJ{AAW2h#M;QVOH-2xVCk&Uq${{IFCN=s^7(faeaY#bL8Ky#76}JL6VH z2>u(&*@yQB`zhcu?xO@tx7>m=cinAI){VWlEwb}_a-$7IB(S!61Zma^{+n=G8te0A zaP9I%eCoG<4dcx#Fj8SM9ziRGPN$1}VF8LH*iYBMj@$xGm@~t)6HQs!0;-L68V?f+ zyASj7s~+VV*ghD7p1v7~vwGu6NXey?u>;jX7iW z&tTN9p3)Y|s0CeE`uFf)AMGf@!s0S?S%xOwoGQFyb5)j@Y;PfsB9w)VC&)S-aKXbg zJQ@!XMRB;dwqa@KvQXnO0TXmTKR8$rU}0ei*SD_1R8`|=*gq=+jK&izA3FwRG%h~% zB#sJM@~Sjo(8zEr)&)OKrtRjfq3NCo3HIgmi8b>qZ#Wb3q*Sl!gpXURyJH(tk*>pMJSa--e0i6b6P_KkX41*Y>G6^f;P(^7A(Db zKAzh*Am2>1)L@LG)9K;hU=O;g97WpR-0tEMjI^YUlegT4D30;^6OSWpwHnPOEwn*`@Dl+62>`h^)^;BZ0hhAP&n9{rsT`xr zeqAg3J1Qxig%1tW1SOVTr~P!MOvu$Cgn_Sx~#G z;74u-kCS=@N&%_dv_Syr9*=G`1^-y2s-2VJra$c*wktQi84!>-M^R}or+*q99ANX} zMXbijd@?EOq*g{5cCTNBEK3ab_aT);+U=p$>tR+*LnMJU51k@O-a_^Egb~!MG`}L# zrcQXJ%r?s9rB#f_!#Q(QfPgTD?(#B5`+G22Vg1x;^p;le*dKoyR7pT-R8>V6msi#Y z`+GlI6ti6bj~rHyZ~luN-h%%y*Ljh2%c}Ti8s>fj9m83ttle=3ZhP=yFfJN_U2jdB z4AmDx2pm0n0(stXJ&A-9=43p?(~o`yzyB})1+G2+9E_A8Zf!K3P0?RlL&Y;}^A#i~ z&x2P2>9|zXet`EH1>5NjrAD_8jp>DdGpbRYqzJV+ewawoEzpR%W?`bxCU8H zECUY)QIa5SwQ>5~ouJMI+)ue#)&gf7pZ@J%$L4FVfN|?my?XX6Zo2CpM3Hr7F3S>9 zS;k(PX3j6o;H`v|=G4?Q61CGC4Tp{KWE>n0>ZW5cC_*V@Rlyjb)$O9QvV!YxzJWZm z(bZK|a>D3RRaW<{5c5(%i!Xoz5R4;^Vid+8 zj$$kyKL#Clu!lwP#p5t63IH#2R98l6uxKH~1Vc>_Vuk0%HZ}>DQSa(du*x^a3Nssy z&y}%QKvl^QH>{j@mtXmKmAkdfSAj5~jRALSGoU6EP)b}0C$9Nv7aL$81h;HBw~jJ~ zKnNOQ4pgOsyR=d@tv8B9Fede{gTKdqW#@?wD%jfC=dMT!&0=T`2u_2!r#CI7j)7)e z6*}DoJE1uT0>D7Z3i}89Ae3TtWg{@#yjws`X(n^{(tHB9ZKnN& zMNFq-sJZ+j`+TA#wRSk}m0UZ10uO!v58`W|{v`BdWXT2Eq-ox|wVcj=shZCI3jlw1 zSWOO_O}OrDb-Ptnwzbx|Jr(PWP&#HH#!ymdtr>tRp>*e|a$5dPxQTK)y)};F|13$f z6ojBKGo*P34}A1*p}nvWD8_G{W;J_FCF#{&xL{hTTgfczC|tO$>hh z#5r@s2uBTP)V&^V-KY%8Qi8cPB}v;rdl`CfXl;J-Hb5ETB%PZ;tksPGw8kT!{WQvn z&HdRw*t1{f7MrVNRlnwhLQ0AK{atV_u)MU2hQ#VExk3b-li(l_CNlT;JZ~dSGn8fd zHb(+N2;ww_tYolL>MSke%mWYMsZaeI2rirzk4zY4XU)+34-aJd-+g4T^Xg$){u|tE z&WX66#M!wtN$)1HxG)|Jju+EO!h}F?Wu-gX+n@DUma%p5asi6M7&9|kKX-SMU#w*H z=OEoiEoACy|XK|GprGHduy*`;vM5~n}OIo<;{U5;kiIa_ec15|MK!G zR#!K`9aC+1u#Y!i`364ozyCU}zWfqoS%6TAENww+g-R(LJ#!X&QwC&9kSnJ^dF)Q+ zwo%vsFdhmgr36yy#_xxfWtzHp`66h6_}U{)N?SCvF)*mX@NlAalw75pzZzlg#A_N> zIxZU8y9*QF>r^)yIxA`M(^<(_u6@)z;Ji~&zi48?&?4`tu?fg zAe7}y56G;!od+N8i1AjqaRnz3qHkvi8tsZeE zhC{se;tTlHZ~h7nE?tDy+8S1h$nNI3fF=yt@)}HQ8MMESw7rDNW!^^-4ME0NLU*fefH(9gaWoH)A4w zQG#z)&ed2qov2|8bf8+h^^8&qMgfY}KsBQj+!G5Njv%;Qb#czZ#N_Y4Z#Lm{?&1KH z(oj;@O;vkL%Soc#WA`gOpHpL;Akv4=xm;CHR^vu~b!eNjy{|?$*lE2D7@k6xQ%t85 zjK?D^#sa#kfH+1ogy#9jziMgG#(;BiuG_yxxJ$C6fdF3HlhR=d5x7>j4q32o;c zDx#Q9ClhLnV%lgTM8uSo%)qcnh?on}JL$lhOTHERApgTT$E!94&7cs**}G{e+n|n!^zv@1i@q&wcSV{1<-~smr~FUbXQv2 z$s=UI)ar4qG$6-R8fl{qCD6>DqP7xNIC(f9XIs!xG zv^XZLa@ZUu;4OCix{#pulT%99!pE8y5u%v2!%#S9Va@1Qe#V8Fd3K!#`L!~ErNx|> z)id!tm78%^EBak5bW@pMRx*et(eNj0#!{08K1M_gH9dgZyAntl2Eds+FQDD)IWkFv z>A?ZM{-rPC+8eLIC=Dqkv@z%`ui~crA3|qw(XK_cfm9O8(ZfTOfYM-`J9ZlI);I}I zoSaX*z{Y*LIOmK~8WBQz#^3`+>+d=iXVuA8=edi;JWF5Y^rZQv!1Bu}?6Ba{T9gm!j1e&y(o^2Fg$aBDHvh`#2Wg^9p7V(KEEjh<)7 z`8AzVCafiW^U;34=L$GwBuouj*At7L96;|}1R-uZrUbV=_#UKr$GT&S2(LZ;6kd7e zX$*I^EenhgB&{3|eeVz8)_d=>D@#h8IJd@_D5arUYi;awBgESdMF@@BKW7AM9{)X& zh%lZEp=4EKkhG4Xpp1pEH-BlPanr4LfF%iD{rXd=T;5TdwnUn>mI2(dO4!qzvb=U! zroTy>%{^)7Ap`SITiwouENf+qF;W&YNTm^F8KS&{w7ZC7cU-{9)3@Nn+6kOIaTa&q z|9+f%-^18Ab=nFK0H{hk77f5SLlj3$L?RMGEK6B^OIgm=RtVeP$lIG&oyp<5s@YI> zCxD+a#w3IilEf*_+;=}t-gdkDP@DSH-8d;5W55W-aAyZ!_{0Abpa0EY!}c4mVLBOO zFgQS2mDt(c!rs9iM$-Z&?IAmQE7-ByKo^gI@FcLi{91`~7Bm}C#OJ`=+W*QlCI%Wb zDby9|aC~(|Pr4P7#Zm0;T`4oy%#_lB7~qpT8Kd(#_oN~2e9NR)hjF>4&}f38;K8$` z8n_Utu@>JXZSiDtblqlwB@Pqv4bjIb`02toljhpfKOSGmL$eJymu(`RU_qKO(+#YS zy0lJ!%uPH7_0KWJK=_G>01zpvol9VH8XEDL3wL8>eFK~eTzTzP{OKS39){O8QB@VR zJ9OOt(I3ROfBz35Yqc>Mk5HCHL!@a!MvrMYH5A1RdETD8=hlfFjdf}xOXn?A?y4#= zE2d!$_}16hvmX7_>0g_hZod;*e+jQY^AxmFe!a=#C|)#LpI_8wvKA*VUX$gakJ>jA zW^<0mYf@JKecEna5F&~xr4S;9PC25ig}Xld16Vt824~LSgI2qTrR@W>_J^Pbh?d(( zk9DAwL{SzfCR056`A6{j=4Cwj++%p-iARF%2oszqBwfSV>DA zCKOqkVK^M1loBNAfUh1!cKS~6g;jvEx_CRr6>~tiUFXAWB{!Kk-_6PO&N)Hb^_ixp^^%W*#_OjEWC}7r#Bm|A!{vZ9RsZm zxL}qpt~9t{;d$vsLATjB^?K2#cT%@r)XJhb&D2hkcTp}KgL-otQDnXGUU}|WoWA)s zT)%P=pZbko#=(`#ARtI$ix*}~%XsgHz6V)u>GQpXMeOcwh5Q>2;CqaPQX83IjKS{C z7M7M*K}_TI_J@GR^w~Vf_}6Bkw}|0zfQcLhfwG^3{QYB2e`D8|5~I-ot1Ihx?}t8& z(O?gcf9el0I@m{97GRwBC};O1dH#=QqY*!!w0`H%Y2ce=vN<1ToEYL@h8qY3ZQ05Rnl7dS5nP^-xk((Jbs}M8& ztv~!@xb?2PEtX<5qA136I!0L(ff&LV4=XO4k7FN?tSTgNf+&jJq*b|`zGN4?r^cNu!!~Jr?7eDGKM?X?fO$|FvjCJi5H_Jd3ZJ+ zlT{u)c1=ljShnBg&1RL*bClAbjk9za#xOz%dPk1q!$0{mc<{qNh)#bg&@EwrsI1UB z7=cR#LkM^tfw$w@;a4hDdo##LK=l`}aq2AYeDAkm^_FvZ@u|mA4F=$xgHlR`5IKOG zi(+>7DrJwq8-T&%akea#{FzdgtJB$p6-9v~x88=+=gxz35k8DYYYcXF@cMJl;_1)) z5iY&(JgAG-rGz5OawKsAP>w3@LFdazPuzjz)Ln>r%iux;yMFC78OF@-+^NH2Y7pIF zFxodG7LMdzOlmDoY6}zN%p?8l>#-QGNAY`lze3@x)}7Es7?`)Sr;Wyl259crWz7|m zpO`$JrIc=9MQAt>`$keL@IAxHiYn{pZeYF1yg!*#dQR!@oeiwxfN5YAB>e7X%tA05 zC3S?ee}3NE%>S-H`)!aO1ZVa!G3!ncn!CHV9@8Q2@l~>R*H+hGg)Ao!QH(T-uyyGY z#@DZ*oXuPl1JYI--|^%BIo|sn-wnn^X!_ROLD^NBdvJk?M<}Qhv??VbmBe&9K`YC_ zId`wSahXb9XN5Uj$~SgKDYy_A3=izVq`|uM(k^3a@u7T>B z-}Ye}*!317N<1sj8jQgzX@zUu7`!FI%0*2osGSLlJi{zctqG(M*f@Cu&ffhXF2C_A z#@p8sM-ilwgb)$|WChxsTp{%JYg%1;C*Qa=CN40_en>=dPY6M~{Y9*vK8=mDH@cIz z!uHioJn?TokFS63vv}jlCs56%;G7|fVq|HCJny2E3VVgZkYz|u-vxf`R zd;mI5@Qp|R45})@ID?3zh;zPPmF4+W%3eLX(0_AtI(cWV(K`M68D$^pbh@3gDhLF} z%E?n$+&GGBZ@z)&zWgOT_UTVz`_)%Kq&-;zRvgI~!>p{ZHd7L$NmtV1WJ#KaxM_b6;HFLDa(GDN018-U+GsRH=`wT(=9530c}Jd+BDF-P-LcLX zS%&o!r}5n5Uxk*HbtkYZT*5fdVa&PdWRkAO$(J{)@^Hd=w>6t3gN|0}rwFBesVW-r z2=DznKaSIP+*L!S4JHva2HmZFL`4Z|Gzg~J$V-2{da#H_kPQFU~y$NJkBNwAvADGpp9Kw zd2>w4XrLrB_h)cL03ek_9LI>`BupiyDXMkvCoj0QgN&W(%BsX{HVN`<+3gOx|J&8&77SQh|80Mn(bFu#R`ECqpT;VDLt~VN~}t)HuOMe*7nq ziU?2s(f@^FXt@D|5|JeFS)GB`Bqc;~2&Oahzf_nSka?i26qmt>1ub`6!e|mus8!Af zj4|d;jRr)!CZseBn-^31P%q9Hgy4-Aay}PDIpHQ6jH?>wp}{er$sS_?d#G7)-u$z9 zO6SZ?J4(YeoRtwQXl#?@jG|I1-~x3@B%EHy1o0@NCNvLouD8 zC<+J>5h0?L!T#PqHiO|c0G~T7Y{t`a&@&5{))ir$KSAPkzEbVTdF&3rCt<|jf zPw&Y(&ptL8yko>&A6z~X4@L*a%c?x3_Y9+yfm4c3y9>n_s>(Vw6Jn{goz;^lNQ7wP zMnwH1pq!&pCNy&$g0(b!u9_Dtnjqdlm`t>>>oN~Y7ljJWyH%;cMSV)u4OW$Z5o!#f zrf1iQk~MS6lMMVMppA4OMt1+48noB#kYt9&|;V!D- z7T9nLkYsTQ(m)^o6%Bn;q zD^yB`_ugCCiv9ySnT#=OO>MHLsj2(K)b0*}xo2vZg*463Usys}6mueA|9x-0e8}Kv zI6y1QgKOO#_dbZ(c#J>(&0m2u8p_2ZL{TI}9G|F);(tBI;~!>e^61xw2ZuVGcf%U( zc+%=?r9MU&NsIwqyzw?X_~9S4HsQWRIYx;nD|EK@EKVbxay^Sc7t*jYk-JlvoiV^D z4%U`X368hD$4G#8z_`G%n{EToT6pd2Uqdw=gEET9=GSf%v+2W>w%j&SUXarU{##1QNja8T64v%r&KP`UMmpPp z+P{q1a326r6b0JdE>?~lN2kAtm1D;PyRV6j_$kagD;TXoDT6l3YNQ&CMK|ng!u`<3 zHD3)OB5;?-6!0L@Ab2#yxqwrN=n@SJwJe~ z(|sEYEt$-a9gJ+VDW!d*v}5pY5~`NEK7yH4(3D`f+K1-sFT`mvjQ{`uAOJ~3K~#QF z+b@AKj-zL90ipy~p8GngVruvHLZq^)jwq$rMx6fXRryyB3Z4^5XO;4MjWNHdl=^#8 z%ZDguM>rQeNfK8G6(-XOq%x50b+m7N5BP~YKo-_PcpOBG9$fc8E*eze-~s0L`l^Wo z7|a!iS}|YiVL}6#bMUnvaL!mG%f?SiwX#0E64t@fVgyhi#=1^=9K{PmsmsU-MUG&O z*%c<6#^VWMP2YqlP?oZ`|98ZQCf(ZO89vj-Q;EF?TT{ychb>tLXNwJazc}wsfCj~T z?f^vU$;pHIRxqj|#|JQ1UqbQ5Q!qQPAx4F2Jc2H#(5kHc;oSe{Jc5)G1_otOy60gr zAL_6!B`Z`_g`${Q8~*V6Q3xR*s}hV-#7T^*m}0blfZ^UA#={|6?G83hoI+=51xxEk z+$wB7gbxje@tUMz48;z=xkQ#{AZ$@(R4)D$jc$A|wmjCa%tbP5WnF_CA4w25iTJ|og@sCKC?c)fHTP11(Fc3tX)>sh?xVtC*)K41|ka5ugUZ85p z1O{Yq73;G%Ff|j5$Q0lj%+32%RYFMx9!G#7&{835cd^o6#gUbxh|>%eqsV&;Xe}+H zv$}!Z>zgQw84irZXf%Y9rDcg}4J9jFed;j`H!mV>cTmozkW~dr2;w9OP7Q;d9X#{Z zFXP(wHuC-=j-5Fha3##K!)jM+zm_z{AdXX%W#Opaat^EUO@KAezo#mS9|rIXhb8?T+-&Y%ULkrgW{feyI0rRA-d{wNB-WP1 z%%}7L))0`ma1R<$&`bm^h1{FLNZW*D96__19@#wEG;PS6%_kND-}}Mu#nx*tik zNL`k)CM!4>h5GG9Lc%9;)1c36@s>S)4ag_~H4<|7GB)%T#F4?l<`x7r@`XiELNS_* zkVL5sA5$8=&N9|lkK*Bb-iPC>$I#2W;KIf;!x$Jwp(zD!AkYLT+8H!sm?aTrE&Ew3 zImOo1%h=nxil@HxIlTGABiMc8RU~nOm~)IL1I%VqE4~~KG1|O@CqMfMY+bqor*FFh zy`|+~Ei>=OS4VyWKs&ddX=%nm8AVYP4U&Kk7*L>)yFj)i$pxd4C}X(&o(IwGFXNAY_qXuM z*S-oEgI1oCBy07rUA*}H=Xm^kPs`$^!?OMkU88MI##rakJz1W=Kh3g4DTO2F?!mp^ z@?9Ve{f1|xrK2g5(G=0FumpgZfv@D2d~Xco)gdsgKmg1-EsT0?nEw~hoi8I)X{?Sb zoH%v{m#$vKV6cnCvSFwYQCAk#v2`v!u_>#+H2mg$i|br#{h$!yr`vh^p(IY?{lPvt zIM{_T22qki5`mF4h>qQkg*)DdXkiUpL?E=*eE1cP^#W@G@-_R%1Ju@?mxd`%gw;}}Peoy3vu z8g9GsE_~MqK8lZg@S}LoJ@3QO^o|Y+!?^9i_v4oLe+OvZ#@_Z8%2|nanxWTQK$^C|3Bk_R7Iv>)#w*V} zjo#`S78aN1;s8v;F~DnutRGrog`UQ`2M{3OWX2tAf&&^MP_lwH29Xemq9}Z3>s6Qw zk5e@UV|7yuqtPG`5qvF~r$Cd2{>BnfI^ABd%&8;If$l;dy`>dgee-otSQA(&D>fUC zD`U)a*R*=(u&lpBo6ViAHq}-2y@Zkn(kzQW2$pa7zu9~9VB4~@KJ2%qJ)HU6`M$B= z>({Tl)uXyKbxVzqgb)IOfgnt5z>Kz|6i{UVETZ$8+vEXP-T-A%Cp3_TJ~-+Yl-^=}P)lb-jM?-FxmC z_Fmuj{l4FDA0B<`qYKs&lsJ9O);>g0fR$Fv$(CZU<+kmD3I%z6Y$?$&$a)=Q{Z^&i zu4vSaJpnm1_VZGryE{g|H^iAU=keM%U&ig**KAIQR?HBr%Cfw;M){v^X*v7%@un;r zbDj|V{Z_a8t;uZKD#`*A-;MC>!-&s6hR&&rpg~kIx0=*WQUy&l!lz6< z(bTkN(_mrMR`k9br-V9tn4`Jye!oDfLPQqe3GXEq2y0p6^%Ql;S>2(+O=5AtB^$j(H(#?(GWl$iV!%QO3G-%q|#IqZ8hzn=_=5$j%hOF z{GPyFP~6$^_`}gOoQKiyC~MMu{f?{>e!7lZrBj5vmvEsgkxfRJP7Y9%IT&S#x*fE- zJ*;$A@xDi%!gqe;`|!j&-;Wb(ClG~E?QHD#8I-^@8a4o+QiD2Kz~Vnphqp44-}E*^E+JC@gmI8AmiT3y)!UYk>%mXDNy@cYj~+ zb{4$@Yc~a{Btj9Q)9KXEy?Z1N1=#vy)U+ zCF`{?@3di>7rI&0+~CkIr!`3s&*m_MV5v96>TnG&y!;H(Yz{6sxZr{^c3PF?nUg{M zncK1`|0>^*M?@5Bt$)5V7<`;Dwj!m3rX1056Ro@7h4{j|5uUgQ(efr(5L*u^rxo!F zal7}Rjn`@pEwo+GmvOrUZ&lLhNj>ag*~O+V407feMq#UHx$9S4y|Jv*5M+W)+7GydHq7H~ldaM`BSRgD=SHPa{lQUTT*FQ?bZAq|#(F*-AwkT{tSc82|swrE~fS1Y7Kxiw5w;n}B2!h&wMKNVY-B z0znXBZ+{1o2yy1ZMGU(`Jbc%q_zyqvBY5bZhtP@I_VrN$I6{IQ$Lyaer>N}s8FfZ) z^=lx$5%E)|v6N;*2#n9{kqSX6vC3n-So#!GX`4;fx~7(mM9ryF&YGud&B74Z9$%HQMb?y;Eys8Tk;r%67-S z=J`BK8g~$c;ev^zQgr~ALETk* z8gJ8xP&-zdsm@9J{Zi_RwPR~hV+q@!wR$F6uS!Z^0kWT@NA%KnZi2FjBDs(SRLE!|sPt z3aKP)oQ8nlVqpjCBd>kFk1uQ>aAXLzp#N!lq%9_d5=d2IFj(?ty%lY~e(oWPGRLK7 zpFuXCK}xCec{&S%@ON*O`TpBS`&+QsoQPY*l=5AzcIPR|X-Fx<#(fXrq4$5Na(VU* zNPlMxE){rDf*50MYEJb4PGQm#`o_e4ErDVb)72r0C|o$S7H$0&3ylHLIhZ0$a_dk4 zfC!HB58eyai}2dBPrJ+zK?o(0D%GQ#Jow6OS=|1sD>nQA5r!WQ0 z!8=RPgw-9Ox8#RuObs})n5`(g73H_as7)13>GuY<nTJ$E?z2hV->B0i$c^v_iC6ZM5PRq*hovv56n}kNz&c@5g@> zr|!KQ!LSY1kHFdiXiI>$1!yEdLk=2o&{){tBLUhHV4VQ08-VvC@TC}HDF$C@L9Ey! z#Ft~xeh3;eP~kk&JWWv<5TmOJ$af;42yyY;y|{hn4wSHy;AA{PHlJgfOc4eGw4DxaZH(%a^IR6-EXJ}u8&yUdt zhL}29xv@8)P3w5~W_YLNKrowG;zf<_o$!HSK6<#BW8<|ho={Lt39g%z)y+(5D6et2 zGk3V#g}o0EYgqNW>>BWTIqOYEh_iD6r65b2C{%%D^cq(BJ%}X1?zQJ&@;Sum2Vf{` zOnxq{)x$@|G%V=t=dO|oXgUSWCcwcqNH&2gQV;;~pa-QivRuJHBFi(77GTO8LMtm) z)E4V!)q9V6GIYhPC{;DlQAVMphAc9$t#3jkI~WXl==TPdx7*zhJcu9oiJ!nd4?F-K z2pFjV$Fj47gnJ#zxPo9IG*fCoFc3~bm}MhY3rk8sBMyWHAlA8=JE#=Oz(@^KNa(bL z&J;{0p=JeisjVwQW*fbBtAii>jvvB5|E>QKll?K`P6w0G2s<}#R{NBvKmSK~&xbzh z;63+#8&biM{!L<>SDtqMpl^~3JWI`go<)(PC<{zx2N({P8!SOnUH28M&D{h2{=jOA z9L?K%qE%WQEXGy?!1!Qh@dmCLrN=m6paZegcSQk@Li$fimfO87QM6Hhj z@n-Z6*C${U7Dm}Phg4}?6xvHC{ z()}GJR+L5wbYqXsnG(2xPG05o=!PsBk22EvFfPL$X`hYYkGz?2%g)ZkiMIe{{e z;|y|=f#e!J!SFXf@uT=}|Jgr8bhZU;4ASWgd$(`l?D@NK>(!T#yz71F^al-wU)|mL zLfq~2kR;P;mzC#s0wJW{i+Kj4zkZV0)WY6Dj3|m7_1Q0K4zYd!bUS@ZwkYz7`VTZi zGLA;4E2S`-9iZz{N1H?h?YiNE{au7%0Btm-ln7%;_V%&<7A-ce>u z+5oNT96YyOm2t~X(h@LALrrpMWdO!(AyNi16p)lzNT6ZFXHZWy`Vfp3WY$F*zWa5n zZ;uC`I&}uW_^E$@fA9}~0b92(BaC7&#^NkXKU(I+r{B{a{E84k+!1l7F!Vg7tgm5C zuADfrcIWEVGK@N15rfmN!!8uN0-UG4)(M}9B%PO53VH6}}KrBEx1&KIVHw29s2xqqNP=GK7gxq@O^#TM% z2RgIU&ssmk-}&2r51;9GSBhq^Iym6iPPA*eG}dOpx%8|leY`2qXdKq z3+Jzhq%|4!q|h5zUay{O9JUa(JL5E&?Y@1izeRU8>x>5gemrisPp+)2ie#Q( z^MOb4*tdR1mH0~hHrL2m#pKgzBYzQ-5$rOyGN(iP{iTF4?Cm{J@V)F zBtev9mW@LRz!-SM5UjSW{zGa+DaXe02|WA4*8pf}qreywjL{Vk@&QW8dkH1)W`e(? z)ox#CcRQzvF#|&}2&1SaM9@-3g&+_BSXq`dOH-PqDS-ivX^b64r|#7pcXr$Y^DmSMKMNMNgV4B&}(>SWP=d*G&Pz5tVmSd9SsB z0tM}^pxnQMm=P;L3mL+T1dpL99B0liZkK@B1_&6Sa;C(oM*G0729q3ks{zDI;v$~G!&p(Zja|j_oppkB0 z2Oq3~cLobLuBY(&eF}4t$iShT8ShZ6&7$t$P}o%EJB4OP*ht%KMDTJ>tIl$fNmdI# zJ*~m-mdrwyL`_s+oHg=c8va(K4kiO3pdx@+Jqx1^(*13eWr|iDV`neL;U&!DFJ zpf{gKU+yDj3Y;^f=^RQCl&lT4b`kM~_aHv|Ai|{$&>)63q#_$wfgh^QwzAT19GX6K z%`@KMY3Pm$?X^l;mcX?aKn`v~aEdUD(T=-#=>B)%li&R}v9`KlAtCRf<^$9!X4oRb z#rT8OGyN#7sLK`8%Ak%7typKIg4p0_(?q%AGnddw0aHkr+&UlVw1l1)?iz4DbJP}| z&AU!xXKxQ%H*SD|pw((4pU;tJDb7FiPP97R1vc7(38}j-gbI!T5g z&8nh6+5)s2T6}tx9&!jUkYR{C6c#Q!L`6Hs{JDBu?iS*xPnM<>t;(S5kY`F&yi&|*^mYi2oFHGz;I~=D^VB2!7?7W?@=r- ztzd9!1?L{U7wuLTYAZppHM37tD$qiL7~ruFeH?j~;!pnBe~&0jo$9*4?By?@_h=hv zFV&I1s0%?;>)>x*t#l2ql}*~TD<-DtrDo~N-UPA2uCXXu^q`tlx-!eG(j|#^GxqzM zI%KQ4HzV$l@U|@#nojCiXEcJxDhfNLK-32AdM8Y81IcTjM;K9bImh~P3%BlE1ECzt zD;>ZXj7B5u>`%ZtD+tcqkD$MXK*ZpJSGhEHr*5j^3}U-!Z)f98GV2`yS%_CA4z8@V zLe{?DCpvKnrNRBcvVXdKb zsz9Owgfp;+fwlyAAn@3G-;39tdj^3JxO4M5D4__-60bh{RXq8z?{XA#QtfT(N!Gq! zVHly`A7VN^Koo|UN$GS@zHx#h5txd$4Fb&51oXhdtWn&m$SS(gWHJYl_4`AlS!ziJ zMP5A*LJl7kq=JH_l9(KftzVU2}=dFo7fwt(Z)=ddPg|UZ|Yd% zcZvq!c?l{th_sgLh621TDjZlB3WicpC1C=AQ}^7BpZcX=!3%%-M|kGjF*vk{z{KsFbQXmctl_ z7VLvb%f7S_w6;@}6WjU%2dzO$1tk^qyae`Bkd^@L1TZZDnv|e1$36GlhsU0HKR*AN z-?xV}6y)wMzVXc0@c0M64I&88jCVFoZHprhbUQty^8}?SA%t+E&x&18dFasvM+=47 zYy!qOR)s(y0^8g)U|?OjLol@49ZV+&&YaWOWK)d_cJmjPPyf_f<6ttv%F0@${kwVP z4NOP-6^A>^(ljsfX{+5T@ivI|mL&)T2IjhwQtglT0YY(b=O(iG9Ni@#&T~Yw`qLvJ z2kAxj0-EH|vwBwpN}wr4-ia$Bz?;+-cr?QARD+@fE8S>K-Qf^$kkF0vN<(Mv0Av(p z7{GvFdGj>B^^-q}cYWI@uygA=IA;i>7+M=dtv0&DWiWq}5lg@2Z`=gUit5?uOgfb} z0qX?TWto_2;-bzu86bG)hrbiAJ@aRH_S3%sV}Mo^VX}J#MJdsF$5S9dY^RP!w++vp zA+>V9($^S(aayHIDzAm{K2=6n2P|g}K1!y+_2`HX;?EQHD&%T1C8vAn!iYkC?~Xb-I!V+Ex`mUgq15r^*v3)4#gNaAr(|-N(G~|O&jGDhy{?8HbOxguv|h%Ho3Ib8{qqY<`*%WPH^+f zpF=525TlUYcp0QUME~CRxr!U|DpMU;}!UR@1rUTG_lUgrqLz)#})y?wR6zQ ziUoNPz!28(f+GuADwPP8*!Tfoz+f8M4Pz%gPt~T3xcC73db!){RC*+1s`rpFiu~Fu zFuQLc91bv@P9VY<=N@?vKKM6&3axH$5tVFG^a+j>3Ud_2T)3_mnksTk)76*^RlqR~ zdYBA)Ag2LVN`zUC_9Q_(Nx-wB`b<0#fmGTo4K>MNMsu)`VbF{5f%kq0|NNB~tGI|{ zHpR`WSFp0aS>1F5i)^!+eFg+Uh+cPqy>c625F*P>#Z>eco2nRkwRc^bW}tKiLMVpA zWh;g@KCG|)ygQu^#^XKb8m_DFuSLGbH6NA~B9#IuOYH9L;8ff~k*4_SAN)RcZeE9$ z5{xm!4B2OdJ^l8P{#J(3C{;$O-KfxYIRD5u~7ML9Dz9)TFNW(kshyL$Y=>IS~^$3BJs50+-f17w;*MS5E1e`TbO*SB|d5`R#_HEk{OKF;6h;Fbb8vs5fEs7 zPZY-=5<>LhEKDB%*k8kD0h`JR%fewNdtgf~Yr46UKyAHd7-|OH{Rx;-FqGNDfe`RP1l|p79?MPwos{-wGX_d+bZs7qd%UIfO|*u~c;zd9iZY!ej$%+# zBH7;o9iD{YVj(S&xLg;?>S$%!_&2|R_mhR4OvpkGjJMqPQ;>F@tDh{CJGecrZT;4C z<*!y1A?M}hrwXNOy0dqwwvNcA_F8LL`1`yVG$semEG*`2Z~Q(?xkBY958o9p27P(G zP$PEO)bv^J4()}ihgo~Z)Lh6)R|HfJ3%V>JZoi5?FOla7(k#VzJjQ!}@W=4vcYo6A zSegeuIlc%qFDUXRMdq+rnY;-b(@IPiCnmDUo_2I=iaZW6Tk0X}wvqNbFoIdgnbhQ( zCS=`;T2NZy(v?>bh7pX>m}fa2dGGtJDJiK5C5zKd^Z8j_%siP{(vM3Xr43f2htceH zpc`y5JH>a}9ZR|PSvrkFfeV58e1@_tAhmKV!&(GR8kB4Qd^zVBZ*SqVzy7P(xqSmg zmU%}5QWSZoD2wqX53ZhwTC>|_Zr(oL-&(&Y2r&d?rh(KDAc|weaU0>m1jAc9pkDbM z3$V40EgsrH?a$r*q{bHJ!)}$#S;I9A*x6t~VC-)?TGt$V3lahdYRLh9wNWUGhPx?B z1^kG;IbE!0iw8pE(vzXE%U4~`T{4vplP6%ku+~igpiAuhCS`Xp9r<#t^lu{P}W-V%@BO5dg z3pr=iKEoH8+?ixs|2116n4_K&XXC;-L!Otl*akol*q|*xZ8hPS<}SleW(ySRhNCob z);f&Zz_UOw1cHN6>+Pl;OxQdCD?1*TdZ*(aTX2R@F!%ViqcdZ)dav1R2WtO1C{d6u zj=6vKZoK#7-w%4oO!6>%)+_*&EKWA$5S8EjITMdQ#LR3Kqa%=<|N7=*5`wf7V>aku z=kz*mT|9&Bb0?83^`IGBco3pA&Y!-3G)W+pL=?9$zIGMUgHipUO+!q&7%N>Tmol_F zJ!q}L8L!Ker$sjnR4J>kGbyVnrxuRZ-%E&%9%#jF1VLCinm0)@SkU!YCg*H&fZzYk ze~HVlyofxRLrH}!oueoULK#~YLGX*lnEy$a^7}uqw9el?-rs648e>M6<;|!S8%n7% zTC;36LF?)*1f4Z|T@rw;v@8u7fS#8ylbT*=D1qh-ljZ&q)6M2G_1IrdG$+#BE<_2j zP>f)p5w{RpT0$kI752G;qnVw6$e;7#4o^BBrOTrbno;n)w3#D}LYKN~kx9VpVqIFd z1RsM9r_T2a;8v%PC%^ZHu=CnW*uHWJaTJ4qV7he)a{X=u%O{~tWA{QRlvdSLVbeHy zEds5amhHr%wcVp|M!|wQFwG`VnraH6j4bRaD5=vy8KYHNrtJe5bi>Bq?>xMjsEshJU!<*AlY;0viol0>DW#!qT>_o&L3P#;M=`(yeE5fd8Y?GHADMm+!)V9#E62gY zvtF04+D&kVdebFDkv>0ONxZ7-dy*Cn2nEB2hzYll^^s?f>{gyz#{^9DQS4Um}8_ z%{YHbOBLsNmIC;lw~zR@5R7)ZKOid0ddb1$Ls7fEtjaPh=P91L_gz?9S%YzEWqx)E zL^uY})e-cpSZM9?c8u}*lI^k%-@xt$@)0BpFaZFO73l6AG;UZ5))5GnVt^9J-8s}a ztKkAdkapY1TG7JIY8r*$@M3nshm8`n$1^ZT5C8zm34-Mq{**5FQ;<@-iN@H-gifm_ z9JtppT;D{YHC}(_&p?bl#M^NTOiGZIQ!s8ZR2{IW71f@e>>&!y7$_qZOw~*>T$m=@ zVOm+~yPlJ{@GgJYHsbX;h19RLNsIOeMg?hsRCzSjzq#k5 zwT5!$uHI&()_ItQe#fztICq|C7G6}MhxJI((;GP#(e6U>_t37W;Ed99d6#QI@oTk|^gQ`7?F zOb!uk8jmXjfn?By;g*2Ga|t6Ao_+OMT)+7SvOGnWrr>dmyY9OmVHi2K7_F$|e!8HI z6jiThYPUNM5><{If?71|KX-qhG?ukWS!3QPjH{vnW19-^sF9|L+hG=sU^UYa@KPM? z?c#I)_BV0!nWw?Is9ny35WLeyz&WB;j6BN#Lbx${qb!Tl>y$mcrPcP^$NXDlvia)H zmU)YLOP8&hKjBOrZ>qf8@Jx_4%jq%f`ROn*SrDtxSdkp^f4hPj`sYetyX z)=P|Y2Ekb6KJDG3rBsdL=7oF4E%+^LWg`h_{doY;q+yEbsKLq?4flKq@q`NHEO~7H zjc-dRMYTVwBc3S?v~`C{##VY`cu{KU^9cI>k^`$gVlZ$q$X zbAKE$Kzm4+)YA$j#}Eh(7aCH_0gffR9QxUwbU+DEa*o~gWze-HbjMS~R~3eXWyDb% zOjx+};N}h7xpf`p1kmgD7oEsGHQD%mV$BdV#$YfQBFocisqa4r)rfjFRZ1tVY;2~J zF<=aagJlpdoLf0@PSFJIP8UU4fH7{Hu8JaU@7Bp^AD{p9ZzH*K6=4*kEDQVjGmiB~ zA4X?+8N08(ihO4eTm&fc48|Y;Ay2}X|M-!x^)p}3=YMHB^h05bLa9ImK`F~JfH8dl z0b@K1L=XXHKnT@ESqznuGhc`Z9bq`nDj%wf z9MBDFC32~(hKH~Uzi!^83k)B#*f}=g5#1<8Wf&lwrPUW{Z3_;K1qi2Lod7bC6}?(y zg@ySTs}BM9g8D0~c;b712$!Dw3Z^%&flz{EHb(i)uOjNNfrjmx1kix47HnsXTW)L1 z`VxYnaE`)+l6twWHMGm0;hZ7hyjqACrQH!VBcF+9s=1IgUO7B#$CRom!XDnEgrJnt z6+*J$r{#;9HX6{R>eMMEAZ*cp%Qx>nwpmFZBu6SekoPe2^k(ByNexUT-1Ly3FSNc0 z`XWgiwkrk(0q0O!Bj39Ty?+(K=DM9~(iG>v_2W4A;G+vP@Z*;B!11|D&0WhvQE2?z z;lg6x1RiTfDK`l<$L(>dVt42V9(lfmU^47s8lD3=v4QqCdjPQ!4}u4njYjAWhG3MT z+qKaRl+`|FRRifn#C2h3=(M|b5+M{)mTs4*7h*7cq49M4G|jBjg8_!aWiZAUoG7}T z9;TB61R{i#((RAz&z+6-@ukoF9=2Y5!S0cR2!ab30P$U?v2^MbKnRvDUc{by6>2g; zk!K!;=E|7wGfMpn0H6Q!4#CIbE*Fe<@**E-tyYXuOH8WMJP3wL%B8s79`Z0)jk~?B zk|otykwu+Oym#j|jR(Dc5JfVZ&N9k*I6pYZqE5TZxQNOuJD4WZNq2Q6p6u<7WuBK0 z^Du`o2?*f;MgVRD7#U*{LdZS<31A;UMhMxWoKK!jXX^hm#YXVpc9G1}ga8@iIMLsz z3?A8P3nbunpHM2O?WD#Mh&{B^VHbHjK9=tMW;%oF1~CTlG_eQ1$BtpeqXvM^B}`IS zA)B+1*34$*&_i8cQwq?epDqM0yXTj7TIlRgT((LBLQxt-FwkxQY|UME51=F7K#}XN z-5wYO=O20u4}Iu6@P%Lf6%=KGqA0)&0Xet>vVNiR?xG+Km3c!e(!immwD#_T(`wgY zH9EEKhPVU0RH~6dVju*o6!sRXBQ=u?2BlP$yRK3$n}*cd7tUE#48wp!X=7>Hfc-bE zodu5Lls8Bka5+c|u?lWlqRv^=8-RLCdge$DzBth)bm+qAh(Q(YY?sTtip=&Y+L)CYh%8W+1rS;>c2N`YYvx^E3ftDY7g>md)K1YbVlXJV4DZBq(KpgUJY-3k-+L3)ebh9G!08!e|+R*0y1~ zdHHob^9P^7{^d7dj6ph2knsC?Z8$|1s_Nld{hwr`b9pCu+bLPy23nzO0 z{y>(67DDu*xYcKj2l;%?iZmrfo+-vTU0Yqz24Id7u4l6uIS@j327NXdE)lJi(#B|G zH0P9NqwQ_g84Qa4(h{FdClcC(jB{`y3}aMeX_gm7ksFxNba(H-Xfsh|nLHw*+!#{= z*a2`2z)kmi2H+;4Yy?Wo*Rw>w<%>-ziyjqBgwDA&$>u1Pgoy>nYR9pN0KGi}lA_K@ zClq-cVb<>~+;m59ZnJn}HVZ>*ItR~WZ4m>2L<}^riI^%^)=hwzYVuK>E!@FP@f`Kl z${cfJH*U(J9YHe+CZ&6EwwU?@jK{)u)F8E;h^`T;CCc&aGb83e`^746}ski7sQG?wbl@VSCr=(`m0sv2B-w4 zJ_fs{FdL`Is7)U^XD;qQK{X;5D5YS5j}ed+EnI0pDFc>HupSt&XbH(|f+U&Y$shR< zoVoYGg-NDKnQs<`g^8mv#VqXp8lIe#93>XeS^{*0q|z)*9f#UVLaN$%F23`YhNeIyI4UlJZq*;oC(H>s9&L;vn`SiX1>g!8INql_V3T}7Vf=xnTGzOxHe6yQQIXxt}NdFGu#E0Xhh ze@^IN^}_kP-t{d{Jt6brLcrK5#<@tx2gUm4<|AiLoG4~{`^*^4g5_1x>UL2~W=NA6 z)GPr95QZ^};}$wR1mO%aE1;BO?K0TH>HwyK*()ENXA<&_4Ih}|8*)je{$m_A;6i{wY9TZGJ{|Nq98`6)df4z zt)?1eCDhimg0T#>!pda1hpgMGJF-LF8zpZ}1qEPTo{g^VBtrla31`g#sBvZsK?Apv zad4!7EXX6wA;_bmk2c^n0%*=a9aLigm|Rw;0pS#sQ|Pp`(1_OHQb9ZSX3`)d`2WA} zT~FY?_dJDf%w`ykb|Ga3ljq2`t|2;g5v84$wt6vNw`!c-_j#9SLI{k~05R3`^H2+Oc`=^VV3P>+Lv5?68K$n4``b#~}dC>vf@69fph# z5ebX|L=+<_a?H~NE6c0cJaYzjF29b`_uYpmYC#)~{n0LlgJn?07N%4yz;vF3Hmkxl zHXLhfo7mgmMOl`RZnua=Fkcn_trZCIP$)r~&M_YCVRda21T{sUGKQt4HQcyz8DIK$ zzm1zOzJRjG5k)O)2%5~mB7yeVGZ>sdUlC7OrQ5PoRqwQk5sP z)(^Gf_Ose(k!9)N(I=m{=idA7e_xi&9#3ZpCxrwHTVR@?-y4R*yDp&H?|}(XQSzf8 zK)cd-@57<8# zqZP&YyMO0X==HkRzfMXNMS;91kmos)`5ehSA;~NeX*w6v$)uIcrkETYAe+xSMVf+B z+VA!I=(O8d>h+bOC<*6!=k}c%H$7LeWlz?ar*5(yXuU!G4##nLyB~X+>gB-EGIee4D9N+aB zo4{*fJJ0CSev)wC%LquwK<0&|SyKYyI=Ca0YWI6*u#88)?Yr>u=l{^|4uSwegh;PE z3%+t1EQ~81h;|t`&D{g>CYPvV2MkPQJJX~!S8uc1Yk9^Sr36aB?rq$^`S)E)N4sWp zu}FBWjANxW;XHrPODM;vv(#q0g!izCrO=} zmQYGUmJ(yKkKtgsiovK@eFjHZk#?`gu{2ykQCKJVqR1hHlSOFL2w7vkC2)VIF`Z2i zMllBcrRs$wlQEwE>X-4^U;kB1cD7vnfI*(8Fvg&}w2b(~2Kwjj0_A)m*sY46wl@-3 zuZ!{CE=-vNgb+e$K!JBlDZe6Bd5RF)`s}a%i!0BbKJ}R=Kl0%->+9>AcWs`=yB~iS zTCEm1WuVNFTqr$+TYAx{Ug%Ql7;KGhe}FR2!3D>OlP9aM5sZT|ickogK7AS|Pn=x% z;G5ba<6e`}+A#o?EoT4CFX7TRUIk^0KKkfm_`p-&0;v?;+1nLYu3W(%{?4bN!w_K{ zFK?`^gRHLIC(Bajd7dTn=i9ZnzLx zz=PPj_bP~13$633K;VoLvl8YY19cv0N-)TPV?OMl41y!NgXYbqk57rM#GZyxz_>sv zchT^cW11-qGq+k8f7cmfC#nUy^D&2pIYvCXNKUhiwjj-G}5>|!6c~@)dK5xs~GnK0Xrwo$n>a}|(1bj(SI1OGRM)N4^2A8yg318fvI+=@j3Fd4?$_P zQE=WYo}+mln#RpXMS!KHK6bAfXeDv;`c<@7R-u)uj93}dNRpX##aLRgA%%@XLl@v` zQ%%{7F?73qltqEE%-vqD;RJ1&!-%N>03ZNKL_t)n5Y%d&P)$BkN=&B*V3eWPA7FMc z#;^a=e}pT~J`I)UXvHnSIb-BS0n%xMZ?2gRL83`aXp%jGRtsfOz~lv5?KX-$ z52aQQQH@V2Vor0;TkUSQ*YEW%e)EMFRxf@1dA!i>;7ecp0xn*>7kA%%0sUSNXHK87 z3`^6X^825Eh`HgjmDV;laqDYe!%S<;Q%g6mnhR=+Zxn^dvkd9HVQ%bb=st{(xSS%+ zn4QEUf}$)iOJ)#Zh(H85bNURPeBTp@qZW+Oc>U@XeDM!HgI2GD)s;2!^FRL!FaRHq z$JpB03bwYky0>oK#P-$}Nt5Jao@L3r$UmlwGBa9VE{go6#|V8`bcKBrO++Ie+kVgc7%>^$3&>1Y@1AqO;@UO32!mUfMz!(iSPeBWf*3u~`9#l6n zqZFl5mHDP;oDo=$s!Es8AQ1R?0qzcZLyZJ zJ-*K(VUz&O5gPpDAa(5`p!7oV&_;vkItF1;P}^|7cB*nlSwk=6UY8>%XrsZ5b2o6> zp;B6>1k#x3%)(-uz|3}$U3m_Il+aQ@X^n^8{S;1LeDHWfOe`Mc=I~+ep|-|;=dknh zVZSr-Ca^IjN8K$JSd+(0N=+u);c40&e*TAGKOTlSaEwc%HS%;0Go3)mvg+KWlwizY zp3Y(Rb}$$$SELOeFF~-_EO`6>PP>OJO<@3JS?cU+>bxW4;sbo-hG#lvX^OGg$L$+e z@rD2G(|GL*pNA|9gkgkPGDQ@|i28kWmxl%v2HRq-xe=o@4KgHxOw9 z+G%5+%+XyM@(cIh_to_)q_Z{0DB5b{rd`Ja$> zt3~|2>a|y2L7HdS31j@DfBfH}-EI>Q0>K41=ZNCiAt3@n2n0chAP@+`2(2hWk|Y?9 zMu_7!iabRShDegRHBJpfY;A9Ww_=2?7NRJ^*}Kl6-|JP)o|Gji%Mw|hV{&jnclLI> zx9;5OZr{0MZr;3c&&})Czb&07BMOpFt zK>!2{2)Mnl<^_z@F2caVUfRX_zA5a0#T&IbT{WMNHbB~nG4Qtdpf;k{)XQ7m5*2)- z2|<*km@f69y&u~#v{(>=vkyLsl{4pY^U}+%+t#4j0rYeaY`9q+VA?DuYS!tV-n!mV zj4Q=RY1hUTFW=>Pyu4|T z8oGH{_Wc+^31_dDWY}r;MFVX0xEk^E?n?C;}n! zB0o_Sd7RGY%or0ALW-|_@e9iO&=Dz1*^^RgN~lQ8M1Og2H(-nf2FRTc9PJJIkr1Ml zOsAy*z&Ixa(5jRwZpAHa3^^E&K`A4$Ebwc;_CK3{`|o~>v|4S`>2}F@G{ST`MJ^>c z<#^$x7jW*(S@ipTgkc0B0)%0NZl{ZtKF}% z|GnKC;DK-XC`NBwLOK~C2m%ykiR8wMpuH6kCKezo&jPBlSB$BGA}dw5+nwnK*i&1} zxEs2$Y;TG%wadF_oK{f*j1iDSJ-E>Zx+-9%j*Rw-e|KnDD6VF+QU7?(;)YgGAgZ57 zU8H~)h@v4*)eSw5r+rtE$!0MyjyfaITD1-c;r0;8K5%dYakqyc2+-~HaL-3SfeVkm z+f81_M*tjg0x$s48bli~sUV61QV8&(w9yunT2Zmv^--fC<~fJ~%1}UZ4nrw4rAM?k zhoQM+NeG8EKBQ*69Y@V>!h)43)Br&gL$rqoqc-N#DWWLC>iULtDHj2N5}a|jQ?YxT z@z_Q-udZxV4F#&bUcF}|lvI1N)wKx_flv$Sc0c-$>0IU*1s3s&Z#>7&V3P9Wa zmjMXVWYz;AeWUdzfPuRhx-cf-2ckh=OqGM}P9ChL^8iJ@x$8pL_I;S6-QyWl#R8;guxiJS}m-tZ`dXJ_SAZy5duT4t8=o_!)&F$aAzzCDv!aG z&6^la#pA^4Se(a_Apl>Btk;(`P}2g2JEgzx!o42jxGK{qZdJFqCohKumzc zcx=y@n6PJJm>G;c0gS_7BohM?8i_?qOG2n6y4C8XdaK^5x~gk?dv13gLRT))c23o9?{Z*b0XAIIqSClv_d1Ekv@w}@8U?ytvD%h++Rjz5w zHIz2-V5?v-i%FbzJ;(7Uj(f)kB{eiLQ_r_>1d{y9OTb_Y(c&To!#>)JE4cT&-|d`_ zXB7H%VKI)<4(phaVVXlog>amLNd-}qV9K-ZJiXF3+7PNuowjK`N7+(>vK>K1F5s^W z18Bifwn7*B=V4)LEfiy3L*`z49t1}ZC_=NvM>lMtEZp_Gy*F_8TC0ua}Sp=NfC$3qxnu)cQGE%4TYa5e>5GD2(T2TB;ay+ueRk*0}P`IEXUtzLVu zwT&nK?6bId>PZMDpuL+kAr!%(MXcU_JKBd1)mAvwIbwcbZ)QwjZ7H$$$}6Brf`xV) zK@dP|jn-lZ|KR6-3OC()GYBQodaw4UkcK-82~aLV$)GMuoB@TbI~K?OnmkTpKJ^nsQ9|>0 z-_1{E?0MP?OIW|{PCWbBKZVuW4Lp-Q*o!YBzV!h}rRw3gu@;0{Z%bn*jIau}@M)FR zAf6CFD`)E>eE6C1x)x(Cv_XT8OdtgFN_|ofcqy4=l2pl^m3~F}yct{5>7i8cbwFh}pR zuT#RbV}Vo($#4%@o}w&r?CtGpxx@AEOXC(ocP%}#tm@gwe$G$$3BW=YZHtzNF`y7L8%p5Cr@C_!)8smtfF9B zYapeZ6dG%h@9kl9?mPme(e3u2v_zg~Si9~Re)<>x8{B!%o%PIf+N^FGt8`*zffLuC zz=`Wm;NJW0#`nMX`*Hc=WqkbiKZa+%_!y!nhS4VKEiK(P+}?RFfM5H1GqoPuxN?y9 zYwrI5z+i3xQw8o{StE8dCf1k`*7l6iD@B@K2cTV+g$1w<;B^32fMILU7dPzg?yj9V zb4I`#5y$Ss@9*s~t+gP8kao9=D_5>yeRUO||NNgJh(au~CHlHsZj0Xkqdyit{mj$L zPdxq9@fRL@EH{+=axzXflq^4eHxEAq;35dgPRV>Qnc2V+Yt2$1LKe5$&`Kc|A=J5D z81KSsh})g*9AAYN{OTc4c6x3?W}twIG(fEd_qHQdcSAV9A`Z$4Y+@!Upw;e^0$H!M zA9}F+XD9PC2(trRLNHZ0VJ%GQgqGfH)E8LCyqC3M`|z=WXx6AOG4v zN7mm(yVC_F6zRnm5Z-VPXxN_YkgXejSFcy<3A3l77gyh^ytbtt*7^cPC;|75xk~Gr zgizhQw3++k1GK8hX!u3j*vZbZQsh^DYz7vkbhPF$5OtSS!z#XN`7vIt8JZMG2bmO7 z)!uE!SXknH-8ijnb*ynt2NiLG`Wchc7NIVDlbyeX>R(30EPCBVD6J5LFYet&D;Qwhy1JW@MnGPE56#bPP&m zVAImI8UP?Qh%%ts0#Z#v0tpa}QV?sguswhZxFg?mS}=?v>$D(4fieoAIGZL8*amT6 z=4;jc*lKmqSz1P~*F(-2q%6TGL6N%|i6dj^Nx`=U@CKX!L7u0$bon)`tsX@XgnkY; z$z;Mz%IEUZ3IY*=^%;hv0i=`|?QG-nOE2QNFFlOW&K9CDngpf=0haE%1B*wFOl1l+ zY1UOytiGnp61!*5B0GNpQ4~Q^V0-HdtTuSZ``?TAf8a-P;<^)_L1{5RLe);M1XDyK zg4MNEtggF1zjEOc2A6kWtwpEZ3DUvn(1Qy-^6=*6Z>qeUuWWBtzf|V`Yyfz6_WSOM zJJc!_DJgGJQZ52m1aQ}2Z|^vO4uD$$Tn~VA9u&XykAFU5obip#jnG&_!yqJOS>Vv} zGWm)3{}_K?`JWtm`IT4j(B~e)qhI{u>To=|el!^TEm;(27LFV~d*9~P=d_f+|5To6 zZmsPS=XFH$EJZ*#%w7T2RyAf?v||j{7M;uTRc!sQmT)+$V~CO*RB1o_cY9hY1Ze1{ zzIt3V91I=aYZ!y6H6i_Xf$#b9E(V^Sfs&ZLMjs)`EskGh&sa7jvl_sh@LF z8>_DqU}0$m_q^x5c;rL>C$cPcYGJFO`kP>@*G+||@yhAgLq^nxqrpyg)2i&B3H{vlwGsf$IdI?Z9cOywwk2K|fw0DQ@dgpFNns;Jkoffio z3q>o0VGL66{dE3{>*60^LLiJ=7$;+lw|CL$b}`!7!G#x2qs%fKT3Z7P1Sl8v@SX<( zfe1l8bFnN7oIC#t*4B<-!6$FBhW@1PIz1HJTUbO8gvgQvr%!zuFFx`mjJLNyz1@vc z3Z*R3T3^TVO|L`i(4krcTFqq4yv!u6kq!pfeCAo0@facqzy*ia8k}+bz)${N{AWM; z6X^DO2V`~I`5r`M!nOl}(;W?lxO8y?zy0gKiHk3sf%SfG(wL$svYkiAd*8CbY)@rl z10V(PWaGmrd=Ah31AQDm)nU(%qRGoP8w=9nS{G@cNTl3G_|{ceM` z+>ROALJmu>ubB)K@@}j4Pn**b?<+X_*l>2*KMj;X#v$w|b#NL0qs$Z!mU5S&LcC>v zG)!D>Ps45>^N0f0asJ=_!#|F#Q%@k-x&pvrFdjp0pGSE3dT$+LY9VKBC|Nlvdpfac zP_^hx?100vJ;+8;XVqw9!5OJ(163DdDyL>!&2{_`*6P}^+4!M8FAE`dS;%D`r>5TQ^Yg#uR^B9{oq z8M=G`%{i2C-A}gAh6)7IUI$7rs0wjtT!-U99~-Zo$L8fLDAO^<$p{t(X_n&B>E|ZU za1fx~TLfVgVW)$3w};NsGS*I>M61^W4?=8jU53^QhYqa*VAGIdgH>sb!Ek2>Pk#E7 zIQ{q|uu7rV>mf^B*x9(quzbfISUi3VK^)hGrDCmFi>V^x==KU5r%xj}cOKk%iJ{%@ zKq-lZqpSFl5Bw;8=!bs*Q5;SCRLzN9&9e;S@fdlQp(siW_WC$?_8dl|1lk(xZ13VT zAN&ZkGH5OI+}x59ltp28H#e_XYd?$sA0AH(_A1Ek#Rfe0CjfAF5C_)HgYE^eTo(C} zow8j1?4SPet)KamKR(iqTZb7XDdynt0e^h-tF{|=UGM> zmz2^V4%%Q***ln`Z!I7v}P0eTy|P?0M-blp?RDwjHNK@o#iVLrv!DFBO6gJPi2u=XeZb5K?BuQb~F%I8$ z3!L>VlZnUl>{Xer7q>5-`^~Y z;(sTcpN1;%H|aR#XwJI;9%_F5E=DL(dO1szbpZDh;4M4|?+QCDIVy|%{JC?xK@eP~ zOytldc$y`xwIfG6VH`z8k%LDO5JxD(5M?_;)`{oBVYBQnd<~6dwa`a-4kq=4XX;}N zX~g~9#K0)e!ty^rb6$s+&8n|wH4t;%d+oIAbJgW!?Ffc*w}df@&QO z2sm@yh_QO=y%I8k>nc4?i7*2A&Z+)m0Bf{^y0nEY6l|#g=@x#1%3;FR!x3A#Jn)9%swzG{ z)s}<|;LJH?u!*Uq4V<~FWfKM(5zNJ(Qzx_~0RvjzEu+++Qp0NNx}T&3A7rowC<_je zNp!adNS3=umb#E!ICzvXEUm5~+uKGEgy2G$?JHL{I_+*;n7mD0*cfAsBj5sQnjjkv zP$VN5W3YYsBF;Sj3=SPXf#WybjODe%SXn=UBganGp=VncFJR;RIc&am4rQKUDq zlQFE;7>&m;0J=BcfcDxN2xC)uL{*_7^|k5eHCkck>^bbd_##5a5qRZ1fn&!0$ba$o z@cs|{C>9ob2Ml8;_n?vzzx^A(jSv6U@7CtFlrjkKej7a2PZkIh0?bA@XDf)IstG0T{!)(9bB z0rNvZ4Z|p?Zn(!XPc6k&VtEhH`OJ16Q(X{a$d)>2?Tp+L3+0LpB@-uUK^>j9rX?P% zcbH0k^k2XA{cPT^m{SGIxC8*8Dx2U=B>t3WuVb)8>R zkrXNi=!&&w8y5|+WD1IzoS%s@yc&WTV;T%C3up~&y@GBWgEK&Dg{9Rs+;`twvAnhd z77LJ=gH(maGe>NxVf(p*tZvF4@rL)vM9iWaGMzB*pcl{7eN@J zpXX?wJdW1#a(xbI0}8J;FBKCK0Lmo6l^0F}+k0q-q3>ilq>^Z_EaK3@p{9()sm zFqpH+CHvr&3$Ii7Kvub^^fDT}>)|oB*kz93%J?bajxh?Z6qz9AZOVgb4Kvp~*nE z$>*jach1V*5R?}ZCJv{CfOCHL>IZZWt>8`H{T_Vk*(b5Lw*y(0FnI}^?1IJ}Up!D_ z)=h&>-tdEhbrf+5I)SAO{M-do8Wu_>(aN+jx0x*JlO#_@Cb0%p%t4c7B$e`zn-^d* zhCpy9uCx|qscL(jDzdtAc~(_CwTDZbO^Wk*tD>u?EO{*IXzYr7(A42ne*|YhK7cGz z43b@}t*l{nWgTnlg%LY#001BWNkl()z&im{MWyynoB2Pi!S00% z*g5?oXjvi*Lnx`Bltf`I?tl09;2-_s&)~WnPE4LdaDX%JWH-FJwt}DeAASLkJ^DC4 z_a8rj{*`T%Wr-|F(C&88?sjdOWtWoO{>zN9N2Qc6SlGuv$P1@bQCurBU!Q}AL9`UN zS{d`EZ>@G4oN|npdoW%FJ{#UN%jqZffPws+K|aw2aerLvcdBlMwT0i@8E5O?*u?@s z5rs~hFzcRs0K|7Sc+bo@9w=|e;%rOrC0ZUI(EuI>knlO4o}! z_wEIZ(21C`vd8i8ROL8O=iDd*O-v*AW|Hj58HM)m!5IiP6#mvui8GtJCsHx%Fv*+` z0zO5l=OB|PgGQ%EU_p~zEXN@<=|M^f!WdSLAI0J0#~h8h(9q)oc9_F#r$DYiv;nQJ zEnB}{eeUKknM~dDc@d~JuAnsO*T!{2hEV_%#Cjq?C1CaO4KU7~z*6Ff0S)qp>!4h5 zA?*Mp!p3i0DV_Yd*6f8_hIxY%o)8V+mq%rLfrU-(nm?sV|L zw?BaU-||Mh_dosH`0OV>i;sWk4=~;vA#Sx%7UiDI^Z!68``4C|3s0$BUn>pYfMQdW z#gGQP*Pd*ea;Zg`hc{-DSyBKMQyB53OBz+Pnf^iA-yDoUzVq zlTlDI5r#G)HJ2Fb;{!lyHM&v;G8@92e-=Tui$HM5vcT%mqqy^)`_Z8>vhzdOeg@>K zW{FWs07Kj+%9zO}sVaol_*in<*etuQ#Q4FH4ru)d)ut{^)S4+jB+?MUOeTQ-I`o7B zGIbGJcfA_}lnOSHZf*lO)>}IOjXB6d3@pa5i~%77i3P}FRMV(YI#L1e1`r*F#Um>i z=`luod(c{A(BG5e@hF%*KaXc4319$K46vEJ{Dars`f;UXCmSbAuZ~}OP#b-x)~Xc- z(ILk8$v}v%l2Rt)u>tIk1u2cz4@FV*hcYkUIvfoC`);>)a}-5PNd>ky!g%xo?1^It zR+d4-us+x6V1VuCpNHJutr?4Xo}(;FteiZKU;JPGN8EegogPY_xQ;rz*m+#$85Ozz z+=bvccH$V`_xIk1d+xsnANu#di&GCj24hSY!27jP+8X>bFP^-1e7%beCFDHg0-a6| z#u$`@AX!~PyAi^)k7YH>^qR-WBJ*Q%!!&nP)JBE_p%g4~iWbEcj5fgUjZY) zLa&Wsn!@g!gFSQ<8obVy2?9~ybQQc-2_vC3xL~zURVAFP2nAK=VSFA8G43(PMl&@` zgetQH)9{;1+0+dd8S}rk^-cf^oHGc)Y5}J9H0mI59%b4#b74*DU`F(Nc!nogGNJqn;7h!lZ2!bFC1O|gXM$haaSX@B&*inRwi_m3>D^ETJ zlVuP>_#{_1gZ#E1dMEzwPrMKJ+;>k+jh;9GRKvVEcmAxBd+ISj2!T8AxdT7_3;#9# z`OdFk^R-KYaj`b)_kVmm7$grY9{QjDqQ7&E2=MhTHiXh0Xx#^}n&lZr=U>7gOVFOv zJygC+X104dfOR%+NH3(E+(h2;gLVVZ*mX80l@oJGzd1CVIqxXD-`>a`5TMp{;Kf0q zRiq{&`J3D=2qu-lQZ(_Z5>D_(OaT}JUX*i1$jt&bON?P6z};{EK78>bA4DD+iKFF+^nM`QV1WFmt_9C^_yWq^H zTTZ$O+c3DS90yF}Bs)@q6L(gS+*HvgQ~P)~XEGCbuNlr}8d{s`d$dnrtRT9|%|O=? z(*$O`g)rFygN0xmMr(wf9&S8&E0#OUFjfAKcS``=klR_rFi7T^U_R~7(uRJ*@ftKz$t@HQx}1tHMkIn+HJh??cau<|D~V9%IZq({=ILNY33@-*?d}U>J4r4 z&{|{j@&;Zy{W3;_A+**AgOHSQbf`@8cj{7p2*3;1%EQ;Y*btIyt<$n76eWb)GDk8R z>?3wmr`uemkX)7W?2jPLed?ugP?~lAW0jcE6w1RJw(e^3PVAKae!kjdz!8E2-^@N& zW-`AZ8cXWMpk6R5*w=S3R8Y`}!;DKXZ9rjNcBS3#yEd-~84Dc0={B6a=Z$#j&p+)A z2nj+cU~n1s@QvPT#LZh|WsB2z;o4iQ@;CEdWZD`qP$*-3hlARH(^?Bns?I~Wgya{*Rem`0)bZUKPE(SvZ*=|7x!eu8J>)97r`&(wFJ+q5Z9$sQFjTVr6Q1A4d%+KnI%i8M{@ zaJPS9l#JCkdSQ98NECpNz9EP|M?wDaU~l(77ecJW?G{VYF<=dfy*(6gY%mrGckOVB zD2{RY%z1qHgCE8_fAF0+aoq{GsoCd`MW#C9=bt%^3+FF5D%5C zu@4di@-)Nl#x~N?7?iphW)#JANC!)0o+L`ujAN0lu@?OjNR2d7EY8Yi}p)~O@N=0jh1$a?H$I(7}9zv#>Z_Qat)ahbr z^)QYcKY^s*N2lFIZ)q8unZ$VS3i!f03`%7=q1Vt-E0mouYdcfY=l6#&ph>{FZ zR-m&p1eX$J6oQu$%on1Xx#=UqtBp~0Jz5K3Tms3SV{`UG*g=k1gkY2*DROiyNNb_H zoTlU5zp4f1$ub+fp~ZihX!Ah;|LfM)#@iWZf^*IoV?-+rtyN)+NmouBAFQvhpV-^l zV!G6L_~Q@Z^MCXyyzK|yfp`7DyKwB}ad3aWCXu8BT5Ejx|M@WfuYdLL>a|5zt{0vK zXFL-UAPB~WUZ$Pmz)<7wh7-PyXjAP0e&YXH2>1c$w z+nY+lv}DHN8S@9hgv^=cSb!_#c9)H9VwH&LIJ3q<1PxJ^B?MCNx~tg--`V+Xl(lo= zS{FAk)A?9JV8q1T*_PUo1Q?%kys&jOu?R|qvY~e+WPkB%78(M83xQkS^dMe)>|sQ4 zjIEsw497zR-5$i=t1z8IAYAyEY}9a9bB?Skp(dZi2UL%9&rCDKPQ@(1PDGcS)2Vq| zl{*986oy)G?wq2f(x8+;aO$!?ly-xCKY!7FZp5f_m#$Rh+6y?Db%TF4Lw&7$*s3{B z1({XUZ|V~|LA@VaQTk9e4XT}?2NapvsOVWI^L<^koxVnA2{Xhq-M|=W5qAW*ecZe{_oin;GP?dWH z)do~4(8i3Ya^bK9z%X-!jf(@QB!|(e`aJE{<;w{nWaF=Lk$Ey58350|A#A z=4p0L@Zir1nLmB4G<^LJcA?138ljuk+8++$uuCZ=YBa`)yYI)!k>mTC20Ld6KXuby z#ig?~PSM^Qf#}JKPh*boaL4&=jS8sk%s~czkgGz0(aNH;W}h=dnr(8XcBcESk)~+* z%{-7OD-ewm(8}Irx#i-iiDQMCTsb#iPoI^o5Jh|1FYZ9%n|GrhQFzW7c?3GIYRL;llCUx_u2WiZYfLVJkjJey8|?1knMWVTLm&S%Ub}D!!_gR4 z8A6X<}|bJ0oC2m*&Xgs(xfv^yJ0yUwWUps3gKR0U9J<8yY57n72jAW#%#4dYg< zHbx1oEO8+hD9!#l7o9H-w)z16)xGWBzkW*QHS6n?%o70r@^0WG zWs?0s%);+1$Jy(PEL#U5G2<*S%Ft)O^dx!e&mV)7C4?81T0)(roK|))7;KDo_ntFa zOAA|4LI;fV(_s(}2D`f*SX)Ay4P%YgT3fi#v*!q7uRT>{*EBZY$YK+=TB*#7KhCr4 zmar8s8m-Bz4}TU`sOX=D zQ;b#@_jOH`(WX{z;bVf=#DP3b5CkD8cRp*fEFr>hB0$CdZpQ*(b>gHaLMQ`PIUf^% z17J)!I0NO3(bNXH*~)>Zm19MuS?~=s@Ts!nb0#c{c#5bZv*j8XXq% za~>DaM(d(jQQoT##rpHfJxr&h^ZwG>z-UwdF7*ZJEqDZX&}n6IS#2hCSwI+tcDoA; zi!9I3JA4dB?|mcEZfhz|dKS*a9B8X~jaJvyEX%Q`(9c>l&EzF{Huc2mz#g#UnD<`D zeyEN#a@*2Q3u&hXQA)H2V=Qj$fJqn8p#*~pTAfuH)pbcY-ydW9u1$ozp=yicJ%rj*1dg>RNl?R!QP~qpG2C zJ`s5urLhwLyGA1093UGF0Bf;z;T*Ej5P6Xy9rmHh5(_J9pp-!=i7Tg{!P4=Qh{P2J8oV4LVxR9LPPTK?uG&I1pr_8 zj-|CQ4CCwbJpZ1(?d`W)*mWKR*I8>jLI|s*ER8nO82uFh|JzeCPayMaEBrTGvGKc! z%MZlu-ysGkr7Vwg#-N7V_}yRr8GPIOK7jka``uVvJv=oWCFE;1dym^3 z;{t0F+^XVtpB-S>V-&}ZE61|E;ayA%JQSzo15=3 zuuEYawp(!vI_cxn|Hr?;Ghg@&?tkYG;&pf3k5+F1ai<3^f&&NSv*=Q5lz=qu7-B7G z$f2~t#-+>HI(rG*r(eShuRM>7m(O7|9$+UQK&t|w5a{pjAS5P&j<@t}{3M4%N%==3^JB*V+EJcZUm z2MepKh)$p%ia>-v{uK3$J2n?J$Ph zTi&i+A|^TXnMYujPJpc507PvNOzd2$m}p)FtLX|(n=EC{MH*9i@ssFZTMz9W?8fS1 zMyx9~Vc-<>YR3MZG1@RqoEUG`!~|a$tJkjl&a8ECr#2RpqNZ9qf&!qR!9Z&fs&j(& zt_5gxRRHpFANgp2mSAYN+Ad9Uu!m$k#39U&|DHWBb7wR&vp;e`(V4ow?eyPuFdvyF z2G9d6pMV33U-ek6=b3(E&F3h=F%li5tr&-{?4Y~Tuf?I-IObp!#o$bo`6%sMYrcZv zxBkNQSUM~KJXJ$v*N$(kV)J-DzI- z@WSXKuKej1o_zSvv3Gd`Mj2#9in!YaOlWdWrWtTEUH&|O@_XgC4|MQ3pV;l>3B zAyAejiY&!LpZqY^7LH*3@Chuu`OWB^IElEkfR!W1z=fD-VjA;;c}>pZ${ODIp1*^~ zKbiwD=x=SIC<_SA&b*ws)rUg7DUM%Y2F4ZviRLa})H)cePx*}K6n~BGu4D@7s zZ!74GK`B+U+Z;J=0vu{%5p-9;@;<1{P?RMCA)wL(JC`otFCpLLce}r`y1tqwV_g=7Ova;9YBgFr ze5AFoytK@?ph+^e7hiof$;M;ZUR+#&2fi3%C{qW+<#`H98MM}j!Wc5mFc|Eizq9F- z{*)pJLNLx@ti}1WXV7W&(0=|gSSw+9h$DBt0X%A5!YH zG;%x$p}766Z^y8|hgY6|8Y(Z5CoVddbB=r4EwroznAU}enc7Z<)VQdW3WBpbG)-&N-$&`hEtwD(Pf!sC znBbG_oF=8(3qKiUFxEgyRg(v*Zpm6W=jH~KRz)EYN}xUKO)MTkx9mfWwvc6$^Y-jt z{2A`~&Ud(EOzd|ApRzNVBfbQt-N2j7Gdl-zvzQN`TD!lqszNE>WHiDn4}S*b-nOUyau}(QWvM2F zUhjeH000w6Nkl&)@9SHNF!-Q5$ zrbz*I2Cte&<)3}(>>TBwxzD^=k)1Q>pL)F+SZp4=HVg3`)Ron48HAvS0$fP@Xmz?^ zN`NOkFO&RYx6}LG4?g?oHO|$wKDag=zZt$6MR)SUGm|rYudN z%d$f#jpBBT0if0zYK&1)6jP<7HpbA9M3xXRYc17UYi+d2^2`!KWvO(jRZa$jejbJq z*HUStwYIP&fIV53)>=zp?U)deK?|{LR*wLs1m(1wq()?Zp?n3r7wwbQX4Q zUs+y0ndT`!eBH_8gy4`hcAN}`DWx<5r3< zl_D_Pz06Cqou_XmUu|wQ^CsumfI*x6FW8hM=u)Ep+N%(OK-`J}ZIx{0TX7h@dTq(O zc6`$no9cMR>MQqbZvG2v%umPoCbdSpVJMQfd=x}4R+YjR|W$f1R%JMyhR>zsP z%iO>a!8_O8ddn@zaHx~vDA6!S<6if$&2mC14e}x<#khbF0WnsSvMh-)8l@}|$1QB{ zY@*%nAdX@LVE|(^0w#=6#>lXwfe3BL!hu%mLa*KZr%9H)V66ELqU@XUQF15a{IDwI zp@oITupPH(nkLXv;^g`gM&krYzKQ<(B zI||S`--dGv=n1vD-Z^_wX4SEpCSBKB1cE^-r=O_}H|g}&-Ngh<*mI0dGtny6n=4}8 zw#Pdr7;PZI5sL^!M3Ab4-~yd?2c<0Wl|T9e-2TAZaQOP0zG|VEH~7EWOk`fcuro#C zz|hF66fT1KgqnjY(`?LovtU*oU9qzb?CzkD1&T69k>{orwflB z;SKE`<%Ezj&lf<5CWIIlF>DZskVqv#D3P2CY2#3B{Gw zP-34<$KN#k|9ip~=ahw=R-5KU&SajoL>S)`Me*y4vN*yz@y%;%hYu4%!o9uS&hDkl z5JK=?r)R@3Y$sXT(pquC7&X>%Ap|+Jw33fULroYZ>1do%Lb$Pp2_EbcLjHgd{H7)3 z!b6wOnRlLe9ihgyy6xVZIOlguCBIjQ;OOz!-FRb>W{e3QWmytrc?PW%%2FcBQsj97 zLpTf%VR?k8yA0b|K^ZT?#yv0a#0*V(;#KDKIg=2HiitK!wWKah)mTJ=fpS)-fJQ_k z9}iq652Ikj86Uc9Lx6x{bzuQLPEkndX}t!_YEVK^Dvew#q@{#57Hw)E81pU$fYAzx zRIr09D0k00y0c$t$H@qaQhe7>{Vd-7f&XgCmS(ni%(5miXa0f%ijBQWQL;1NNXXY# zbjX};>#LYvOw+WR#YQWf`Phf?~29RS?+Fb|GQSJ{j**qdF{A%d~?ER*N*Aq@$sNaI#4KU zRAM3`yP9fwbbuh>tLOy&@2CI5cv$c+e&##A|I6Cwo|5I;D52|C+VACp-_q?Y9JW$- zC}$xTf@N8XPAf#K-S)+SBaYk9mZB_6$g)Hb1P}oO4+7++gq9_OFaV(;;%*0}(mqQY z2zVO-rLZn&3Q|k-hZm8g8FsceFxt8Zt`&OLfGB_{0;(ti+J)c(Cb@!6yoPkRf!DT6 zoY5M2k%3bNE(DTvjBclkD2QM!(284FUR;Jnfj|%}EiQn7Aj@L}LLguP1moxr_F%Mk ziKY}~R^WxteG*5nzZsq7Lz9J=V%pQJnu(Bu;{lpQWOjB!u2N*?xft0woyb>%@TPub z!*Sg<7!0*&6@cEjf}O{|jI-xn!p6o$Xk$z|PPVm@zjv+BTs!`{4|eS%_|`feIscM< z^2+)A>8(qr?>lz=;{gvozSv!St`oIi33*73HWb#5m6Swqq2n;tM~@uG;^H!iM2I*H z5Jw^Dv|Cv0EnxM~GFqWPD`04c0=-Tf-F6F9D-b2ojY2GUJ6K&_!eXz7PTWGb*TrDC zgVQfPiwl>}q9_xWMocV%K!9`ZvpOxbarX9Eo+3>Xv|DWqlc61r`ozK_FEXpNw3dKm zX@XHQvg34Q`@=r6A}6IRtTqa{6Ihk82o)ui2q8cWDZ(JKj4>970D=opLZQb)6nTo3 z8*YXOqxqc++3)TQ%y%}Q?QFBIQR9x%FKvhzJvI@_o&8W;PmqtZCu1> zzw{}jS%M^QVelQfR)G`45PGoHC~@44rm z!(tqhI8F#sp&^{#X5P|_=A8TKf6o7WUl>QDl+e%=hKoQ|9C4A!bKC3o>`=7^E(Ifm zP{tTZvkWHJ1R#Vl=Atk{6qm5N+P016ve7m#rrw>@oz)egb*4Z_p65C@g~_aSg*8TS z&I9W#wMCHwBqxMgDmdyx!)9UbTt+B~qjDv0HyYMjo2wv*tSzk3c|X@Wb=E=#K^cVT zT77FVQmJ=#jqf%)$8XQ#s8l!Bh-S0#iZ*(i&a=FI?>^l5 zxhEi$y4s)YvcR)z9mHJE0=kjP<2sej#){`gm(i{grp+A23xjla7MMK?*KXp-vA6K@ zYrjXD_H}D{`H<222a_gy-Omt@7aPw<&JVx*Em9N~f^ivz(H13xAt_}!%hK(sPVWIo znCJSwZf~_r2#JSlTQZ$%&N;V)&>{$exKgVr7RdVI-24h9)KW@&X+K$=U6|EV?@i{5 zOAC6fGL{$wQY8TL^(97-0B{Qb%&T7pPy#RupaUSqpp%dQ1A8|NGVPmd3yWgl;N^zhL)8-*=e0BUQ369ZTTAq_%^9)$k5LHN(0 zba0|va)%c`bpMIp@~iJY^)LZ>I!pQw=UNBG7ziOCl!6)=#nwk3Lv_to=~@^2%MVX( zCJ>tis$F_*jc>X(2KV*vH8OrG+g(*Vo1|e*W3yAtKhClTfO1t;0 zCtF@?NzA!_z1MPuviv)n{Tnu1HaY@aYY%6=;;{~$3-f5qOk;WSFzTfU&;{Z!#=_zP ze)G}`&_%JZJU90uK%Sj6*=avAJU0uzLBMlM@asc=TKMWCUpV6&CMcuhj4?5YxE7Fb z$miz4l!V-}1!NP!xm2NC5iP#q{>IN-PaLl46tJPPy5iPtZpc~W8p~&9Fn{zdG!Gv{ zrRXE%3@aCxu(Ws)A1+Hu~^eAY%19sP4AYlX+hU+P#m#1@H!fxn1yU}F;w@Kx*v8{UD zu+v>FJXmwu?sry@wp*Bg`yfbb2}*KQLxIlHGMp{|2P`)iK_r7L1+AsU#gi9LpLyy8 z>^pumcy2cQGMeX>JUcv+zlB-2%-o<7=bW;&53d&9cS1?ST^f=yXw8xB55_TaO4oWXJ-&H2PGwp&R}y3 z7>hj9$aMy7bFfHap?UG*UtW3nxs` zPVKMxxKYMY9;(Ay3(7bc=hqdUjjqvGOh5uSQy^`xAZae6HFFZC)dX}Gai}0DN7hY{ z_Ij|^K`I3mh5#oxacZicS+j8P^;dqjJbV5}Qzr8;LOYm~AP z#yDl1amG0f;}}8;Sm)LQ->kK;MKRcrHwLT6~?Fz z)p5tpJ2A3j7c6H0D0ND}mBjGaZ4hCA6Vu1r&%f}CR~OHnd6o(F+EkwT_GXV48;|GXWS@$- zD~g=}z6szngpj?IQlg@WMWqrGLU0f;vTi&&aIVgCJ%}(69Fz=VyBT8xPq{VMYrugA z5$?VB0QTRrA5=)BSszXaM77~T8f=lnISaxCoH0mOTF8?wU>)k!D(dAJFcwNk*uo;s z(t&3j7vPKmi~@?mav;xh{oc&Gr~dHA-~O_3{`}7;O}6O&VUHIZkLPBqbVN{vb6*7T z5P$~=p<5VdCBj(1IS*ABs2~UxWz6X;1tG*)Yn?R(V}dg&C4fQdY@TJXg$3gbQU!Q$ z?dfq!)pO@RI6F5tdvyNP zse`MXrK3@OBstVv^bG(WFE$>}|0?@SRpPW3&N+sGh;TS301N>bUlXJVz*YeJ0dPXd z7D7lsIp?LJp-?JC(Wm36h&PSb zDx)#VMMx+K3#}ahO+X}N>`rS7Lnswes#s^)Xb^;_DHAjO-s(zW%{T_Waf<+^oV6J! zy+DP~$NOEsOy==o<{907*qoM6N<$g7{rr0ssI2 literal 0 HcmV?d00001 diff --git a/launcher/ui/pages/global/LauncherPage.cpp b/launcher/ui/pages/global/LauncherPage.cpp index cae0635f..bd7cec6a 100644 --- a/launcher/ui/pages/global/LauncherPage.cpp +++ b/launcher/ui/pages/global/LauncherPage.cpp @@ -351,6 +351,9 @@ void LauncherPage::applySettings() case 2: // rory the cat flat edition s->set("BackgroundCat", "rory-flat"); break; + case 3: // teawie + s->set("BackgroundCat", "teawie"); + break; } s->set("MenuBarInsteadOfToolBar", ui->preferMenuBarCheckBox->isChecked()); @@ -424,6 +427,8 @@ void LauncherPage::loadSettings() ui->themeBackgroundCat->setCurrentIndex(1); } else if (cat == "rory-flat") { ui->themeBackgroundCat->setCurrentIndex(2); + } else if (cat == "teawie") { + ui->themeBackgroundCat->setCurrentIndex(3); } { diff --git a/launcher/ui/pages/global/LauncherPage.ui b/launcher/ui/pages/global/LauncherPage.ui index c44718a1..ded333aa 100644 --- a/launcher/ui/pages/global/LauncherPage.ui +++ b/launcher/ui/pages/global/LauncherPage.ui @@ -386,6 +386,11 @@ Rory ID 11 (flat edition, drawn by Ashtaka)
+ + + Teawie (drawn by SympathyTea) + +
From a5051327dbbb0b225e6badd05921d445d3022c7c Mon Sep 17 00:00:00 2001 From: seth Date: Sat, 7 Jan 2023 03:42:15 -0500 Subject: [PATCH 052/152] feat: add spooky teawie Signed-off-by: seth --- launcher/resources/backgrounds/backgrounds.qrc | 1 + .../resources/backgrounds/teawie-spooky.png | Bin 0 -> 183698 bytes 2 files changed, 1 insertion(+) create mode 100644 launcher/resources/backgrounds/teawie-spooky.png diff --git a/launcher/resources/backgrounds/backgrounds.qrc b/launcher/resources/backgrounds/backgrounds.qrc index 7ed9410b..87e70935 100644 --- a/launcher/resources/backgrounds/backgrounds.qrc +++ b/launcher/resources/backgrounds/backgrounds.qrc @@ -14,5 +14,6 @@ rory-flat-bday.png rory-flat-spooky.png teawie.png + teawie-spooky.png diff --git a/launcher/resources/backgrounds/teawie-spooky.png b/launcher/resources/backgrounds/teawie-spooky.png new file mode 100644 index 0000000000000000000000000000000000000000..9c57103e00832e2c2c676f3852f37733c7086265 GIT binary patch literal 183698 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

vGlKBaF$8sZ)(H2G8}tS`-o^ zAnXMsX{HJ0`##;Ei;@bZ9D<-nk|gLfB~25;C`1TJ((N!YJzpQ^sJ1%WbjA z`!KSU-H2gUnh?Ww7ez=xKWj_l7~_CI91ub@$%Z}0F$SD*jN^Yz{Kr99XZC#k)u>5CuugC z%pExfN>FRmaVu3)ono!UT7$I~AtlCWW{zG%-0iWszDgWKBw?4HR!rUeTHJ}rzQjy! zswb??{P5I>7HQ#uRAf7aWlz77_)qcF7A zjyVHkz!}Fl{yXCX$6ukMI6B@71IJo(t+n=+TBVlMYYo5C>(sY*wi>PGBvOd3@A*n= zooZvYeb4W7dhNtyTG2OV+>$OS0S1GkG3M00*Ht{`=_4N&2dh&Vd z%?303=4iGis5ENK9^S{?{2Z;R7M|zhdJeUE9VsQo8j>i%T8ral^B_uPx34X)apm$A zHn+C8a&dtRr_ZsuwoYehgGRH-`sN0mogJ{?Ixdw;l_ZzM*HKEZ*P&KxkfsT#Npby3 zJ`5=lP9{Jmm7-dy;yE5+*ux4zl!RCTQ-_c6jt_nxN3OpS->*<_Oi*n!5JC_HJ(iXi zNRk9&a*z?3HFBY%t&L&s_;Fr->PaS=lLVp-7J@kCz{KPr3(P@48&DD}L70+rXW3G` zNOXo|?8I>CY}EV4mNFw*BKR4bv7wcqCWI7@<6v#1u{N>RxZ|s53^?N$$A5FY%B{9~ zLGPz5ywi1DiJ%^*aVtvW3PHE#dY*Ua&`~Ff0v*ROsnMzD`NqMDG);7Bv=v51LDaKS zD#AFFK;l=cUbEhsNll7C5OlkAwzu(ppP(^8uhV0FWu58S8KhLK-g6&mlF+D63?>tW zz@(a}7b0^t&~B%V@*KjThej!raMMk6+FR5B SXYm@Xep#@u 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 Date: Sat, 7 Jan 2023 04:13:53 -0500 Subject: [PATCH 053/152] feat: add xmas teawie Signed-off-by: seth --- launcher/resources/backgrounds/backgrounds.qrc | 1 + launcher/resources/backgrounds/teawie-xmas.png | Bin 0 -> 200137 bytes 2 files changed, 1 insertion(+) create mode 100644 launcher/resources/backgrounds/teawie-xmas.png diff --git a/launcher/resources/backgrounds/backgrounds.qrc b/launcher/resources/backgrounds/backgrounds.qrc index 87e70935..dc16e788 100644 --- a/launcher/resources/backgrounds/backgrounds.qrc +++ b/launcher/resources/backgrounds/backgrounds.qrc @@ -14,6 +14,7 @@ rory-flat-bday.png rory-flat-spooky.png teawie.png + teawie-xmas.png teawie-spooky.png diff --git a/launcher/resources/backgrounds/teawie-xmas.png b/launcher/resources/backgrounds/teawie-xmas.png new file mode 100644 index 0000000000000000000000000000000000000000..55fb7cfc6455c3f996d5fc266c875141155a8f47 GIT binary patch literal 200137 zcmV(#K;*xPP) zaB^>EX>4U6ba`-PAZ2)IW&i+q+NGUamgPo{W&b&f8UpQY9F9F`YxN9z_`NsXr7Ed1 zD_2%4qzG5|bzag8I5-Ce;9l?l{XggWkN^0OP~sD>Tw1SBtvCPVCqHrUMZbUk8Q<^W z&fo9fKhOOAD*W~DuMvO0^v}ef>E|20{`?vK`1{`<_~Xwo_^SMu3-$H;jrrGaJo)F( z*#CB+-#1F$QRIvLd!gs|LMi@yBm7=q{5;lOpWgoKBh2^b&)=W_SqA>Q{Ozpx_;+Rg zeDCl7pBt;U3-6O+d{am_zo!&`ZXo$zywdO+{GO68yi?|DzQOM$_1`;TLH^M?{rL}X zdmV2z|HBvkWmnGp^Kbw1_kVnK{e8FcAG?_Tw#fC*KmXz1U!nZtUHO*&zr1tqf11f( z{=V)se(vaRtN$(j+5LRa%u|e9S?c=xP=7qi_Z6?>fwOPjFK(^gjsFUlXy31gUmdGn zn8Lo-J^8i56C%5ELJm8OaKiijxx(TWb9}GZ_#(y?)AyG>H5_NLdx8DqS2(eyC+*bP z$@Oh}PVvus3E$g3_qx7!>%8+0yfg+b7QXHOH-BCK?hl-!D}-?WEU`0Vh7l@kV>fGZZWnHSjf%>4Q@TA949*vN{O4Bj5(#s-t_2O^CK;MFO4Pg*B5^e zn6VQj)zsLVCk-|$=YsvT+_9liQpu&1T3YGlDWj&EYpJ!iDkzSYTE1FowYApU(?(A{ z_tI-`z4xb&Cq4<#EKhyf)1T)VgZCUT0X6 zXN?_q+Th2oyY0T8J>KwQN^gG4Ti^Ef_j$*`)K0oiKIPQYPCw5Xf9_iNb=$xH`Zsnh z{O(%(cFGsFKX;A4ZS9Xs1mUE(XKWZzB(USnEx@3odv^Dbb9Cq2v-^p7MTy*GanE*e z$JoLAhFDJcWACo`v!m|Z|JS}qLEMeDH2+!v}?~~HY6Flp@xflmo+{4PO3jP{t zp0pk?`weC6v+4u-o!;Y#rKc6KZ&z3y6{4G@Pm>os3~gxkXef%P+M8m)~T=6m`x z`9aK!Z!WMtoebXB#l08IAY##{JbSfRX1v}M$G|#Sh;`Cwyn8eLA8i2dCs}`t#5F-V&LswnC&~mY=bbA`MEthmr75I zi{)7}q&CXUYjeRM!+y?64-b+8%kbhM`P~_;n`yVY7h5Ioz%{U?H3%K4uxOF2_W>&d$SMN%kyOndT08>jTQ zvu0_o2hS-13%>WtnC7|j0YlmoaQuQnj2`+6Ex_HP>W52z$dvb>>=OsA=ULlIl0bIu%*y{U=SS9uoXJ4aE z9R6vOTZh*0P~5=U1A`(U0B|Md(&s8|wD=A{Z}!Rx?G7nr8@L!3-Y-v<-v_8_W7Hll z3}bWi*jogia-TTj8$mAM8@ISY%*AcJg~td0-ozVscy}up2Tt5;cy4U^y4!dg2FqIF zlxeKfbA54(G)H;M0NU4h-|%3}ligvh)@VJIU4RH)IiC>*yVfx=*0T`hxXl2*JV!Vj z0rd0Mg8#d60-HiF;{lZiCc-TN>h0y3-9zpu=^P21$qm4nrwm|fcDxLrX|>|&k-J;9 zulHRr3StXhhL-~p?ZGi{VW9Rr-(E%T_Hg$gP3CuiJtpuk>ABu; zFD_OX|2eqM;pcHMSn)~yu(cQgAlHeoM4*RT1t<&8VIatSC!L4*L`Vn7+l9-5iM)z? zO4v=p_5e_5$7Z1cDdGX7oK<=&zKXLaSR?SXtJ}(M*06R77XVdpavyymf#6IpqK}}* zUICF<0M~iB6iaaGQh*x8~}75Xuhm}3ZOn~xA$EDH(ZhcMw~RXgR{hr*$y1t;KOrc zg5W;tUjTte5fY$Oi(O0X#`%#9i?6|=);9M-jF?qwcUIYQk{%;>RSiKb*I*S(yz$)x z4u`of=>{M=6DzynGy#w;5)QaZKpf(PMF02z;9wp`Qwhi@uf0|`<}IwBHQm?kGvKES z6WcY($##!CdW14bhT_B-OYLg-#(ivg!eW<+L4klfebX2&AciMCco0U1klE**{0kA!0{}3KpXqSA9Pfvmi>5-cKqu^SWu#M(q^ z=}ybs_A@r5suy-A!foM-Ab4;49s4Fs3?dG~kNZS7%wkTU5x99R@q}%|;DEUPaRPM7 zi~ve};aG(I=a`^~^L`Ok1ye20wO+)*{eTb^dmjtSg#!YRu!Vso#YzChPE!GjXNV!?}kS}V+}n(I9zhVeOT=}cg4VqkqJZ1pia%W z1JSv%Yki~zC#X6)ftt;262#EDcC+zfdRJZrjSnPqdV6PpJ zgcCp57cmQKj~_$YH5UA~^pWq)mXC>%j2pfb&`vM+Ox(h$3%iCtmQ3&?zTw>N2KsU- zf*%8FBoN?Bkf;}~1BfRmFYAZg8ZauUf%%x{f*8bd?l;6~va9a;k+gb`!Q%B6wGDA0og3#o8|++*yw$rVe4O1TdW+yNOFRKkB|u`2AOoC+EkCXbA@IdSkem(T`4aHZonSsh z45che3{0Z-h;+e#^N@f)s3TOvj(9{uw!(9PFz9837L5G`H=ydDhrK_;-wRCgw#@ z2(%*nVcaOFcIJ%O6=aWXc{46lE;gk4RPZ-gec%ROjJ2Yc@XgFOBdQr% zh}H)gVQmJ;dp`)jZH5MloIsNYF7=8&H~?P12}3MZdCb=#BbJ#t<4Ab2T1l+faCoBb z3m#$WT{!9qUN!)qjWXuJ50<<5No<}aCmtt=0o0F8X4f~It30OEI{=ijT7(Gv<_80R zVaaeJJOsRF?-1wwDo}bv0QZKxLPm`o%ye{igg_4ICc=3xg80CTGGZ3<8mh=K4^Dx< zgFX?V45{f#Z`8{ivnM_iamt~^9q_5x{I25tb4i}iVLaDq4K zM77Q$Lkqa0!4F4(u@vYN5@cTJ+|FVW#L+GIhtG4Pr|%Zt!-j)MJlBS6Oe6Etv79|k zhC;zJwhGyuaNtYNWC0p^m~e220WF0L zyz0N$+lCBcRRB5+V8dqEW7CJ|vxYmY0}n@vR`@Bp7sZLA+)&YgH!Nf-gcyY&bw-(< z%wmMfU3sj4sNsuK&Vo}Qlip*}Y~1YZg7~GFS>q7)gKXesP<@D2#5=KF*zj70a&-{ zxv=NWmSETLk*+Y<1k?~!jN-t1*8vj?MAZR>V+mow^Jdk zfHGn=;`6;(PN6l!?AdQbHum_G`^&XROe>5Eh~^98V8$TXN+5*z0_%wblwuJLJ(w;; z1RTePfa>(AC8#ps1?tsQpD(AoOgJJ8FSO$gzCy#ejfQ9-EEc2_OwDvP_B%5!hR!~$ zJ@^;HcmX1W@BQ3Zig&a$>9Sk+tM8o1C1NQ&p>=F*hs7DO)+iW^p;l=>wyq4ppeAs}Sn8$iBR zMgV|k@6!T26K0M;LP%kj+zIXrf9akyC{PgnJ-;&{!g1JTVjhx=Qh{h4c%Tm-n@Oy_ zPxj8zZQcpIPh5WRJ8wax$1mY$_ostdG{7$znYqpPY8m`O>Uaubs;Q$x7BL2gUQiUE zfza^my4Y3>hPxfEZQX*H8RTW8ukAvgff}3F!>J(%Fu><=jitaIjb<4L=m1&_Qv!-e z;#9}6#z20-!ZB?^HcnwKF>F-WU~4_?*l|7*B4(aB z%*NKFR42NzJ=N@d2r}n#h;XK@WrZLJiVnh#bs~Mzmw-U7hXds1Jqp}BBWKLAzIiIQ zVTj!Eg1`c_uW18T#2pT}IVDZmBExuNi3av^=P!0CV-^vA|cmfB2Rze_Z4oP}Ta{vh*a9(Cs zGO`#%OH#{w;nUG1sH<63Ie?%!;{6b*UYNMhRfHD;hdNB_y{`e{)!|7AaDWRn-mQXW z=0h^D0-nJ|Ur@Jw8MY)vm83I=f7CK$?(P@s|J%D5x(XI7qXerb#O=GIv%0BuGj zm$ZlNVkrS>&0ZoddFC9YKzmZv~EF@tMmXxW`EuQ~^Iw%;@@5!oB9<(TH3u zKNv|c+Q>{-85Tm!enM)sOYzUootUftGra!w&+9+`!aO@Boe*x2(0E`QDquJS`^63H zMx6Oj9}@4GJ9-xn_zp+XeL=1<5m*LtgIF{PX^GJ3S)gNk%_ z&RAoq(Bb(D zUdKCcsU!+n+k@7?Ksrzh0S7#=mB-Di`CSI~^knp-Dg#r!$9YhoW_z>e4E=a&o*Q62 zu0Du_NVG;sBmUf2FYeM^F&;HVBJ1PPDhNKStcH*Pm+5XkPPGJVIsC=%`!Uf#n0wbI z((_0=E*M3Gv~%mGnvvo9WdzK>aapoQ=^4pD~PJM%Ja128>=qhn4PqJdTiCUKy~ zx?o=c=ziOccELq-u_E>5MtE+vlZg4Cj?#BDkThO!GJGcp9&ilLoZ=Ypwc*+RlPXka zbJuJpA&EE#n~)tMcJF=6)(qrQEgEvbkn1Kw5K%jq#@jp<12YJ>tUpV4m;Grd&<)93 z50Hg;71&UM=+NM$kwg1THR}eUDj|A92HEl#?05tCIn+8(7UF~cn+uB8qJnX*PYTg zEC-1hnu-??Ot3JPT8pp~n%X zggqaI@(Pj9wuWkplBjP?ja?&tu_U-gHm8E0fSl&5EWPlo0PJFmd;hY}k;gD)ksxId z9v%UekhaX=>d|7Z`Ee|$CA$Zk-_lzih|3XcSZ$!YgAfg`XSrSinCh7TW8RG0WU*OV zH6;4lQ`ZRrf<~R^F)aoigT`$FuZlQ(_;|it&-66*AyUGQH+&P@fs7(pqoVL;qj@3k zM`VHaxT%BZgbtf*jS;`?U!fqjKxjuv0QB5FaFoV%6AByb1yX2s5yqBbyDw6NWgQ>V z19C5WyCYuJaN~UwdJ4-AlS-SLe;N{^5btM!;aR+}h3(zU!vG%I&-P4KS=kF;6Ue+t z1r`bq_b0-4HXQT5BG)&_S$}{$TAnFdR>6zE{3f1_k!AM@#OFFMiim(%Rkww>#N*tZ zC}IU_$UODP-?2}Ja$=$n-Yu(nL*?0CaQi#FA3v3g&7ca`GxZJyzGD6)B`To)E75s_)rK;09dzwn zVaQ?ttw?}OvBO2-i%?n~>xIxuV+Weq%rx62ieRjGJMoJk4b4F^5)aqo;hkqZ=>CO; ztLkww#1Z#tZoISJ-EhMCzUpo);?%%iK=we;R~CYV%qB4n%I&!VWTYdx6tgV;LH9v# zCvd*GHIr`}UIt#Uj;npN+X@Dcka^Po#u*vbG21MF>LaiY6#S_pB z23cytrelNJ0VOcrF4gcdMmv9f0EI)=@T6YRC-X?#WW;Ka-#2vE~ z(3)cH01wJu0u)@x+7mF@ydQ7mk&gv=h}^h{+n#-JOW_^O8cYBhX@_EjkEor^B&;4+ z7*GfP>E;+c7x>Oa>TQa1d7d)12lPcpjfT+$R1x%e?EJ71^}X>GS8Ltfy!=98+#QCO ztYq1)Mm8&sy?|YthyneCwj)^iE8;7$JhMHwr`-`9ngVXy5@OWs3%nG}x^ge*0%Zd- z!Gj;%7a5!&F^HJ=@uZaBdQh&`FSmVrZieEwBoV??P2KGaJSJ|6+zKY1xc}6j$R8IE zyZh-DM4^Eyc0H|~LMqM^|Evx_WGJOm4M(+mB;zI%p{Hp?w@^Be?yW%^9LBh09|2ke61 zt&(lP2mu&Lw1q$doW&1vPA}U&iDyA1e2(D#Yyu-;BEzP!Xp2jZb$+~UtXEu*73$Vm z5sN&+k#(3TWc?9tsS(jS8bhi#zdZeuw<+@ppM3^khX_pbY6-LpSj({T1+Uy5o6qtB zc*7H*h^s?kvvrXJ1`@N!g?h#&)X(o!`Z0a@ekV4C`#^u~d`xOQp#hMl2S5zAU#%I< z9(^fZ@MQ|&9({BSm<3-0Hj?mv^eNa?SZt4R`2{2?ifRnC$rlc~iVH$6Dufg6+F41S z0+oj<>v>+5On_2>Pzy?!Bs%8nni+}m;Ir%a79eb!WkAeJSz^%j$+?jaoy3)Tz4z`M^naR_I_T!K`Z<-iTTH$=wxU@ z7C9_XL>K{;PyRwF0F|$4F0+{v�`WSQPQ;C65;EEXOBnNa3Qs#ue03KmYC$MR%+ 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

Q)XSK+j=lo{LQY{OR87p#59dW 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 K?ArB) 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

ju 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

RtpF@!w_^YdgtTi@ z3WUhiT8Ao_)C-kbmKkp>t=X;f5BGnyOE~s}RIvN}Myni0A&|{)l?R96UAh&cciahX zJJ4&i5wBcq+N>_}uT%ussJP!*e_18oxkg9Z%9)q-(rcantFyCBHtLuk-vd2;0$e6I za9}Tf^y6I!f~IaYTgUuH;~xp{y%!T zS=S)MKg}HZiGw`nuJlG9w{!D3guhzOp%9t?5z`20c-GlPB52Eiw z9|7U~v}k2564=Ng6&pk-r$sL-7x)@V&Z|zujYLXTp7|<4?a)EgUU(WM!C`{{iN<5k zJcfg>?$wR@^3kx>dJ@2&OeE24zQQj^!r>;Zb^j57+gA`udvnj$$;_f7%F!K5u>BQh z8_TBvJOSX_V>|l&_k?J+KU8y(`YilKmiueJaJoVEp>u;+MOyfOCSN`|Ho|uv_{%PB z^fy4sFB@Zq+A} zdyo#ZUfS_%-9akAYKIfrvFtU{j=ae%rS^mD_K!Is$2Y2(lXvLNSxBAdzKOKpPuEG2 zt(7t}D-OV60H37g-}$SHwANoIjC_)S3~6Ji>!$j2gfe)ST!!LJH-os| znl*jeVS+~12)WnDbe*QBZozkdQuhUo1oK}Rtf^GleX zsUnI)96Wju#Nk1s(RlI1q1V3ja^xS(S3{00x*;Yl{C@?hNn7t4OSrup+d+WJNM#1V zG*VxnU0HT%96(43k~6(7pL3$w{!nccz}Eo$8q)l{T)~FzTyd1Wp&F$O#I0z;L2|Q*?Q!(##xwzA(p02pnl>g>aQJy3kBQaV1%NzP)8C; z7^6|0okBu2rWUKy`^Lv#Y%MJQEh`qD9ba0=H)c6H35S`q%lG#Hd=hE>23ZA^ z0Gvds#dZW~*IAVj626^z#dm^eZb#Zpwjb&I%aM$57PIp!L|OzES@`83?O(T%7LW}9 zMF92Gjy#IV@zdd>mA(!a>O(an@|MYXb#Wh+0 zxHBW1D9aq}%r^IX2@n$keJ-;|BtPsq-p|;^?JEKEdDkm{x?Jh*TbQc~%eJY8A&haj z1us=d$Xdk+_h78LsiQ7m`C7!BL9!8k|HzzF(AHbcn-PO%8vapBl5 zsFgmpJxkQtEMPjSl1Ugrg#m);DagfnEFOLZ0va-kz@b2N=8~cmWRgHw7Ro){V2;2G zFFjX(?Ty#>9zL+|pBf8`kNiZ2asG+tI19qD0jRNaJe-*mO^yo^*Lx+Z<9IuO-vsap zN~uQ~<6H<@D3z$~*reS6KnaCX$tm?{lj+9^AwVdDRtlgD+;+i*0A(E9aj|LiGJNdk zK7mr-0Hjt>jG?gUB3NDlgc16c#KJ@bR_fA@QHlgIzg>Z-i{(tItC{&SqS$cZM$g+n-{ z3eiD$Er7cKe3}q4z=RNv=TXbHZAz)2lrYW(Z!1nmQEK7`04h-+lt2gzT5BXKK_Vr9 z)W+c2t3QmbqnE>ST*P4jOW0tHAx#%lRA6%pjB*Gepj7H> zWZ4dE*98$2#O7!O%i-}8$CoFM9ea7~KOg=3FbQ`~$S}-5*&I31|} z5jH%x0M0q0I7X64NGajEE-cFi=K{rI5u9-(u>_+O1+NI(vQtq|!C@B)FcgbH64L3} z>Dl8aj~$(wsXjh+;FVpkPac|l^-XJy9655L$&urH$ImzgW|XQR^+)OgFkp-sCWKf> zRS$ZRwuf#4Fw7WtL5WK#HIz|GKuX*a!m=IK)zvLZ#Y)m_G?+G8Rf=UX)IUUoWkYLC zjWMuLS?2lhXHZ2I`@RzZ;BSJ`t7{s>c zS+?sA#!-ZFrOS)MFoA)rl)4&@c00000NkvXXu0mjf^(t0$ literal 0 HcmV?d00001 From 03b75bf2a98edd4114be4799f974bb10fe9b82c4 Mon Sep 17 00:00:00 2001 From: Rachel Powers <508861+Ryex@users.noreply.github.com> Date: Fri, 30 Dec 2022 18:06:17 -0700 Subject: [PATCH 056/152] feat: Import all the things! Signed-off-by: Rachel Powers <508861+Ryex@users.noreply.github.com> --- launcher/Application.cpp | 35 ++++++++---- launcher/Application.h | 2 +- launcher/CMakeLists.txt | 6 +- .../mod/tasks/LocalDataPackParseTask.cpp | 4 +- .../mod/tasks/LocalResourceParse.cpp | 21 +++++++ .../minecraft/mod/tasks/LocalResourceParse.h | 6 ++ launcher/ui/MainWindow.cpp | 56 +++++++++++++------ launcher/ui/MainWindow.h | 2 +- ...ackDialog.cpp => ImportResourceDialog.cpp} | 19 ++++--- launcher/ui/dialogs/ImportResourceDialog.h | 30 ++++++++++ ...ePackDialog.ui => ImportResourceDialog.ui} | 17 ++++-- .../ui/dialogs/ImportResourcePackDialog.h | 27 --------- 12 files changed, 150 insertions(+), 75 deletions(-) rename launcher/ui/dialogs/{ImportResourcePackDialog.cpp => ImportResourceDialog.cpp} (73%) create mode 100644 launcher/ui/dialogs/ImportResourceDialog.h rename launcher/ui/dialogs/{ImportResourcePackDialog.ui => ImportResourceDialog.ui} (80%) delete mode 100644 launcher/ui/dialogs/ImportResourcePackDialog.h diff --git a/launcher/Application.cpp b/launcher/Application.cpp index ff34a168..581e51ae 100644 --- a/launcher/Application.cpp +++ b/launcher/Application.cpp @@ -259,9 +259,18 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv) m_serverToJoin = parser.value("server"); m_profileToUse = parser.value("profile"); m_liveCheck = parser.isSet("alive"); - m_zipToImport = parser.value("import"); + m_instanceIdToShowWindowOf = parser.value("show"); + for (auto zip_path : parser.values("import")){ + m_zipsToImport.append(QUrl(zip_path)); + } + + for (auto zip_path : parser.positionalArguments()){ // treat unspesified positional arguments as import urls + m_zipsToImport.append(QUrl(zip_path)); + } + + // error if --launch is missing with --server or --profile if((!m_serverToJoin.isEmpty() || !m_profileToUse.isEmpty()) && m_instanceIdToLaunch.isEmpty()) { @@ -345,7 +354,7 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv) } /* - * Establish the mechanism for communication with an already running PolyMC that uses the same data path. + * Establish the mechanism for communication with an already running PrismLauncher that uses the same data path. * If there is one, tell it what the user actually wanted to do and exit. * We want to initialize this before logging to avoid messing with the log of a potential already running copy. */ @@ -363,12 +372,14 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv) activate.command = "activate"; m_peerInstance->sendMessage(activate.serialize(), timeout); - if(!m_zipToImport.isEmpty()) + if(!m_zipsToImport.isEmpty()) { - ApplicationMessage import; - import.command = "import"; - import.args.insert("path", m_zipToImport.toString()); - m_peerInstance->sendMessage(import.serialize(), timeout); + for (auto zip_url : m_zipsToImport) { + ApplicationMessage import; + import.command = "import"; + import.args.insert("path", zip_url.toString()); + m_peerInstance->sendMessage(import.serialize(), timeout); + } } } else @@ -938,7 +949,7 @@ bool Application::event(QEvent* event) if (event->type() == QEvent::FileOpen) { auto ev = static_cast(event); - m_mainWindow->droppedURLs({ ev->url() }); + m_mainWindow->processURLs({ ev->url() }); } return QApplication::event(event); @@ -998,10 +1009,10 @@ void Application::performMainStartupAction() showMainWindow(false); qDebug() << "<> Main window shown."; } - if(!m_zipToImport.isEmpty()) + if(!m_zipsToImport.isEmpty()) { - qDebug() << "<> Importing instance from zip:" << m_zipToImport; - m_mainWindow->droppedURLs({ m_zipToImport }); + qDebug() << "<> Importing from zip:" << m_zipsToImport; + m_mainWindow->processURLs( m_zipsToImport ); } } @@ -1054,7 +1065,7 @@ void Application::messageReceived(const QByteArray& message) qWarning() << "Received" << command << "message without a zip path/URL."; return; } - m_mainWindow->droppedURLs({ QUrl(path) }); + m_mainWindow->processURLs({ QUrl(path) }); } else if(command == "launch") { diff --git a/launcher/Application.h b/launcher/Application.h index 7884227a..cd90088e 100644 --- a/launcher/Application.h +++ b/launcher/Application.h @@ -303,7 +303,7 @@ public: QString m_serverToJoin; QString m_profileToUse; bool m_liveCheck = false; - QUrl m_zipToImport; + QList m_zipsToImport; QString m_instanceIdToShowWindowOf; std::unique_ptr logFile; }; diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index 8b5c63ff..a3520e72 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -841,8 +841,8 @@ SET(LAUNCHER_SOURCES ui/dialogs/ExportInstanceDialog.h ui/dialogs/IconPickerDialog.cpp ui/dialogs/IconPickerDialog.h - ui/dialogs/ImportResourcePackDialog.cpp - ui/dialogs/ImportResourcePackDialog.h + ui/dialogs/ImportResourceDialog.cpp + ui/dialogs/ImportResourceDialog.h ui/dialogs/LoginDialog.cpp ui/dialogs/LoginDialog.h ui/dialogs/MSALoginDialog.cpp @@ -992,7 +992,7 @@ qt_wrap_ui(LAUNCHER_UI ui/dialogs/SkinUploadDialog.ui ui/dialogs/ExportInstanceDialog.ui ui/dialogs/IconPickerDialog.ui - ui/dialogs/ImportResourcePackDialog.ui + ui/dialogs/ImportResourceDialog.ui ui/dialogs/MSALoginDialog.ui ui/dialogs/OfflineLoginDialog.ui ui/dialogs/AboutDialog.ui diff --git a/launcher/minecraft/mod/tasks/LocalDataPackParseTask.cpp b/launcher/minecraft/mod/tasks/LocalDataPackParseTask.cpp index 3fcb2110..5bb44877 100644 --- a/launcher/minecraft/mod/tasks/LocalDataPackParseTask.cpp +++ b/launcher/minecraft/mod/tasks/LocalDataPackParseTask.cpp @@ -50,7 +50,7 @@ bool processFolder(DataPack& pack, ProcessingLevel level) Q_ASSERT(pack.type() == ResourceType::FOLDER); auto mcmeta_invalid = [&pack]() { - qWarning() << "Resource pack at" << pack.fileinfo().filePath() << "does not have a valid pack.mcmeta"; + qWarning() << "Data pack at" << pack.fileinfo().filePath() << "does not have a valid pack.mcmeta"; return false; // the mcmeta is not optional }; @@ -95,7 +95,7 @@ bool processZIP(DataPack& pack, ProcessingLevel level) QuaZipFile file(&zip); auto mcmeta_invalid = [&pack]() { - qWarning() << "Resource pack at" << pack.fileinfo().filePath() << "does not have a valid pack.mcmeta"; + qWarning() << "Data pack at" << pack.fileinfo().filePath() << "does not have a valid pack.mcmeta"; return false; // the mcmeta is not optional }; diff --git a/launcher/minecraft/mod/tasks/LocalResourceParse.cpp b/launcher/minecraft/mod/tasks/LocalResourceParse.cpp index 19ddc899..63833832 100644 --- a/launcher/minecraft/mod/tasks/LocalResourceParse.cpp +++ b/launcher/minecraft/mod/tasks/LocalResourceParse.cpp @@ -19,6 +19,8 @@ * along with this program. If not, see . */ +#include + #include "LocalResourceParse.h" #include "LocalDataPackParseTask.h" @@ -28,6 +30,17 @@ #include "LocalTexturePackParseTask.h" #include "LocalWorldSaveParseTask.h" + +static const QMap s_packed_type_names = { + {PackedResourceType::ResourcePack, QObject::tr("resource pack")}, + {PackedResourceType::TexturePack, QObject::tr("texture pack")}, + {PackedResourceType::DataPack, QObject::tr("data pack")}, + {PackedResourceType::ShaderPack, QObject::tr("shader pack")}, + {PackedResourceType::WorldSave, QObject::tr("world save")}, + {PackedResourceType::Mod , QObject::tr("mod")}, + {PackedResourceType::UNKNOWN, QObject::tr("unknown")} +}; + namespace ResourceUtils { PackedResourceType identify(QFileInfo file){ if (file.exists() && file.isFile()) { @@ -57,4 +70,12 @@ PackedResourceType identify(QFileInfo file){ } return PackedResourceType::UNKNOWN; } + +QString getPackedTypeName(PackedResourceType type) { + return s_packed_type_names.constFind(type).value(); } + +} + + + diff --git a/launcher/minecraft/mod/tasks/LocalResourceParse.h b/launcher/minecraft/mod/tasks/LocalResourceParse.h index b07a874c..7385d24b 100644 --- a/launcher/minecraft/mod/tasks/LocalResourceParse.h +++ b/launcher/minecraft/mod/tasks/LocalResourceParse.h @@ -21,11 +21,17 @@ #pragma once +#include + #include #include #include enum class PackedResourceType { DataPack, ResourcePack, TexturePack, ShaderPack, WorldSave, Mod, UNKNOWN }; namespace ResourceUtils { +static const std::set ValidResourceTypes = { PackedResourceType::DataPack, PackedResourceType::ResourcePack, + PackedResourceType::TexturePack, PackedResourceType::ShaderPack, + PackedResourceType::WorldSave, PackedResourceType::Mod }; PackedResourceType identify(QFileInfo file); +QString getPackedTypeName(PackedResourceType type); } // namespace ResourceUtils diff --git a/launcher/ui/MainWindow.cpp b/launcher/ui/MainWindow.cpp index e913849d..1d2e44e5 100644 --- a/launcher/ui/MainWindow.cpp +++ b/launcher/ui/MainWindow.cpp @@ -109,13 +109,12 @@ #include "ui/dialogs/UpdateDialog.h" #include "ui/dialogs/EditAccountDialog.h" #include "ui/dialogs/ExportInstanceDialog.h" -#include "ui/dialogs/ImportResourcePackDialog.h" +#include "ui/dialogs/ImportResourceDialog.h" #include "ui/themes/ITheme.h" -#include -#include -#include -#include +#include "minecraft/mod/tasks/LocalResourceParse.h" +#include "minecraft/mod/ModFolderModel.h" +#include "minecraft/WorldList.h" #include "UpdateController.h" #include "KonamiCode.h" @@ -954,7 +953,7 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new MainWindow view->installEventFilter(this); view->setContextMenuPolicy(Qt::CustomContextMenu); connect(view, &QWidget::customContextMenuRequested, this, &MainWindow::showInstanceContextMenu); - connect(view, &InstanceView::droppedURLs, this, &MainWindow::droppedURLs, Qt::QueuedConnection); + connect(view, &InstanceView::droppedURLs, this, &MainWindow::processURLs, Qt::QueuedConnection); proxymodel = new InstanceProxyModel(this); proxymodel->setSourceModel(APPLICATION->instances().get()); @@ -1813,10 +1812,12 @@ void MainWindow::on_actionAddInstance_triggered() addInstance(); } -void MainWindow::droppedURLs(QList urls) +void MainWindow::processURLs(QList urls) { // NOTE: This loop only processes one dropped file! for (auto& url : urls) { + qDebug() << "Processing :" << url; + // The isLocalFile() check below doesn't work as intended without an explicit scheme. if (url.scheme().isEmpty()) url.setScheme("file"); @@ -1829,28 +1830,49 @@ void MainWindow::droppedURLs(QList urls) auto localFileName = url.toLocalFile(); QFileInfo localFileInfo(localFileName); - bool isResourcePack = ResourcePackUtils::validate(localFileInfo); - bool isTexturePack = TexturePackUtils::validate(localFileInfo); + auto type = ResourceUtils::identify(localFileInfo); - if (!isResourcePack && !isTexturePack) { // probably instance/modpack + // bool is_resource = type; + + if (!(ResourceUtils::ValidResourceTypes.count(type) > 0)) { // probably instance/modpack addInstance(localFileName); - break; + continue; } - ImportResourcePackDialog dlg(this); + ImportResourceDialog dlg(localFileName, type, this); if (dlg.exec() != QDialog::Accepted) - break; + continue; - qDebug() << "Adding resource/texture pack" << localFileName << "to" << dlg.selectedInstanceKey; + qDebug() << "Adding resource" << localFileName << "to" << dlg.selectedInstanceKey; auto inst = APPLICATION->instances()->getInstanceById(dlg.selectedInstanceKey); auto minecraftInst = std::dynamic_pointer_cast(inst); - if (isResourcePack) + + switch (type) { + case PackedResourceType::ResourcePack: minecraftInst->resourcePackList()->installResource(localFileName); - else if (isTexturePack) + break; + case PackedResourceType::TexturePack: minecraftInst->texturePackList()->installResource(localFileName); - break; + break; + case PackedResourceType::DataPack: + qWarning() << "Importing of Data Packs not supported at this time. Ignoring" << localFileName; + break; + case PackedResourceType::Mod: + minecraftInst->loaderModList()->installMod(localFileName); + break; + case PackedResourceType::ShaderPack: + minecraftInst->shaderPackList()->installResource(localFileName); + break; + case PackedResourceType::WorldSave: + minecraftInst->worldList()->installWorld(localFileName); + break; + case PackedResourceType::UNKNOWN: + default: + qDebug() << "Can't Identify" << localFileName << "Ignoring it."; + break; + } } } diff --git a/launcher/ui/MainWindow.h b/launcher/ui/MainWindow.h index f96f641d..6bf5f428 100644 --- a/launcher/ui/MainWindow.h +++ b/launcher/ui/MainWindow.h @@ -80,7 +80,7 @@ public: void updatesAllowedChanged(bool allowed); - void droppedURLs(QList urls); + void processURLs(QList urls); signals: void isClosing(); diff --git a/launcher/ui/dialogs/ImportResourcePackDialog.cpp b/launcher/ui/dialogs/ImportResourceDialog.cpp similarity index 73% rename from launcher/ui/dialogs/ImportResourcePackDialog.cpp rename to launcher/ui/dialogs/ImportResourceDialog.cpp index e8902656..84b69273 100644 --- a/launcher/ui/dialogs/ImportResourcePackDialog.cpp +++ b/launcher/ui/dialogs/ImportResourceDialog.cpp @@ -1,5 +1,5 @@ -#include "ImportResourcePackDialog.h" -#include "ui_ImportResourcePackDialog.h" +#include "ImportResourceDialog.h" +#include "ui_ImportResourceDialog.h" #include #include @@ -8,10 +8,11 @@ #include "InstanceList.h" #include -#include "ui/instanceview/InstanceProxyModel.h" #include "ui/instanceview/InstanceDelegate.h" +#include "ui/instanceview/InstanceProxyModel.h" -ImportResourcePackDialog::ImportResourcePackDialog(QWidget* parent) : QDialog(parent), ui(new Ui::ImportResourcePackDialog) +ImportResourceDialog::ImportResourceDialog(QString file_path, PackedResourceType type, QWidget* parent) + : QDialog(parent), ui(new Ui::ImportResourceDialog), m_resource_type(type), m_file_path(file_path) { ui->setupUi(this); setWindowModality(Qt::WindowModal); @@ -40,15 +41,19 @@ ImportResourcePackDialog::ImportResourcePackDialog(QWidget* parent) : QDialog(pa connect(contentsWidget, SIGNAL(doubleClicked(QModelIndex)), SLOT(activated(QModelIndex))); connect(contentsWidget->selectionModel(), SIGNAL(selectionChanged(QItemSelection, QItemSelection)), SLOT(selectionChanged(QItemSelection, QItemSelection))); + + ui->label->setText( + tr("Choose the instance you would like to import this %1 to.").arg(ResourceUtils::getPackedTypeName(m_resource_type))); + ui->label_file_path->setText(tr("File: %1").arg(m_file_path)); } -void ImportResourcePackDialog::activated(QModelIndex index) +void ImportResourceDialog::activated(QModelIndex index) { selectedInstanceKey = index.data(InstanceList::InstanceIDRole).toString(); accept(); } -void ImportResourcePackDialog::selectionChanged(QItemSelection selected, QItemSelection deselected) +void ImportResourceDialog::selectionChanged(QItemSelection selected, QItemSelection deselected) { if (selected.empty()) return; @@ -59,7 +64,7 @@ void ImportResourcePackDialog::selectionChanged(QItemSelection selected, QItemSe } } -ImportResourcePackDialog::~ImportResourcePackDialog() +ImportResourceDialog::~ImportResourceDialog() { delete ui; } diff --git a/launcher/ui/dialogs/ImportResourceDialog.h b/launcher/ui/dialogs/ImportResourceDialog.h new file mode 100644 index 00000000..c9e3f956 --- /dev/null +++ b/launcher/ui/dialogs/ImportResourceDialog.h @@ -0,0 +1,30 @@ +#pragma once + +#include +#include + +#include "ui/instanceview/InstanceProxyModel.h" +#include "minecraft/mod/tasks/LocalResourceParse.h" + +namespace Ui { +class ImportResourceDialog; +} + +class ImportResourceDialog : public QDialog { + Q_OBJECT + + public: + explicit ImportResourceDialog(QString file_path, PackedResourceType type, QWidget* parent = 0); + ~ImportResourceDialog(); + InstanceProxyModel* proxyModel; + QString selectedInstanceKey; + + private: + Ui::ImportResourceDialog* ui; + PackedResourceType m_resource_type; + QString m_file_path; + + private slots: + void selectionChanged(QItemSelection, QItemSelection); + void activated(QModelIndex); +}; diff --git a/launcher/ui/dialogs/ImportResourcePackDialog.ui b/launcher/ui/dialogs/ImportResourceDialog.ui similarity index 80% rename from launcher/ui/dialogs/ImportResourcePackDialog.ui rename to launcher/ui/dialogs/ImportResourceDialog.ui index 20cb9177..cc3f4ec1 100644 --- a/launcher/ui/dialogs/ImportResourcePackDialog.ui +++ b/launcher/ui/dialogs/ImportResourceDialog.ui @@ -1,7 +1,7 @@ - ImportResourcePackDialog - + ImportResourceDialog + 0 @@ -11,7 +11,7 @@ - Choose instance to import + Choose instance to import to @@ -21,6 +21,13 @@ + + + + + + + @@ -41,7 +48,7 @@ buttonBox accepted() - ImportResourcePackDialog + ImportResourceDialog accept() @@ -57,7 +64,7 @@ buttonBox rejected() - ImportResourcePackDialog + ImportResourceDialog reject() diff --git a/launcher/ui/dialogs/ImportResourcePackDialog.h b/launcher/ui/dialogs/ImportResourcePackDialog.h deleted file mode 100644 index 8356f204..00000000 --- a/launcher/ui/dialogs/ImportResourcePackDialog.h +++ /dev/null @@ -1,27 +0,0 @@ -#pragma once - -#include -#include - -#include "ui/instanceview/InstanceProxyModel.h" - -namespace Ui { -class ImportResourcePackDialog; -} - -class ImportResourcePackDialog : public QDialog { - Q_OBJECT - - public: - explicit ImportResourcePackDialog(QWidget* parent = 0); - ~ImportResourcePackDialog(); - InstanceProxyModel* proxyModel; - QString selectedInstanceKey; - - private: - Ui::ImportResourcePackDialog* ui; - - private slots: - void selectionChanged(QItemSelection, QItemSelection); - void activated(QModelIndex); -}; From 30b01ef053df670dc2d1912d88a8e9ded46c3c5e Mon Sep 17 00:00:00 2001 From: Rachel Powers <508861+Ryex@users.noreply.github.com> Date: Fri, 30 Dec 2022 19:27:26 -0700 Subject: [PATCH 057/152] fix: *sigh* no implicit QString->QFileInfo conversion in Qt6, again... Signed-off-by: Rachel Powers <508861+Ryex@users.noreply.github.com> --- launcher/ui/MainWindow.cpp | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/launcher/ui/MainWindow.cpp b/launcher/ui/MainWindow.cpp index 1d2e44e5..6412728a 100644 --- a/launcher/ui/MainWindow.cpp +++ b/launcher/ui/MainWindow.cpp @@ -1851,27 +1851,27 @@ void MainWindow::processURLs(QList urls) switch (type) { case PackedResourceType::ResourcePack: - minecraftInst->resourcePackList()->installResource(localFileName); - break; + minecraftInst->resourcePackList()->installResource(localFileName); + break; case PackedResourceType::TexturePack: - minecraftInst->texturePackList()->installResource(localFileName); - break; + minecraftInst->texturePackList()->installResource(localFileName); + break; case PackedResourceType::DataPack: - qWarning() << "Importing of Data Packs not supported at this time. Ignoring" << localFileName; - break; + qWarning() << "Importing of Data Packs not supported at this time. Ignoring" << localFileName; + break; case PackedResourceType::Mod: - minecraftInst->loaderModList()->installMod(localFileName); - break; + minecraftInst->loaderModList()->installMod(localFileName); + break; case PackedResourceType::ShaderPack: - minecraftInst->shaderPackList()->installResource(localFileName); - break; + minecraftInst->shaderPackList()->installResource(localFileName); + break; case PackedResourceType::WorldSave: - minecraftInst->worldList()->installWorld(localFileName); - break; + minecraftInst->worldList()->installWorld(localFileInfo); + break; case PackedResourceType::UNKNOWN: default: - qDebug() << "Can't Identify" << localFileName << "Ignoring it."; - break; + qDebug() << "Can't Identify" << localFileName << "Ignoring it."; + break; } } } From 9de6927c3fcdf813957dd6885b793a2c54100513 Mon Sep 17 00:00:00 2001 From: seth Date: Sat, 7 Jan 2023 19:18:22 -0500 Subject: [PATCH 058/152] feat: add CC BY-SA 4.0 info for teawie images Signed-off-by: seth --- launcher/resources/backgrounds/backgrounds.qrc | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/launcher/resources/backgrounds/backgrounds.qrc b/launcher/resources/backgrounds/backgrounds.qrc index 83096aef..e63a25b5 100644 --- a/launcher/resources/backgrounds/backgrounds.qrc +++ b/launcher/resources/backgrounds/backgrounds.qrc @@ -13,9 +13,17 @@ rory-flat-xmas.png rory-flat-bday.png rory-flat-spooky.png + + + + teawie.png + teawie-xmas.png + teawie-bday.png + teawie-spooky.png + From 0481ae187acf3392aa158af9e6e287f8695d54ad Mon Sep 17 00:00:00 2001 From: DioEgizio <83089242+DioEgizio@users.noreply.github.com> Date: Sun, 8 Jan 2023 10:34:45 +0100 Subject: [PATCH 059/152] chore: update windows msvc to qt 6.4.2 Signed-off-by: DioEgizio <83089242+DioEgizio@users.noreply.github.com> --- .github/workflows/build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b6e179e1..51b5a81d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -61,7 +61,7 @@ jobs: qt_ver: 6 qt_host: windows qt_arch: '' - qt_version: '6.4.0' + qt_version: '6.4.2' qt_modules: 'qt5compat qtimageformats' qt_tools: '' @@ -73,7 +73,7 @@ jobs: qt_ver: 6 qt_host: windows qt_arch: 'win64_msvc2019_arm64' - qt_version: '6.4.0' + qt_version: '6.4.2' qt_modules: 'qt5compat qtimageformats' qt_tools: '' From fca40c1c6b336cd4231852737fa817e1dd958c01 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 8 Jan 2023 22:40:41 +0000 Subject: [PATCH 060/152] chore(deps): update hendrikmuhs/ccache-action action to v1.2.6 --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 51b5a81d..406d079c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -143,7 +143,7 @@ jobs: - name: Setup ccache if: (runner.os != 'Windows' || matrix.msystem == '') && inputs.build_type == 'Debug' - uses: hendrikmuhs/ccache-action@v1.2.5 + uses: hendrikmuhs/ccache-action@v1.2.6 with: key: ${{ matrix.os }}-qt${{ matrix.qt_ver }}-${{ matrix.architecture }} From 7fdc81236e36a092dc1cdb9e7237e50d705228c6 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 9 Jan 2023 07:54:22 +0000 Subject: [PATCH 061/152] chore(deps): update actions/cache action to v3.2.3 --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 51b5a81d..3c2ede8e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -165,7 +165,7 @@ jobs: - name: Retrieve ccache cache (Windows MinGW-w64) if: runner.os == 'Windows' && matrix.msystem != '' && inputs.build_type == 'Debug' - uses: actions/cache@v3.2.2 + uses: actions/cache@v3.2.3 with: path: '${{ github.workspace }}\.ccache' key: ${{ matrix.os }}-mingw-w64 From 78bbcac0eaf1bb9df1ac87dafffbef659116fd80 Mon Sep 17 00:00:00 2001 From: TheLastRar Date: Mon, 9 Jan 2023 19:36:31 +0000 Subject: [PATCH 062/152] ui: Let Qt 6.4.2 handle dark mode titlebar Signed-off-by: TheLastRar --- launcher/Application.cpp | 16 +------- launcher/CMakeLists.txt | 10 ----- launcher/ui/WinDarkmode.cpp | 32 --------------- launcher/ui/WinDarkmode.h | 60 ----------------------------- launcher/ui/themes/ThemeManager.cpp | 17 -------- 5 files changed, 1 insertion(+), 134 deletions(-) delete mode 100644 launcher/ui/WinDarkmode.cpp delete mode 100644 launcher/ui/WinDarkmode.h diff --git a/launcher/Application.cpp b/launcher/Application.cpp index ff34a168..9d528d7a 100644 --- a/launcher/Application.cpp +++ b/launcher/Application.cpp @@ -62,11 +62,6 @@ #include "ui/pages/global/APIPage.h" #include "ui/pages/global/CustomCommandsPage.h" -#ifdef Q_OS_WIN -#include "ui/WinDarkmode.h" -#include -#endif - #include "ui/setupwizard/SetupWizard.h" #include "ui/setupwizard/LanguageWizardPage.h" #include "ui/setupwizard/JavaWizardPage.h" @@ -1353,16 +1348,7 @@ MainWindow* Application::showMainWindow(bool minimized) m_mainWindow = new MainWindow(); m_mainWindow->restoreState(QByteArray::fromBase64(APPLICATION->settings()->get("MainWindowState").toByteArray())); m_mainWindow->restoreGeometry(QByteArray::fromBase64(APPLICATION->settings()->get("MainWindowGeometry").toByteArray())); -#ifdef Q_OS_WIN - if (IsWindows10OrGreater()) - { - if (QString::compare(settings()->get("ApplicationTheme").toString(), "dark") == 0) { - WinDarkmode::setDarkWinTitlebar(m_mainWindow->winId(), true); - } else { - WinDarkmode::setDarkWinTitlebar(m_mainWindow->winId(), false); - } - } -#endif + if(minimized) { m_mainWindow->showMinimized(); diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index 8b5c63ff..57480671 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -937,16 +937,6 @@ SET(LAUNCHER_SOURCES ui/instanceview/VisualGroup.h ) -if(WIN32) - set(LAUNCHER_SOURCES - ${LAUNCHER_SOURCES} - - # GUI - dark titlebar for Windows 10/11 - ui/WinDarkmode.h - ui/WinDarkmode.cpp - ) -endif() - qt_wrap_ui(LAUNCHER_UI ui/setupwizard/PasteWizardPage.ui ui/pages/global/AccountListPage.ui diff --git a/launcher/ui/WinDarkmode.cpp b/launcher/ui/WinDarkmode.cpp deleted file mode 100644 index eac68e4f..00000000 --- a/launcher/ui/WinDarkmode.cpp +++ /dev/null @@ -1,32 +0,0 @@ -#include - -#include "WinDarkmode.h" - -namespace WinDarkmode { - -/* See https://github.com/statiolake/neovim-qt/commit/da8eaba7f0e38b6b51f3bacd02a8cc2d1f7a34d8 */ -void setDarkWinTitlebar(WId winid, bool darkmode) -{ - HWND hwnd = reinterpret_cast(winid); - BOOL dark = (BOOL) darkmode; - - HMODULE hUxtheme = LoadLibraryExW(L"uxtheme.dll", NULL, LOAD_LIBRARY_SEARCH_SYSTEM32); - HMODULE hUser32 = GetModuleHandleW(L"user32.dll"); - fnAllowDarkModeForWindow AllowDarkModeForWindow - = reinterpret_cast(GetProcAddress(hUxtheme, MAKEINTRESOURCEA(133))); - fnSetPreferredAppMode SetPreferredAppMode - = reinterpret_cast(GetProcAddress(hUxtheme, MAKEINTRESOURCEA(135))); - fnSetWindowCompositionAttribute SetWindowCompositionAttribute - = reinterpret_cast(GetProcAddress(hUser32, "SetWindowCompositionAttribute")); - - SetPreferredAppMode(AllowDark); - AllowDarkModeForWindow(hwnd, dark); - WINDOWCOMPOSITIONATTRIBDATA data = { - WCA_USEDARKMODECOLORS, - &dark, - sizeof(dark) - }; - SetWindowCompositionAttribute(hwnd, &data); -} - -} diff --git a/launcher/ui/WinDarkmode.h b/launcher/ui/WinDarkmode.h deleted file mode 100644 index 5b567c6b..00000000 --- a/launcher/ui/WinDarkmode.h +++ /dev/null @@ -1,60 +0,0 @@ -#pragma once - -#include -#include - - -namespace WinDarkmode { - -void setDarkWinTitlebar(WId winid, bool darkmode); - -enum PreferredAppMode { - Default, - AllowDark, - ForceDark, - ForceLight, - Max -}; - -enum WINDOWCOMPOSITIONATTRIB { - WCA_UNDEFINED = 0, - WCA_NCRENDERING_ENABLED = 1, - WCA_NCRENDERING_POLICY = 2, - WCA_TRANSITIONS_FORCEDISABLED = 3, - WCA_ALLOW_NCPAINT = 4, - WCA_CAPTION_BUTTON_BOUNDS = 5, - WCA_NONCLIENT_RTL_LAYOUT = 6, - WCA_FORCE_ICONIC_REPRESENTATION = 7, - WCA_EXTENDED_FRAME_BOUNDS = 8, - WCA_HAS_ICONIC_BITMAP = 9, - WCA_THEME_ATTRIBUTES = 10, - WCA_NCRENDERING_EXILED = 11, - WCA_NCADORNMENTINFO = 12, - WCA_EXCLUDED_FROM_LIVEPREVIEW = 13, - WCA_VIDEO_OVERLAY_ACTIVE = 14, - WCA_FORCE_ACTIVEWINDOW_APPEARANCE = 15, - WCA_DISALLOW_PEEK = 16, - WCA_CLOAK = 17, - WCA_CLOAKED = 18, - WCA_ACCENT_POLICY = 19, - WCA_FREEZE_REPRESENTATION = 20, - WCA_EVER_UNCLOAKED = 21, - WCA_VISUAL_OWNER = 22, - WCA_HOLOGRAPHIC = 23, - WCA_EXCLUDED_FROM_DDA = 24, - WCA_PASSIVEUPDATEMODE = 25, - WCA_USEDARKMODECOLORS = 26, - WCA_LAST = 27 -}; - -struct WINDOWCOMPOSITIONATTRIBDATA { - WINDOWCOMPOSITIONATTRIB Attrib; - PVOID pvData; - SIZE_T cbData; -}; - -using fnAllowDarkModeForWindow = BOOL (WINAPI *)(HWND hWnd, BOOL allow); -using fnSetPreferredAppMode = PreferredAppMode (WINAPI *)(PreferredAppMode appMode); -using fnSetWindowCompositionAttribute = BOOL (WINAPI *)(HWND hwnd, WINDOWCOMPOSITIONATTRIBDATA *); - -} diff --git a/launcher/ui/themes/ThemeManager.cpp b/launcher/ui/themes/ThemeManager.cpp index 01a38a86..5a612472 100644 --- a/launcher/ui/themes/ThemeManager.cpp +++ b/launcher/ui/themes/ThemeManager.cpp @@ -28,14 +28,6 @@ #include "Application.h" -#ifdef Q_OS_WIN -#include -// this is needed for versionhelpers.h, it is also included in WinDarkmode, but we can't rely on that. -// Ultimately this should be included in versionhelpers, but that is outside of the project. -#include "ui/WinDarkmode.h" -#include -#endif - ThemeManager::ThemeManager(MainWindow* mainWindow) { m_mainWindow = mainWindow; @@ -140,15 +132,6 @@ void ThemeManager::setApplicationTheme(const QString& name, bool initial) auto& theme = themeIter->second; themeDebugLog() << "applying theme" << theme->name(); theme->apply(initial); -#ifdef Q_OS_WIN - if (m_mainWindow && IsWindows10OrGreater()) { - if (QString::compare(theme->id(), "dark") == 0) { - WinDarkmode::setDarkWinTitlebar(m_mainWindow->winId(), true); - } else { - WinDarkmode::setDarkWinTitlebar(m_mainWindow->winId(), false); - } - } -#endif } else { themeWarningLog() << "Tried to set invalid theme:" << name; } From a4870d4834f627f6c730d7b72237d7357aeacc8f Mon Sep 17 00:00:00 2001 From: Rachel Powers <508861+Ryex@users.noreply.github.com> Date: Mon, 2 Jan 2023 08:55:32 -0700 Subject: [PATCH 063/152] fix: fix #700 fixed by properly converting from a file path and converting to native seperators. should have known naive handling of file path as a URL would come back to bite us cross platform. Signed-off-by: Rachel Powers <508861+Ryex@users.noreply.github.com> --- launcher/Application.cpp | 6 +++--- launcher/ui/MainWindow.cpp | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/launcher/Application.cpp b/launcher/Application.cpp index 581e51ae..19d6d3c2 100644 --- a/launcher/Application.cpp +++ b/launcher/Application.cpp @@ -263,11 +263,11 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv) m_instanceIdToShowWindowOf = parser.value("show"); for (auto zip_path : parser.values("import")){ - m_zipsToImport.append(QUrl(zip_path)); + m_zipsToImport.append(QUrl::fromLocalFile(QFileInfo(zip_path).absoluteFilePath())); } for (auto zip_path : parser.positionalArguments()){ // treat unspesified positional arguments as import urls - m_zipsToImport.append(QUrl(zip_path)); + m_zipsToImport.append(QUrl::fromLocalFile(QFileInfo(zip_path).absoluteFilePath())); } @@ -1065,7 +1065,7 @@ void Application::messageReceived(const QByteArray& message) qWarning() << "Received" << command << "message without a zip path/URL."; return; } - m_mainWindow->processURLs({ QUrl(path) }); + m_mainWindow->processURLs({ QUrl::fromLocalFile(QFileInfo(path).absoluteFilePath()) }); } else if(command == "launch") { diff --git a/launcher/ui/MainWindow.cpp b/launcher/ui/MainWindow.cpp index 6412728a..d5aa4c1a 100644 --- a/launcher/ui/MainWindow.cpp +++ b/launcher/ui/MainWindow.cpp @@ -1827,7 +1827,7 @@ void MainWindow::processURLs(QList urls) break; } - auto localFileName = url.toLocalFile(); + auto localFileName = QDir::toNativeSeparators(url.toLocalFile()) ; QFileInfo localFileInfo(localFileName); auto type = ResourceUtils::identify(localFileInfo); From 574af2c795a19246c18e5f07a49d6d41f5670a6e Mon Sep 17 00:00:00 2001 From: Rachel Powers <508861+Ryex@users.noreply.github.com> Date: Mon, 9 Jan 2023 17:12:28 -0700 Subject: [PATCH 064/152] chore: cleanup review suggestions Signed-off-by: Rachel Powers <508861+Ryex@users.noreply.github.com> --- launcher/minecraft/mod/tasks/LocalResourceParse.cpp | 3 --- launcher/ui/dialogs/ImportResourceDialog.h | 10 +++++----- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/launcher/minecraft/mod/tasks/LocalResourceParse.cpp b/launcher/minecraft/mod/tasks/LocalResourceParse.cpp index 63833832..4d760df2 100644 --- a/launcher/minecraft/mod/tasks/LocalResourceParse.cpp +++ b/launcher/minecraft/mod/tasks/LocalResourceParse.cpp @@ -76,6 +76,3 @@ QString getPackedTypeName(PackedResourceType type) { } } - - - diff --git a/launcher/ui/dialogs/ImportResourceDialog.h b/launcher/ui/dialogs/ImportResourceDialog.h index c9e3f956..5f2f7a92 100644 --- a/launcher/ui/dialogs/ImportResourceDialog.h +++ b/launcher/ui/dialogs/ImportResourceDialog.h @@ -3,8 +3,8 @@ #include #include -#include "ui/instanceview/InstanceProxyModel.h" #include "minecraft/mod/tasks/LocalResourceParse.h" +#include "ui/instanceview/InstanceProxyModel.h" namespace Ui { class ImportResourceDialog; @@ -14,15 +14,15 @@ class ImportResourceDialog : public QDialog { Q_OBJECT public: - explicit ImportResourceDialog(QString file_path, PackedResourceType type, QWidget* parent = 0); - ~ImportResourceDialog(); - InstanceProxyModel* proxyModel; + explicit ImportResourceDialog(QString file_path, PackedResourceType type, QWidget* parent = nullptr); + ~ImportResourceDialog() override; QString selectedInstanceKey; - + private: Ui::ImportResourceDialog* ui; PackedResourceType m_resource_type; QString m_file_path; + InstanceProxyModel* proxyModel; private slots: void selectionChanged(QItemSelection, QItemSelection); From a113ecca8b86a0aa9a448795b49c9bb841ddc59a Mon Sep 17 00:00:00 2001 From: DioEgizio <83089242+DioEgizio@users.noreply.github.com> Date: Tue, 10 Jan 2023 15:00:39 +0100 Subject: [PATCH 065/152] fix: just use github runner's openssl 1.1 instead of installing 3 on macos signing Signed-off-by: DioEgizio <83089242+DioEgizio@users.noreply.github.com> --- .github/workflows/build.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9d75a457..e0a80f20 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -342,9 +342,8 @@ jobs: if: matrix.name == 'macOS' run: | if [ '${{ secrets.SPARKLE_ED25519_KEY }}' != '' ]; then - brew install openssl@3 echo '${{ secrets.SPARKLE_ED25519_KEY }}' > ed25519-priv.pem - signature=$(/usr/local/opt/openssl@3/bin/openssl pkeyutl -sign -rawin -in ${{ github.workspace }}/PrismLauncher.tar.gz -inkey ed25519-priv.pem | openssl base64 | tr -d \\n) + signature=$(openssl pkeyutl -sign -rawin -in ${{ github.workspace }}/PrismLauncher.tar.gz -inkey ed25519-priv.pem | openssl base64 | tr -d \\n) rm ed25519-priv.pem cat >> $GITHUB_STEP_SUMMARY << EOF ### Artifact Information :information_source: From 1b80ae0fca5e41d8caaa7d77d19faa9826752143 Mon Sep 17 00:00:00 2001 From: Tayou Date: Sat, 22 Oct 2022 19:43:04 +0200 Subject: [PATCH 066/152] add theme setup wizard Signed-off-by: Tayou --- launcher/Application.cpp | 21 +- launcher/Application.h | 4 +- launcher/CMakeLists.txt | 6 + launcher/ui/MainWindow.cpp | 2 +- launcher/ui/pages/global/LauncherPage.cpp | 110 ------ launcher/ui/pages/global/LauncherPage.ui | 166 +-------- launcher/ui/setupwizard/ThemeWizardPage.cpp | 70 ++++ launcher/ui/setupwizard/ThemeWizardPage.h | 44 +++ launcher/ui/setupwizard/ThemeWizardPage.ui | 336 ++++++++++++++++++ launcher/ui/themes/ITheme.cpp | 40 ++- launcher/ui/themes/ITheme.h | 36 +- launcher/ui/themes/SystemTheme.cpp | 9 +- launcher/ui/themes/SystemTheme.h | 36 +- launcher/ui/themes/ThemeManager.cpp | 6 +- launcher/ui/themes/ThemeManager.h | 3 +- .../ui/widgets/ThemeCustomizationWidget.cpp | 135 +++++++ .../ui/widgets/ThemeCustomizationWidget.h | 64 ++++ .../ui/widgets/ThemeCustomizationWidget.ui | 182 ++++++++++ 18 files changed, 982 insertions(+), 288 deletions(-) create mode 100644 launcher/ui/setupwizard/ThemeWizardPage.cpp create mode 100644 launcher/ui/setupwizard/ThemeWizardPage.h create mode 100644 launcher/ui/setupwizard/ThemeWizardPage.ui create mode 100644 launcher/ui/widgets/ThemeCustomizationWidget.cpp create mode 100644 launcher/ui/widgets/ThemeCustomizationWidget.h create mode 100644 launcher/ui/widgets/ThemeCustomizationWidget.ui diff --git a/launcher/Application.cpp b/launcher/Application.cpp index 9d528d7a..3e64b74f 100644 --- a/launcher/Application.cpp +++ b/launcher/Application.cpp @@ -66,6 +66,7 @@ #include "ui/setupwizard/LanguageWizardPage.h" #include "ui/setupwizard/JavaWizardPage.h" #include "ui/setupwizard/PasteWizardPage.h" +#include "ui/setupwizard/ThemeWizardPage.h" #include "ui/dialogs/CustomMessageBox.h" @@ -846,10 +847,7 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv) }); { - setIconTheme(settings()->get("IconTheme").toString()); - qDebug() << "<> Icon theme set."; - setApplicationTheme(settings()->get("ApplicationTheme").toString(), true); - qDebug() << "<> Application theme set."; + applyCurrentlySelectedTheme(); } updateCapabilities(); @@ -892,6 +890,7 @@ bool Application::createSetupWizard() return false; }(); bool pasteInterventionRequired = settings()->get("PastebinURL") != ""; + bool themeInterventionRequired = settings()->get("ApplicationTheme") != ""; bool wizardRequired = javaRequired || languageRequired || pasteInterventionRequired; if(wizardRequired) @@ -911,6 +910,11 @@ bool Application::createSetupWizard() { m_setupWizard->addPage(new PasteWizardPage(m_setupWizard)); } + + if (themeInterventionRequired) + { + m_setupWizard->addPage(new ThemeWizardPage(m_setupWizard)); + } connect(m_setupWizard, &QDialog::finished, this, &Application::setupWizardFinished); m_setupWizard->show(); return true; @@ -1118,9 +1122,14 @@ QList Application::getValidApplicationThemes() return m_themeManager->getValidApplicationThemes(); } -void Application::setApplicationTheme(const QString& name, bool initial) +void Application::applyCurrentlySelectedTheme() { - m_themeManager->setApplicationTheme(name, initial); + m_themeManager->applyCurrentlySelectedTheme(); +} + +void Application::setApplicationTheme(const QString& name) +{ + m_themeManager->setApplicationTheme(name); } void Application::setIconTheme(const QString& name) diff --git a/launcher/Application.h b/launcher/Application.h index 7884227a..a7938629 100644 --- a/launcher/Application.h +++ b/launcher/Application.h @@ -120,9 +120,11 @@ public: void setIconTheme(const QString& name); + void applyCurrentlySelectedTheme(); + QList getValidApplicationThemes(); - void setApplicationTheme(const QString& name, bool initial); + void setApplicationTheme(const QString& name); shared_qobject_ptr updateChecker() { return m_updateChecker; diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index 57480671..74b7b212 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -683,6 +683,8 @@ SET(LAUNCHER_SOURCES ui/setupwizard/LanguageWizardPage.h ui/setupwizard/PasteWizardPage.cpp ui/setupwizard/PasteWizardPage.h + ui/setupwizard/ThemeWizardPage.cpp + ui/setupwizard/ThemeWizardPage.h # GUI - themes ui/themes/FusionTheme.cpp @@ -922,6 +924,8 @@ SET(LAUNCHER_SOURCES ui/widgets/ProgressWidget.cpp ui/widgets/WideBar.h ui/widgets/WideBar.cpp + ui/widgets/ThemeCustomizationWidget.h + ui/widgets/ThemeCustomizationWidget.cpp # GUI - instance group view ui/instanceview/InstanceProxyModel.cpp @@ -939,6 +943,7 @@ SET(LAUNCHER_SOURCES qt_wrap_ui(LAUNCHER_UI ui/setupwizard/PasteWizardPage.ui + ui/setupwizard/ThemeWizardPage.ui ui/pages/global/AccountListPage.ui ui/pages/global/JavaPage.ui ui/pages/global/LauncherPage.ui @@ -971,6 +976,7 @@ qt_wrap_ui(LAUNCHER_UI ui/widgets/CustomCommands.ui ui/widgets/InfoFrame.ui ui/widgets/ModFilterWidget.ui + ui/widgets/ThemeCustomizationWidget.ui ui/dialogs/CopyInstanceDialog.ui ui/dialogs/ProfileSetupDialog.ui ui/dialogs/ProgressDialog.ui diff --git a/launcher/ui/MainWindow.cpp b/launcher/ui/MainWindow.cpp index e913849d..331ca0e1 100644 --- a/launcher/ui/MainWindow.cpp +++ b/launcher/ui/MainWindow.cpp @@ -1346,7 +1346,7 @@ void MainWindow::updateThemeMenu() themeAction->setActionGroup(themesGroup); connect(themeAction, &QAction::triggered, [theme]() { - APPLICATION->setApplicationTheme(theme->id(),false); + APPLICATION->setApplicationTheme(theme->id()); APPLICATION->settings()->set("ApplicationTheme", theme->id()); }); } diff --git a/launcher/ui/pages/global/LauncherPage.cpp b/launcher/ui/pages/global/LauncherPage.cpp index bd7cec6a..69a8e3df 100644 --- a/launcher/ui/pages/global/LauncherPage.cpp +++ b/launcher/ui/pages/global/LauncherPage.cpp @@ -286,75 +286,6 @@ void LauncherPage::applySettings() } s->set("UpdateChannel", m_currentUpdateChannel); - auto original = s->get("IconTheme").toString(); - //FIXME: make generic - switch (ui->themeComboBox->currentIndex()) - { - case 0: - s->set("IconTheme", "pe_colored"); - break; - case 1: - s->set("IconTheme", "pe_light"); - break; - case 2: - s->set("IconTheme", "pe_dark"); - break; - case 3: - s->set("IconTheme", "pe_blue"); - break; - case 4: - s->set("IconTheme", "breeze_light"); - break; - case 5: - s->set("IconTheme", "breeze_dark"); - break; - case 6: - s->set("IconTheme", "OSX"); - break; - case 7: - s->set("IconTheme", "iOS"); - break; - case 8: - s->set("IconTheme", "flat"); - break; - case 9: - s->set("IconTheme", "flat_white"); - break; - case 10: - s->set("IconTheme", "multimc"); - break; - case 11: - s->set("IconTheme", "custom"); - break; - } - - if(original != s->get("IconTheme")) - { - APPLICATION->setIconTheme(s->get("IconTheme").toString()); - } - - auto originalAppTheme = s->get("ApplicationTheme").toString(); - auto newAppTheme = ui->themeComboBoxColors->currentData().toString(); - if(originalAppTheme != newAppTheme) - { - s->set("ApplicationTheme", newAppTheme); - APPLICATION->setApplicationTheme(newAppTheme, false); - } - - switch (ui->themeBackgroundCat->currentIndex()) { - case 0: // original cat - s->set("BackgroundCat", "kitteh"); - break; - case 1: // rory the cat - s->set("BackgroundCat", "rory"); - break; - case 2: // rory the cat flat edition - s->set("BackgroundCat", "rory-flat"); - break; - case 3: // teawie - s->set("BackgroundCat", "teawie"); - break; - } s->set("MenuBarInsteadOfToolBar", ui->preferMenuBarCheckBox->isChecked()); @@ -404,47 +335,6 @@ void LauncherPage::loadSettings() } m_currentUpdateChannel = s->get("UpdateChannel").toString(); - //FIXME: make generic - auto theme = s->get("IconTheme").toString(); - QStringList iconThemeOptions{"pe_colored", - "pe_light", - "pe_dark", - "pe_blue", - "breeze_light", - "breeze_dark", - "OSX", - "iOS", - "flat", - "flat_white", - "multimc", - "custom"}; - ui->themeComboBox->setCurrentIndex(iconThemeOptions.indexOf(theme)); - - auto cat = s->get("BackgroundCat").toString(); - if (cat == "kitteh") { - ui->themeBackgroundCat->setCurrentIndex(0); - } else if (cat == "rory") { - ui->themeBackgroundCat->setCurrentIndex(1); - } else if (cat == "rory-flat") { - ui->themeBackgroundCat->setCurrentIndex(2); - } else if (cat == "teawie") { - ui->themeBackgroundCat->setCurrentIndex(3); - } - - { - auto currentTheme = s->get("ApplicationTheme").toString(); - auto themes = APPLICATION->getValidApplicationThemes(); - int idx = 0; - for(auto &theme: themes) - { - ui->themeComboBoxColors->addItem(theme->name(), theme->id()); - if(currentTheme == theme->id()) - { - ui->themeComboBoxColors->setCurrentIndex(idx); - } - idx++; - } - } // Toolbar/menu bar settings (not applicable if native menu bar is present) ui->toolsBox->setEnabled(!QMenuBar().isNativeMenuBar()); diff --git a/launcher/ui/pages/global/LauncherPage.ui b/launcher/ui/pages/global/LauncherPage.ui index ded333aa..65f4a9d5 100644 --- a/launcher/ui/pages/global/LauncherPage.ui +++ b/launcher/ui/pages/global/LauncherPage.ui @@ -6,7 +6,7 @@ 0 0 - 514 + 511 629 @@ -38,7 +38,7 @@ QTabWidget::Rounded - 0 + 1 @@ -243,155 +243,9 @@ Theme - - - - - &Icons - - - themeComboBox - - - - - - - - 0 - 0 - - - - Qt::StrongFocus - - - - Simple (Colored Icons) - - - - - Simple (Light Icons) - - - - - Simple (Dark Icons) - - - - - Simple (Blue Icons) - - - - - Breeze Light - - - - - Breeze Dark - - - - - OSX - - - - - iOS - - - - - Flat - - - - - Flat (White) - - - - - Legacy - - - - - Custom - - - - - - - - &Colors - - - themeComboBoxColors - - - - - - - - 0 - 0 - - - - Qt::StrongFocus - - - - - - - C&at - - - themeBackgroundCat - - - - - - - - 0 - 0 - - - - Qt::StrongFocus - - - - Background Cat (from MultiMC) - - - - - Rory ID 11 (drawn by Ashtaka) - - - - - Rory ID 11 (flat edition, drawn by Ashtaka) - - - - - Teawie (drawn by SympathyTea) - - - + + + @@ -575,6 +429,14 @@ + + + ThemeCustomizationWidget + QWidget +

ui/widgets/ThemeCustomizationWidget.h
+ 1 + + tabWidget autoUpdateCheckBox @@ -587,8 +449,6 @@ iconsDirBrowseBtn sortLastLaunchedBtn sortByNameBtn - themeComboBox - themeComboBoxColors showConsoleCheck autoCloseConsoleCheck showConsoleErrorCheck diff --git a/launcher/ui/setupwizard/ThemeWizardPage.cpp b/launcher/ui/setupwizard/ThemeWizardPage.cpp new file mode 100644 index 00000000..6f041134 --- /dev/null +++ b/launcher/ui/setupwizard/ThemeWizardPage.cpp @@ -0,0 +1,70 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 Tayou + * + * 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 "ThemeWizardPage.h" +#include "ui_ThemeWizardPage.h" + +#include "Application.h" +#include "ui/themes/ITheme.h" +#include "ui/widgets/ThemeCustomizationWidget.h" +#include "ui_ThemeCustomizationWidget.h" + +ThemeWizardPage::ThemeWizardPage(QWidget *parent) : +BaseWizardPage(parent), +ui(new Ui::ThemeWizardPage) { + ui->setupUi(this); + + ui->themeCustomizationWidget->showFeatures((ThemeFields)(ThemeFields::ICONS | ThemeFields::WIDGETS)); + connect(ui->themeCustomizationWidget, QOverload::of(&ThemeCustomizationWidget::currentIconThemeChanged), this, &ThemeWizardPage::updateIcons); + + updateIcons(); +} + +ThemeWizardPage::~ThemeWizardPage() { +delete ui; +} + +void ThemeWizardPage::initializePage() +{ +} + +bool ThemeWizardPage::validatePage() +{ + return true; +} + +void ThemeWizardPage::updateIcons() { + qDebug() << "Setting Icons"; + ui->previewIconButton0->setIcon(APPLICATION->getThemedIcon("new")); + ui->previewIconButton1->setIcon(APPLICATION->getThemedIcon("centralmods")); + ui->previewIconButton2->setIcon(APPLICATION->getThemedIcon("viewfolder")); + ui->previewIconButton3->setIcon(APPLICATION->getThemedIcon("launch")); + ui->previewIconButton4->setIcon(APPLICATION->getThemedIcon("copy")); + ui->previewIconButton5->setIcon(APPLICATION->getThemedIcon("export")); + ui->previewIconButton6->setIcon(APPLICATION->getThemedIcon("delete")); + ui->previewIconButton7->setIcon(APPLICATION->getThemedIcon("about")); + ui->previewIconButton8->setIcon(APPLICATION->getThemedIcon("settings")); + ui->previewIconButton9->setIcon(APPLICATION->getThemedIcon("cat")); + update(); + repaint(); + parentWidget()->update(); +} + +void ThemeWizardPage::retranslate() +{ + ui->retranslateUi(this); +} diff --git a/launcher/ui/setupwizard/ThemeWizardPage.h b/launcher/ui/setupwizard/ThemeWizardPage.h new file mode 100644 index 00000000..10913d1b --- /dev/null +++ b/launcher/ui/setupwizard/ThemeWizardPage.h @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 Tayou + * + * 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 "BaseWizardPage.h" + +namespace Ui { +class ThemeWizardPage; +} + +class ThemeWizardPage : public BaseWizardPage +{ + Q_OBJECT + +public: + explicit ThemeWizardPage(QWidget *parent = nullptr); + ~ThemeWizardPage(); + + void initializePage() override; + bool validatePage() override; + void retranslate() override; + +private slots: + void updateIcons(); + +private: + Ui::ThemeWizardPage *ui; +}; diff --git a/launcher/ui/setupwizard/ThemeWizardPage.ui b/launcher/ui/setupwizard/ThemeWizardPage.ui new file mode 100644 index 00000000..b743644f --- /dev/null +++ b/launcher/ui/setupwizard/ThemeWizardPage.ui @@ -0,0 +1,336 @@ + + + ThemeWizardPage + + + + 0 + 0 + 400 + 300 + + + + WizardPage + + + + + + Select the Theme you wish to use + + + + + + + + 0 + 100 + + + + + + + + Qt::Horizontal + + + + + + + Icon Preview: + + + + + + + + + + 0 + 0 + + + + + 30 + 30 + + + + + .. + + + false + + + true + + + + + + + + 0 + 0 + + + + + 30 + 30 + + + + + .. + + + false + + + true + + + + + + + + 0 + 0 + + + + + 30 + 30 + + + + + .. + + + false + + + true + + + + + + + + 0 + 0 + + + + + 30 + 30 + + + + + .. + + + false + + + true + + + + + + + + 0 + 0 + + + + + 30 + 30 + + + + + .. + + + false + + + true + + + + + + + + 0 + 0 + + + + + 30 + 30 + + + + + .. + + + false + + + true + + + + + + + + 0 + 0 + + + + + 30 + 30 + + + + + .. + + + false + + + true + + + + + + + + 0 + 0 + + + + + 30 + 30 + + + + + .. + + + false + + + true + + + + + + + + 0 + 0 + + + + + 30 + 30 + + + + + .. + + + false + + + true + + + + + + + + 0 + 0 + + + + + 30 + 30 + + + + + .. + + + false + + + true + + + + + + + + + Qt::Vertical + + + + 20 + 193 + + + + + + + + + ThemeCustomizationWidget + QWidget +
ui/widgets/ThemeCustomizationWidget.h
+
+
+ + +
diff --git a/launcher/ui/themes/ITheme.cpp b/launcher/ui/themes/ITheme.cpp index 8bfc466d..22043e44 100644 --- a/launcher/ui/themes/ITheme.cpp +++ b/launcher/ui/themes/ITheme.cpp @@ -1,19 +1,51 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 Tayou + * + * 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 "ITheme.h" #include "rainbow.h" #include #include #include "Application.h" -void ITheme::apply(bool) +void ITheme::apply() { APPLICATION->setStyleSheet(QString()); QApplication::setStyle(QStyleFactory::create(qtTheme())); if (hasColorScheme()) { QApplication::setPalette(colorScheme()); } - if (hasStyleSheet()) - APPLICATION->setStyleSheet(appStyleSheet()); - + APPLICATION->setStyleSheet(appStyleSheet()); QDir::setSearchPaths("theme", searchPaths()); } diff --git a/launcher/ui/themes/ITheme.h b/launcher/ui/themes/ITheme.h index c2347cf6..bb5c8afe 100644 --- a/launcher/ui/themes/ITheme.h +++ b/launcher/ui/themes/ITheme.h @@ -1,3 +1,37 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 Tayou + * + * 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 @@ -8,7 +42,7 @@ class ITheme { public: virtual ~ITheme() {} - virtual void apply(bool initial); + virtual void apply(); virtual QString id() = 0; virtual QString name() = 0; virtual bool hasStyleSheet() = 0; diff --git a/launcher/ui/themes/SystemTheme.cpp b/launcher/ui/themes/SystemTheme.cpp index a63d1741..d6ef442b 100644 --- a/launcher/ui/themes/SystemTheme.cpp +++ b/launcher/ui/themes/SystemTheme.cpp @@ -62,14 +62,9 @@ SystemTheme::SystemTheme() themeDebugLog() << "System theme not found, defaulted to Fusion"; } -void SystemTheme::apply(bool initial) +void SystemTheme::apply() { - // if we are applying the system theme as the first theme, just don't touch anything. it's for the better... - if(initial) - { - return; - } - ITheme::apply(initial); + ITheme::apply(); } QString SystemTheme::id() diff --git a/launcher/ui/themes/SystemTheme.h b/launcher/ui/themes/SystemTheme.h index fe450600..5c9216eb 100644 --- a/launcher/ui/themes/SystemTheme.h +++ b/launcher/ui/themes/SystemTheme.h @@ -1,3 +1,37 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 Tayou + * + * 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 "ITheme.h" @@ -7,7 +41,7 @@ class SystemTheme: public ITheme public: SystemTheme(); virtual ~SystemTheme() {} - void apply(bool initial) override; + void apply() override; QString id() override; QString name() override; diff --git a/launcher/ui/themes/ThemeManager.cpp b/launcher/ui/themes/ThemeManager.cpp index 5a612472..a6cebc6f 100644 --- a/launcher/ui/themes/ThemeManager.cpp +++ b/launcher/ui/themes/ThemeManager.cpp @@ -120,18 +120,18 @@ void ThemeManager::applyCurrentlySelectedTheme() { setIconTheme(APPLICATION->settings()->get("IconTheme").toString()); themeDebugLog() << "<> Icon theme set."; - setApplicationTheme(APPLICATION->settings()->get("ApplicationTheme").toString(), true); + setApplicationTheme(APPLICATION->settings()->get("ApplicationTheme").toString()); themeDebugLog() << "<> Application theme set."; } -void ThemeManager::setApplicationTheme(const QString& name, bool initial) +void ThemeManager::setApplicationTheme(const QString& name) { auto systemPalette = qApp->palette(); auto themeIter = m_themes.find(name); if (themeIter != m_themes.end()) { auto& theme = themeIter->second; themeDebugLog() << "applying theme" << theme->name(); - theme->apply(initial); + theme->apply(); } else { themeWarningLog() << "Tried to set invalid theme:" << name; } diff --git a/launcher/ui/themes/ThemeManager.h b/launcher/ui/themes/ThemeManager.h index b85cb742..0a70ddfc 100644 --- a/launcher/ui/themes/ThemeManager.h +++ b/launcher/ui/themes/ThemeManager.h @@ -41,11 +41,12 @@ class ThemeManager { QList getValidApplicationThemes(); void setIconTheme(const QString& name); void applyCurrentlySelectedTheme(); - void setApplicationTheme(const QString& name, bool initial); + void setApplicationTheme(const QString& name); private: std::map> m_themes; MainWindow* m_mainWindow; + bool m_firstThemeInitialized; QString AddTheme(std::unique_ptr theme); ITheme* GetTheme(QString themeId); diff --git a/launcher/ui/widgets/ThemeCustomizationWidget.cpp b/launcher/ui/widgets/ThemeCustomizationWidget.cpp new file mode 100644 index 00000000..0830a030 --- /dev/null +++ b/launcher/ui/widgets/ThemeCustomizationWidget.cpp @@ -0,0 +1,135 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 Tayou + * + * 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 "ThemeCustomizationWidget.h" +#include "ui_ThemeCustomizationWidget.h" + +#include "Application.h" +#include "ui/themes/ITheme.h" + +ThemeCustomizationWidget::ThemeCustomizationWidget(QWidget *parent) : QWidget(parent), ui(new Ui::ThemeCustomizationWidget) +{ + ui->setupUi(this); + 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); +} + +ThemeCustomizationWidget::~ThemeCustomizationWidget() +{ + delete ui; +} + +void ThemeCustomizationWidget::showFeatures(ThemeFields features) { + ui->iconsComboBox->setVisible(features & ThemeFields::ICONS); + ui->iconsLabel->setVisible(features & ThemeFields::ICONS); + ui->widgetStyleComboBox->setVisible(features & ThemeFields::WIDGETS); + ui->widgetThemeLabel->setVisible(features & ThemeFields::WIDGETS); + ui->backgroundCatComboBox->setVisible(features & ThemeFields::CAT); + ui->backgroundCatLabel->setVisible(features & ThemeFields::CAT); +} + +void ThemeCustomizationWidget::applyIconTheme(int index) { + emit currentIconThemeChanged(index); + + auto settings = APPLICATION->settings(); + auto original = settings->get("IconTheme").toString(); + // FIXME: make generic + settings->set("IconTheme", m_iconThemeOptions[index]); + + if (original != settings->get("IconTheme")) { + APPLICATION->applyCurrentlySelectedTheme(); + } +} + +void ThemeCustomizationWidget::applyWidgetTheme(int index) { + emit currentWidgetThemeChanged(index); + + auto settings = APPLICATION->settings(); + auto originalAppTheme = settings->get("ApplicationTheme").toString(); + auto newAppTheme = ui->widgetStyleComboBox->currentData().toString(); + if (originalAppTheme != newAppTheme) { + settings->set("ApplicationTheme", newAppTheme); + APPLICATION->applyCurrentlySelectedTheme(); + } +} + +void ThemeCustomizationWidget::applyCatTheme(int index) { + emit currentCatChanged(index); + + auto settings = APPLICATION->settings(); + switch (index) { + case 0: // original cat + settings->set("BackgroundCat", "kitteh"); + break; + case 1: // rory the cat + settings->set("BackgroundCat", "rory"); + break; + case 2: // rory the cat flat edition + settings->set("BackgroundCat", "rory-flat"); + break; + case 3: // teawie + settings->set("BackgroundCat", "teawie"); + break; + } +} + +void ThemeCustomizationWidget::applySettings() +{ + applyIconTheme(ui->iconsComboBox->currentIndex()); + applyWidgetTheme(ui->widgetStyleComboBox->currentIndex()); + applyCatTheme(ui->backgroundCatComboBox->currentIndex()); +} +void ThemeCustomizationWidget::loadSettings() +{ + auto settings = APPLICATION->settings(); + + // FIXME: make generic + auto theme = settings->get("IconTheme").toString(); + ui->iconsComboBox->setCurrentIndex(m_iconThemeOptions.indexOf(theme)); + + { + auto currentTheme = settings->get("ApplicationTheme").toString(); + auto themes = APPLICATION->getValidApplicationThemes(); + int idx = 0; + for (auto& theme : themes) { + ui->widgetStyleComboBox->addItem(theme->name(), theme->id()); + if (currentTheme == theme->id()) { + ui->widgetStyleComboBox->setCurrentIndex(idx); + } + idx++; + } + } + + auto cat = settings->get("BackgroundCat").toString(); + if (cat == "kitteh") { + ui->backgroundCatComboBox->setCurrentIndex(0); + } else if (cat == "rory") { + ui->backgroundCatComboBox->setCurrentIndex(1); + } else if (cat == "rory-flat") { + ui->backgroundCatComboBox->setCurrentIndex(2); + } else if (cat == "teawie") { + ui->backgroundCatComboBox->setCurrentIndex(3); + } +} + +void ThemeCustomizationWidget::retranslate() +{ + ui->retranslateUi(this); +} \ No newline at end of file diff --git a/launcher/ui/widgets/ThemeCustomizationWidget.h b/launcher/ui/widgets/ThemeCustomizationWidget.h new file mode 100644 index 00000000..e17286e1 --- /dev/null +++ b/launcher/ui/widgets/ThemeCustomizationWidget.h @@ -0,0 +1,64 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 Tayou + * + * 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 + +enum ThemeFields { + NONE = 0b0000, + ICONS = 0b0001, + WIDGETS = 0b0010, + CAT = 0b0100 +}; + +namespace Ui { +class ThemeCustomizationWidget; +} + +class ThemeCustomizationWidget : public QWidget +{ + Q_OBJECT + +public: + explicit ThemeCustomizationWidget(QWidget *parent = nullptr); + ~ThemeCustomizationWidget(); + + void showFeatures(ThemeFields features); + + void applySettings(); + + void loadSettings(); + void retranslate(); + + Ui::ThemeCustomizationWidget *ui; + +private slots: + void applyIconTheme(int index); + void applyWidgetTheme(int index); + void applyCatTheme(int index); + +signals: + int currentIconThemeChanged(int index); + int currentWidgetThemeChanged(int index); + int currentCatChanged(int index); + +private: + + QStringList m_iconThemeOptions{ "pe_colored", "pe_light", "pe_dark", "pe_blue", "breeze_light", "breeze_dark", "OSX", "iOS", "flat", "flat_white", "multimc", "custom" }; +}; diff --git a/launcher/ui/widgets/ThemeCustomizationWidget.ui b/launcher/ui/widgets/ThemeCustomizationWidget.ui new file mode 100644 index 00000000..c184b8f3 --- /dev/null +++ b/launcher/ui/widgets/ThemeCustomizationWidget.ui @@ -0,0 +1,182 @@ + + + ThemeCustomizationWidget + + + + 0 + 0 + 400 + 311 + + + + Form + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + &Icons + + + iconsComboBox + + + + + + + + 0 + 0 + + + + Qt::StrongFocus + + + + Simple (Colored Icons) + + + + + Simple (Light Icons) + + + + + Simple (Dark Icons) + + + + + Simple (Blue Icons) + + + + + Breeze Light + + + + + Breeze Dark + + + + + OSX + + + + + iOS + + + + + Flat + + + + + Flat (White) + + + + + Legacy + + + + + Custom + + + + + + + + &Colors + + + widgetStyleComboBox + + + + + + + + 0 + 0 + + + + Qt::StrongFocus + + + + + + + C&at + + + backgroundCatComboBox + + + + + + + + 0 + 0 + + + + Qt::StrongFocus + + + + Background Cat (from MultiMC) + + + + + Rory ID 11 (drawn by Ashtaka) + + + + + Rory ID 11 (flat edition, drawn by Ashtaka) + + + + + Teawie (drawn by SympathyTea) + + + + + + + + + From 49d317b19aa61fed056e0f14c12eb1997f68982d Mon Sep 17 00:00:00 2001 From: Tayou Date: Mon, 9 Jan 2023 16:54:10 +0100 Subject: [PATCH 067/152] UX tweak + formatting + added cat to wizard Signed-off-by: Tayou --- launcher/ui/MainWindow.cpp | 16 +--- launcher/ui/setupwizard/ThemeWizardPage.cpp | 39 ++++++--- launcher/ui/setupwizard/ThemeWizardPage.h | 1 + launcher/ui/setupwizard/ThemeWizardPage.ui | 26 +++++- launcher/ui/themes/CustomTheme.cpp | 31 +++----- launcher/ui/themes/ITheme.h | 12 +-- launcher/ui/themes/SystemTheme.cpp | 12 ++- launcher/ui/themes/SystemTheme.h | 8 +- launcher/ui/themes/ThemeManager.h | 4 +- .../ui/widgets/ThemeCustomizationWidget.cpp | 79 ++++++++++--------- .../ui/widgets/ThemeCustomizationWidget.h | 1 + .../ui/widgets/ThemeCustomizationWidget.ui | 5 +- 12 files changed, 127 insertions(+), 107 deletions(-) diff --git a/launcher/ui/MainWindow.cpp b/launcher/ui/MainWindow.cpp index 331ca0e1..a921e378 100644 --- a/launcher/ui/MainWindow.cpp +++ b/launcher/ui/MainWindow.cpp @@ -1652,16 +1652,6 @@ void MainWindow::onCatToggled(bool state) APPLICATION->settings()->set("TheCat", state); } -namespace { -template -T non_stupid_abs(T in) -{ - if (in < 0) - return -in; - return in; -} -} - void MainWindow::setCatBackground(bool enabled) { if (enabled) @@ -1671,11 +1661,11 @@ void MainWindow::setCatBackground(bool enabled) QDateTime xmas(QDate(now.date().year(), 12, 25), QTime(0, 0)); QDateTime halloween(QDate(now.date().year(), 10, 31), QTime(0, 0)); QString cat = APPLICATION->settings()->get("BackgroundCat").toString(); - if (non_stupid_abs(now.daysTo(xmas)) <= 4) { + if (std::abs(now.daysTo(xmas)) <= 4) { cat += "-xmas"; - } else if (non_stupid_abs(now.daysTo(halloween)) <= 4) { + } else if (std::abs(now.daysTo(halloween)) <= 4) { cat += "-spooky"; - } else if (non_stupid_abs(now.daysTo(birthday)) <= 12) { + } else if (std::abs(now.daysTo(birthday)) <= 12) { cat += "-bday"; } view->setStyleSheet(QString(R"( diff --git a/launcher/ui/setupwizard/ThemeWizardPage.cpp b/launcher/ui/setupwizard/ThemeWizardPage.cpp index 6f041134..4e1eb488 100644 --- a/launcher/ui/setupwizard/ThemeWizardPage.cpp +++ b/launcher/ui/setupwizard/ThemeWizardPage.cpp @@ -23,31 +23,31 @@ #include "ui/widgets/ThemeCustomizationWidget.h" #include "ui_ThemeCustomizationWidget.h" -ThemeWizardPage::ThemeWizardPage(QWidget *parent) : -BaseWizardPage(parent), -ui(new Ui::ThemeWizardPage) { +ThemeWizardPage::ThemeWizardPage(QWidget* parent) : BaseWizardPage(parent), ui(new Ui::ThemeWizardPage) +{ ui->setupUi(this); - ui->themeCustomizationWidget->showFeatures((ThemeFields)(ThemeFields::ICONS | ThemeFields::WIDGETS)); connect(ui->themeCustomizationWidget, QOverload::of(&ThemeCustomizationWidget::currentIconThemeChanged), this, &ThemeWizardPage::updateIcons); + connect(ui->themeCustomizationWidget, QOverload::of(&ThemeCustomizationWidget::currentCatChanged), this, &ThemeWizardPage::updateCat); updateIcons(); + updateCat(); } -ThemeWizardPage::~ThemeWizardPage() { -delete ui; -} - -void ThemeWizardPage::initializePage() +ThemeWizardPage::~ThemeWizardPage() { + delete ui; } +void ThemeWizardPage::initializePage() {} + bool ThemeWizardPage::validatePage() { return true; } -void ThemeWizardPage::updateIcons() { +void ThemeWizardPage::updateIcons() +{ qDebug() << "Setting Icons"; ui->previewIconButton0->setIcon(APPLICATION->getThemedIcon("new")); ui->previewIconButton1->setIcon(APPLICATION->getThemedIcon("centralmods")); @@ -64,6 +64,25 @@ void ThemeWizardPage::updateIcons() { parentWidget()->update(); } +void ThemeWizardPage::updateCat() +{ + qDebug() << "Setting Cat"; + + QDateTime now = QDateTime::currentDateTime(); + QDateTime birthday(QDate(now.date().year(), 11, 30), QTime(0, 0)); + QDateTime xmas(QDate(now.date().year(), 12, 25), QTime(0, 0)); + QDateTime halloween(QDate(now.date().year(), 10, 31), QTime(0, 0)); + QString cat = APPLICATION->settings()->get("BackgroundCat").toString(); + if (std::abs(now.daysTo(xmas)) <= 4) { + cat += "-xmas"; + } else if (std::abs(now.daysTo(halloween)) <= 4) { + cat += "-spooky"; + } else if (std::abs(now.daysTo(birthday)) <= 12) { + cat += "-bday"; + } + ui->catImagePreviewButton->setIcon(QIcon(QString(R"(:/backgrounds/%1)").arg(cat))); +} + void ThemeWizardPage::retranslate() { ui->retranslateUi(this); diff --git a/launcher/ui/setupwizard/ThemeWizardPage.h b/launcher/ui/setupwizard/ThemeWizardPage.h index 10913d1b..6562ad2e 100644 --- a/launcher/ui/setupwizard/ThemeWizardPage.h +++ b/launcher/ui/setupwizard/ThemeWizardPage.h @@ -38,6 +38,7 @@ public: private slots: void updateIcons(); + void updateCat(); private: Ui::ThemeWizardPage *ui; diff --git a/launcher/ui/setupwizard/ThemeWizardPage.ui b/launcher/ui/setupwizard/ThemeWizardPage.ui index b743644f..95b0f805 100644 --- a/launcher/ui/setupwizard/ThemeWizardPage.ui +++ b/launcher/ui/setupwizard/ThemeWizardPage.ui @@ -6,8 +6,8 @@ 0 0 - 400 - 300 + 510 + 552

vyR)8njw1Xb5UkHS*Owd`&7mHZ!yPnTn^8 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‰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 Date: Sat, 7 Jan 2023 04:38:26 -0500 Subject: [PATCH 054/152] fix: make spooky teawie load gimp fail Signed-off-by: seth --- .../resources/backgrounds/teawie-spooky.png | Bin 183698 -> 204756 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/launcher/resources/backgrounds/teawie-spooky.png b/launcher/resources/backgrounds/teawie-spooky.png index 9c57103e00832e2c2c676f3852f37733c7086265..cefc6c855789b3bbb325dece0664542d64ebf025 100644 GIT binary patch delta 21242 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 From f5955a4738ee5164c5c391086f1c4c0dde6d91a8 Mon Sep 17 00:00:00 2001 From: seth Date: Sat, 7 Jan 2023 04:41:09 -0500 Subject: [PATCH 055/152] feat: add bday teawie Signed-off-by: seth --- launcher/resources/backgrounds/backgrounds.qrc | 1 + launcher/resources/backgrounds/teawie-bday.png | Bin 0 -> 190586 bytes 2 files changed, 1 insertion(+) create mode 100644 launcher/resources/backgrounds/teawie-bday.png diff --git a/launcher/resources/backgrounds/backgrounds.qrc b/launcher/resources/backgrounds/backgrounds.qrc index dc16e788..83096aef 100644 --- a/launcher/resources/backgrounds/backgrounds.qrc +++ b/launcher/resources/backgrounds/backgrounds.qrc @@ -15,6 +15,7 @@ rory-flat-spooky.png teawie.png teawie-xmas.png + teawie-bday.png teawie-spooky.png diff --git a/launcher/resources/backgrounds/teawie-bday.png b/launcher/resources/backgrounds/teawie-bday.png new file mode 100644 index 0000000000000000000000000000000000000000..f4ecf247cba21002a7510a8cd0020f3415e58575 GIT binary patch literal 190586 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%0DWq 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%0DZE8GC%PRwA6 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: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

@@ -309,6 +309,28 @@
+ + + + + 0 + 256 + + + + + + + + 256 + 256 + + + + true + + + diff --git a/launcher/ui/themes/CustomTheme.cpp b/launcher/ui/themes/CustomTheme.cpp index 3ad61668..198e76ba 100644 --- a/launcher/ui/themes/CustomTheme.cpp +++ b/launcher/ui/themes/CustomTheme.cpp @@ -167,8 +167,6 @@ CustomTheme::CustomTheme(ITheme* baseTheme, QFileInfo& fileInfo, bool isManifest if (!FS::ensureFolderPathExists(path) || !FS::ensureFolderPathExists(pathResources)) { themeWarningLog() << "couldn't create folder for theme!"; - m_palette = baseTheme->colorScheme(); - m_styleSheet = baseTheme->appStyleSheet(); return; } @@ -177,18 +175,15 @@ CustomTheme::CustomTheme(ITheme* baseTheme, QFileInfo& fileInfo, bool isManifest bool jsonDataIncomplete = false; m_palette = baseTheme->colorScheme(); - if (!readThemeJson(themeFilePath, m_palette, m_fadeAmount, m_fadeColor, m_name, m_widgets, m_qssFilePath, jsonDataIncomplete)) { - themeDebugLog() << "Did not read theme json file correctly, writing new one to: " << themeFilePath; - m_name = "Custom"; - m_palette = baseTheme->colorScheme(); - m_fadeColor = baseTheme->fadeColor(); - m_fadeAmount = baseTheme->fadeAmount(); - m_widgets = baseTheme->qtTheme(); - m_qssFilePath = "themeStyle.css"; - } else { + if (readThemeJson(themeFilePath, m_palette, m_fadeAmount, m_fadeColor, m_name, m_widgets, m_qssFilePath, jsonDataIncomplete)) { + // If theme data was found, fade "Disabled" color of each role according to FadeAmount m_palette = fadeInactive(m_palette, m_fadeAmount, m_fadeColor); + } else { + themeDebugLog() << "Did not read theme json file correctly, not changing theme, keeping previous."; + return; } + // FIXME: This is kinda jank, it only actually checks if the qss file path is not present. It should actually check for any relevant missing data (e.g. name, colors) if (jsonDataIncomplete) { writeThemeJson(fileInfo.absoluteFilePath(), m_palette, m_fadeAmount, m_fadeColor, m_name, m_widgets, m_qssFilePath); } @@ -197,20 +192,14 @@ CustomTheme::CustomTheme(ITheme* baseTheme, QFileInfo& fileInfo, bool isManifest QFileInfo info(qssFilePath); if (info.isFile()) { try { - // TODO: validate css? + // TODO: validate qss? m_styleSheet = QString::fromUtf8(FS::read(qssFilePath)); } catch (const Exception& e) { - themeWarningLog() << "Couldn't load css:" << e.cause() << "from" << qssFilePath; - m_styleSheet = baseTheme->appStyleSheet(); + themeWarningLog() << "Couldn't load qss:" << e.cause() << "from" << qssFilePath; + return; } } else { - themeDebugLog() << "No theme css present."; - m_styleSheet = baseTheme->appStyleSheet(); - try { - FS::write(qssFilePath, m_styleSheet.toUtf8()); - } catch (const Exception& e) { - themeWarningLog() << "Couldn't write css:" << e.cause() << "to" << qssFilePath; - } + themeDebugLog() << "No theme qss present."; } } else { m_id = fileInfo.fileName(); diff --git a/launcher/ui/themes/ITheme.h b/launcher/ui/themes/ITheme.h index bb5c8afe..2e5b7f25 100644 --- a/launcher/ui/themes/ITheme.h +++ b/launcher/ui/themes/ITheme.h @@ -33,14 +33,13 @@ * limitations under the License. */ #pragma once -#include #include +#include class QStyle; -class ITheme -{ -public: +class ITheme { + public: virtual ~ITheme() {} virtual void apply(); virtual QString id() = 0; @@ -52,10 +51,7 @@ public: virtual QPalette colorScheme() = 0; virtual QColor fadeColor() = 0; virtual double fadeAmount() = 0; - virtual QStringList searchPaths() - { - return {}; - } + virtual QStringList searchPaths() { return {}; } static QPalette fadeInactive(QPalette in, qreal bias, QColor color); }; diff --git a/launcher/ui/themes/SystemTheme.cpp b/launcher/ui/themes/SystemTheme.cpp index d6ef442b..24875e33 100644 --- a/launcher/ui/themes/SystemTheme.cpp +++ b/launcher/ui/themes/SystemTheme.cpp @@ -34,24 +34,22 @@ */ #include "SystemTheme.h" #include +#include #include #include -#include #include "ThemeManager.h" SystemTheme::SystemTheme() { themeDebugLog() << "Determining System Theme..."; - const auto & style = QApplication::style(); + const auto& style = QApplication::style(); systemPalette = style->standardPalette(); QString lowerThemeName = style->objectName(); themeDebugLog() << "System theme seems to be:" << lowerThemeName; QStringList styles = QStyleFactory::keys(); - for(auto &st: styles) - { + for (auto& st : styles) { themeDebugLog() << "Considering theme from theme factory:" << st.toLower(); - if(st.toLower() == lowerThemeName) - { + if (st.toLower() == lowerThemeName) { systemTheme = st; themeDebugLog() << "System theme has been determined to be:" << systemTheme; return; @@ -99,7 +97,7 @@ double SystemTheme::fadeAmount() QColor SystemTheme::fadeColor() { - return QColor(128,128,128); + return QColor(128, 128, 128); } bool SystemTheme::hasStyleSheet() diff --git a/launcher/ui/themes/SystemTheme.h b/launcher/ui/themes/SystemTheme.h index 5c9216eb..b5c03def 100644 --- a/launcher/ui/themes/SystemTheme.h +++ b/launcher/ui/themes/SystemTheme.h @@ -36,9 +36,8 @@ #include "ITheme.h" -class SystemTheme: public ITheme -{ -public: +class SystemTheme : public ITheme { + public: SystemTheme(); virtual ~SystemTheme() {} void apply() override; @@ -52,7 +51,8 @@ public: QPalette colorScheme() override; double fadeAmount() override; QColor fadeColor() override; -private: + + private: QPalette systemPalette; QString systemTheme; }; diff --git a/launcher/ui/themes/ThemeManager.h b/launcher/ui/themes/ThemeManager.h index 0a70ddfc..bb10cd48 100644 --- a/launcher/ui/themes/ThemeManager.h +++ b/launcher/ui/themes/ThemeManager.h @@ -35,9 +35,6 @@ class ThemeManager { public: ThemeManager(MainWindow* mainWindow); - // maybe make private? Or put in ctor? - void InitializeThemes(); - QList getValidApplicationThemes(); void setIconTheme(const QString& name); void applyCurrentlySelectedTheme(); @@ -48,6 +45,7 @@ class ThemeManager { MainWindow* m_mainWindow; bool m_firstThemeInitialized; + void InitializeThemes(); QString AddTheme(std::unique_ptr theme); ITheme* GetTheme(QString themeId); }; diff --git a/launcher/ui/widgets/ThemeCustomizationWidget.cpp b/launcher/ui/widgets/ThemeCustomizationWidget.cpp index 0830a030..eafcf482 100644 --- a/launcher/ui/widgets/ThemeCustomizationWidget.cpp +++ b/launcher/ui/widgets/ThemeCustomizationWidget.cpp @@ -36,18 +36,40 @@ ThemeCustomizationWidget::~ThemeCustomizationWidget() delete ui; } +/// +/// The layout was not quite right, so currently this just disables the UI elements, which should be hidden instead +/// TODO FIXME +/// +/// Original Method One: +/// ui->iconsComboBox->setVisible(features& ThemeFields::ICONS); +/// ui->iconsLabel->setVisible(features& ThemeFields::ICONS); +/// ui->widgetStyleComboBox->setVisible(features& ThemeFields::WIDGETS); +/// ui->widgetThemeLabel->setVisible(features& ThemeFields::WIDGETS); +/// ui->backgroundCatComboBox->setVisible(features& ThemeFields::CAT); +/// ui->backgroundCatLabel->setVisible(features& ThemeFields::CAT); +/// +/// original Method Two: +/// if (!(features & ThemeFields::ICONS)) { +/// ui->formLayout->setRowVisible(0, false); +/// } +/// if (!(features & ThemeFields::WIDGETS)) { +/// ui->formLayout->setRowVisible(1, false); +/// } +/// if (!(features & ThemeFields::CAT)) { +/// ui->formLayout->setRowVisible(2, false); +/// } +/// +/// void ThemeCustomizationWidget::showFeatures(ThemeFields features) { - ui->iconsComboBox->setVisible(features & ThemeFields::ICONS); - ui->iconsLabel->setVisible(features & ThemeFields::ICONS); - ui->widgetStyleComboBox->setVisible(features & ThemeFields::WIDGETS); - ui->widgetThemeLabel->setVisible(features & ThemeFields::WIDGETS); - ui->backgroundCatComboBox->setVisible(features & ThemeFields::CAT); - ui->backgroundCatLabel->setVisible(features & ThemeFields::CAT); + ui->iconsComboBox->setEnabled(features & ThemeFields::ICONS); + ui->iconsLabel->setEnabled(features & ThemeFields::ICONS); + ui->widgetStyleComboBox->setEnabled(features & ThemeFields::WIDGETS); + ui->widgetThemeLabel->setEnabled(features & ThemeFields::WIDGETS); + ui->backgroundCatComboBox->setEnabled(features & ThemeFields::CAT); + ui->backgroundCatLabel->setEnabled(features & ThemeFields::CAT); } void ThemeCustomizationWidget::applyIconTheme(int index) { - emit currentIconThemeChanged(index); - auto settings = APPLICATION->settings(); auto original = settings->get("IconTheme").toString(); // FIXME: make generic @@ -56,11 +78,11 @@ void ThemeCustomizationWidget::applyIconTheme(int index) { if (original != settings->get("IconTheme")) { APPLICATION->applyCurrentlySelectedTheme(); } + + emit currentIconThemeChanged(index); } void ThemeCustomizationWidget::applyWidgetTheme(int index) { - emit currentWidgetThemeChanged(index); - auto settings = APPLICATION->settings(); auto originalAppTheme = settings->get("ApplicationTheme").toString(); auto newAppTheme = ui->widgetStyleComboBox->currentData().toString(); @@ -68,26 +90,15 @@ void ThemeCustomizationWidget::applyWidgetTheme(int index) { settings->set("ApplicationTheme", newAppTheme); APPLICATION->applyCurrentlySelectedTheme(); } + + emit currentWidgetThemeChanged(index); } void ThemeCustomizationWidget::applyCatTheme(int index) { - emit currentCatChanged(index); - auto settings = APPLICATION->settings(); - switch (index) { - case 0: // original cat - settings->set("BackgroundCat", "kitteh"); - break; - case 1: // rory the cat - settings->set("BackgroundCat", "rory"); - break; - case 2: // rory the cat flat edition - settings->set("BackgroundCat", "rory-flat"); - break; - case 3: // teawie - settings->set("BackgroundCat", "teawie"); - break; - } + settings->set("BackgroundCat", m_catOptions[index]); + + emit currentCatChanged(index); } void ThemeCustomizationWidget::applySettings() @@ -101,8 +112,8 @@ void ThemeCustomizationWidget::loadSettings() auto settings = APPLICATION->settings(); // FIXME: make generic - auto theme = settings->get("IconTheme").toString(); - ui->iconsComboBox->setCurrentIndex(m_iconThemeOptions.indexOf(theme)); + auto iconTheme = settings->get("IconTheme").toString(); + ui->iconsComboBox->setCurrentIndex(m_iconThemeOptions.indexOf(iconTheme)); { auto currentTheme = settings->get("ApplicationTheme").toString(); @@ -118,18 +129,10 @@ void ThemeCustomizationWidget::loadSettings() } auto cat = settings->get("BackgroundCat").toString(); - if (cat == "kitteh") { - ui->backgroundCatComboBox->setCurrentIndex(0); - } else if (cat == "rory") { - ui->backgroundCatComboBox->setCurrentIndex(1); - } else if (cat == "rory-flat") { - ui->backgroundCatComboBox->setCurrentIndex(2); - } else if (cat == "teawie") { - ui->backgroundCatComboBox->setCurrentIndex(3); - } + ui->backgroundCatComboBox->setCurrentIndex(m_catOptions.indexOf(cat)); } void ThemeCustomizationWidget::retranslate() { ui->retranslateUi(this); -} \ No newline at end of file +} diff --git a/launcher/ui/widgets/ThemeCustomizationWidget.h b/launcher/ui/widgets/ThemeCustomizationWidget.h index e17286e1..653e89e7 100644 --- a/launcher/ui/widgets/ThemeCustomizationWidget.h +++ b/launcher/ui/widgets/ThemeCustomizationWidget.h @@ -61,4 +61,5 @@ signals: private: QStringList m_iconThemeOptions{ "pe_colored", "pe_light", "pe_dark", "pe_blue", "breeze_light", "breeze_dark", "OSX", "iOS", "flat", "flat_white", "multimc", "custom" }; + QStringList m_catOptions{ "kitteh", "rory", "rory-flat" }; }; diff --git a/launcher/ui/widgets/ThemeCustomizationWidget.ui b/launcher/ui/widgets/ThemeCustomizationWidget.ui index c184b8f3..9cc5cc76 100644 --- a/launcher/ui/widgets/ThemeCustomizationWidget.ui +++ b/launcher/ui/widgets/ThemeCustomizationWidget.ui @@ -11,9 +11,12 @@ - Form + Form + + QLayout::SetMinimumSize + 0 From 6daa45783894fc7517917d6f6df0deaac1a41ba3 Mon Sep 17 00:00:00 2001 From: Tayou Date: Mon, 9 Jan 2023 16:58:27 +0100 Subject: [PATCH 068/152] Implement Suggestions from flow & Scrumplex Signed-off-by: Tayou --- launcher/Application.cpp | 7 +- launcher/ui/MainWindow.cpp | 22 +---- launcher/ui/setupwizard/ThemeWizardPage.cpp | 27 +------ launcher/ui/setupwizard/ThemeWizardPage.h | 16 ++-- launcher/ui/themes/ThemeManager.cpp | 35 +++++--- launcher/ui/themes/ThemeManager.h | 9 ++- .../ui/widgets/ThemeCustomizationWidget.cpp | 22 +++-- .../ui/widgets/ThemeCustomizationWidget.h | 50 +++++++----- .../ui/widgets/ThemeCustomizationWidget.ui | 80 ------------------- 9 files changed, 97 insertions(+), 171 deletions(-) diff --git a/launcher/Application.cpp b/launcher/Application.cpp index 3e64b74f..f2cc7bfb 100644 --- a/launcher/Application.cpp +++ b/launcher/Application.cpp @@ -498,7 +498,7 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv) // Theming m_settings->registerSetting("IconTheme", QString("pe_colored")); - m_settings->registerSetting("ApplicationTheme", QString("system")); + m_settings->registerSetting("ApplicationTheme"); m_settings->registerSetting("BackgroundCat", QString("kitteh")); // Remembered state @@ -890,8 +890,8 @@ bool Application::createSetupWizard() return false; }(); bool pasteInterventionRequired = settings()->get("PastebinURL") != ""; - bool themeInterventionRequired = settings()->get("ApplicationTheme") != ""; - bool wizardRequired = javaRequired || languageRequired || pasteInterventionRequired; + bool themeInterventionRequired = settings()->get("ApplicationTheme") == ""; + bool wizardRequired = javaRequired || languageRequired || pasteInterventionRequired || themeInterventionRequired; if(wizardRequired) { @@ -913,6 +913,7 @@ bool Application::createSetupWizard() if (themeInterventionRequired) { + settings()->set("ApplicationTheme", QString("system")); // set default theme after going into theme wizard m_setupWizard->addPage(new ThemeWizardPage(m_setupWizard)); } connect(m_setupWizard, &QDialog::finished, this, &Application::setupWizardFinished); diff --git a/launcher/ui/MainWindow.cpp b/launcher/ui/MainWindow.cpp index a921e378..ab80fb80 100644 --- a/launcher/ui/MainWindow.cpp +++ b/launcher/ui/MainWindow.cpp @@ -111,6 +111,7 @@ #include "ui/dialogs/ExportInstanceDialog.h" #include "ui/dialogs/ImportResourcePackDialog.h" #include "ui/themes/ITheme.h" +#include "ui/themes/ThemeManager.h" #include #include @@ -1654,20 +1655,7 @@ void MainWindow::onCatToggled(bool state) void MainWindow::setCatBackground(bool enabled) { - if (enabled) - { - QDateTime now = QDateTime::currentDateTime(); - QDateTime birthday(QDate(now.date().year(), 11, 30), QTime(0, 0)); - QDateTime xmas(QDate(now.date().year(), 12, 25), QTime(0, 0)); - QDateTime halloween(QDate(now.date().year(), 10, 31), QTime(0, 0)); - QString cat = APPLICATION->settings()->get("BackgroundCat").toString(); - if (std::abs(now.daysTo(xmas)) <= 4) { - cat += "-xmas"; - } else if (std::abs(now.daysTo(halloween)) <= 4) { - cat += "-spooky"; - } else if (std::abs(now.daysTo(birthday)) <= 12) { - cat += "-bday"; - } + if (enabled) { view->setStyleSheet(QString(R"( InstanceView { @@ -1678,10 +1666,8 @@ InstanceView background-repeat: none; background-color:palette(base); })") - .arg(cat)); - } - else - { + .arg(ThemeManager::getCatImage())); + } else { view->setStyleSheet(QString()); } } diff --git a/launcher/ui/setupwizard/ThemeWizardPage.cpp b/launcher/ui/setupwizard/ThemeWizardPage.cpp index 4e1eb488..cc2d335b 100644 --- a/launcher/ui/setupwizard/ThemeWizardPage.cpp +++ b/launcher/ui/setupwizard/ThemeWizardPage.cpp @@ -20,6 +20,7 @@ #include "Application.h" #include "ui/themes/ITheme.h" +#include "ui/themes/ThemeManager.h" #include "ui/widgets/ThemeCustomizationWidget.h" #include "ui_ThemeCustomizationWidget.h" @@ -27,8 +28,8 @@ ThemeWizardPage::ThemeWizardPage(QWidget* parent) : BaseWizardPage(parent), ui(n { ui->setupUi(this); - connect(ui->themeCustomizationWidget, QOverload::of(&ThemeCustomizationWidget::currentIconThemeChanged), this, &ThemeWizardPage::updateIcons); - connect(ui->themeCustomizationWidget, QOverload::of(&ThemeCustomizationWidget::currentCatChanged), this, &ThemeWizardPage::updateCat); + connect(ui->themeCustomizationWidget, &ThemeCustomizationWidget::currentIconThemeChanged, this, &ThemeWizardPage::updateIcons); + connect(ui->themeCustomizationWidget, &ThemeCustomizationWidget::currentCatChanged, this, &ThemeWizardPage::updateCat); updateIcons(); updateCat(); @@ -39,13 +40,6 @@ ThemeWizardPage::~ThemeWizardPage() delete ui; } -void ThemeWizardPage::initializePage() {} - -bool ThemeWizardPage::validatePage() -{ - return true; -} - void ThemeWizardPage::updateIcons() { qDebug() << "Setting Icons"; @@ -67,20 +61,7 @@ void ThemeWizardPage::updateIcons() void ThemeWizardPage::updateCat() { qDebug() << "Setting Cat"; - - QDateTime now = QDateTime::currentDateTime(); - QDateTime birthday(QDate(now.date().year(), 11, 30), QTime(0, 0)); - QDateTime xmas(QDate(now.date().year(), 12, 25), QTime(0, 0)); - QDateTime halloween(QDate(now.date().year(), 10, 31), QTime(0, 0)); - QString cat = APPLICATION->settings()->get("BackgroundCat").toString(); - if (std::abs(now.daysTo(xmas)) <= 4) { - cat += "-xmas"; - } else if (std::abs(now.daysTo(halloween)) <= 4) { - cat += "-spooky"; - } else if (std::abs(now.daysTo(birthday)) <= 12) { - cat += "-bday"; - } - ui->catImagePreviewButton->setIcon(QIcon(QString(R"(:/backgrounds/%1)").arg(cat))); + ui->catImagePreviewButton->setIcon(QIcon(QString(R"(:/backgrounds/%1)").arg(ThemeManager::getCatImage()))); } void ThemeWizardPage::retranslate() diff --git a/launcher/ui/setupwizard/ThemeWizardPage.h b/launcher/ui/setupwizard/ThemeWizardPage.h index 6562ad2e..992ba2ca 100644 --- a/launcher/ui/setupwizard/ThemeWizardPage.h +++ b/launcher/ui/setupwizard/ThemeWizardPage.h @@ -24,22 +24,20 @@ namespace Ui { class ThemeWizardPage; } -class ThemeWizardPage : public BaseWizardPage -{ +class ThemeWizardPage : public BaseWizardPage { Q_OBJECT -public: - explicit ThemeWizardPage(QWidget *parent = nullptr); + public: + explicit ThemeWizardPage(QWidget* parent = nullptr); ~ThemeWizardPage(); - void initializePage() override; - bool validatePage() override; + bool validatePage() override { return true; }; void retranslate() override; -private slots: + private slots: void updateIcons(); void updateCat(); -private: - Ui::ThemeWizardPage *ui; + private: + Ui::ThemeWizardPage* ui; }; diff --git a/launcher/ui/themes/ThemeManager.cpp b/launcher/ui/themes/ThemeManager.cpp index a6cebc6f..44c13f40 100644 --- a/launcher/ui/themes/ThemeManager.cpp +++ b/launcher/ui/themes/ThemeManager.cpp @@ -31,13 +31,13 @@ ThemeManager::ThemeManager(MainWindow* mainWindow) { m_mainWindow = mainWindow; - InitializeThemes(); + initializeThemes(); } /// @brief Adds the Theme to the list of themes /// @param theme The Theme to add /// @return Theme ID -QString ThemeManager::AddTheme(std::unique_ptr theme) +QString ThemeManager::addTheme(std::unique_ptr theme) { QString id = theme->id(); m_themes.emplace(id, std::move(theme)); @@ -47,12 +47,12 @@ QString ThemeManager::AddTheme(std::unique_ptr theme) /// @brief Gets the Theme from the List via ID /// @param themeId Theme ID of theme to fetch /// @return Theme at themeId -ITheme* ThemeManager::GetTheme(QString themeId) +ITheme* ThemeManager::getTheme(QString themeId) { return m_themes[themeId].get(); } -void ThemeManager::InitializeThemes() +void ThemeManager::initializeThemes() { // Icon themes { @@ -67,10 +67,10 @@ void ThemeManager::InitializeThemes() // Initialize widget themes { themeDebugLog() << "<> Initializing Widget Themes"; - themeDebugLog() << "Loading Built-in Theme:" << AddTheme(std::make_unique()); - auto darkThemeId = AddTheme(std::make_unique()); + themeDebugLog() << "Loading Built-in Theme:" << addTheme(std::make_unique()); + auto darkThemeId = addTheme(std::make_unique()); themeDebugLog() << "Loading Built-in Theme:" << darkThemeId; - themeDebugLog() << "Loading Built-in Theme:" << AddTheme(std::make_unique()); + themeDebugLog() << "Loading Built-in Theme:" << addTheme(std::make_unique()); // TODO: need some way to differentiate same name themes in different subdirectories (maybe smaller grey text next to theme name in // dropdown?) @@ -84,7 +84,7 @@ void ThemeManager::InitializeThemes() if (themeJson.exists()) { // Load "theme.json" based themes themeDebugLog() << "Loading JSON Theme from:" << themeJson.absoluteFilePath(); - AddTheme(std::make_unique(GetTheme(darkThemeId), themeJson, true)); + addTheme(std::make_unique(getTheme(darkThemeId), themeJson, true)); } else { // Load pure QSS Themes QDirIterator stylesheetFileIterator(dir.absoluteFilePath(""), { "*.qss", "*.css" }, QDir::Files); @@ -92,7 +92,7 @@ void ThemeManager::InitializeThemes() QFile customThemeFile(stylesheetFileIterator.next()); QFileInfo customThemeFileInfo(customThemeFile); themeDebugLog() << "Loading QSS Theme from:" << customThemeFileInfo.absoluteFilePath(); - AddTheme(std::make_unique(GetTheme(darkThemeId), customThemeFileInfo, false)); + addTheme(std::make_unique(getTheme(darkThemeId), customThemeFileInfo, false)); } } } @@ -136,3 +136,20 @@ void ThemeManager::setApplicationTheme(const QString& name) themeWarningLog() << "Tried to set invalid theme:" << name; } } + +QString ThemeManager::getCatImage(QString catName) +{ + QDateTime now = QDateTime::currentDateTime(); + QDateTime birthday(QDate(now.date().year(), 11, 30), QTime(0, 0)); + QDateTime xmas(QDate(now.date().year(), 12, 25), QTime(0, 0)); + QDateTime halloween(QDate(now.date().year(), 10, 31), QTime(0, 0)); + QString cat = catName == "" ? APPLICATION->settings()->get("BackgroundCat").toString() : catName; + if (std::abs(now.daysTo(xmas)) <= 4) { + cat += "-xmas"; + } else if (std::abs(now.daysTo(halloween)) <= 4) { + cat += "-spooky"; + } else if (std::abs(now.daysTo(birthday)) <= 12) { + cat += "-bday"; + } + return cat; +} \ No newline at end of file diff --git a/launcher/ui/themes/ThemeManager.h b/launcher/ui/themes/ThemeManager.h index bb10cd48..4f36bffa 100644 --- a/launcher/ui/themes/ThemeManager.h +++ b/launcher/ui/themes/ThemeManager.h @@ -40,12 +40,13 @@ class ThemeManager { void applyCurrentlySelectedTheme(); void setApplicationTheme(const QString& name); + static QString getCatImage(QString catName = ""); + private: std::map> m_themes; MainWindow* m_mainWindow; - bool m_firstThemeInitialized; - void InitializeThemes(); - QString AddTheme(std::unique_ptr theme); - ITheme* GetTheme(QString themeId); + void initializeThemes(); + QString addTheme(std::unique_ptr theme); + ITheme* getTheme(QString themeId); }; diff --git a/launcher/ui/widgets/ThemeCustomizationWidget.cpp b/launcher/ui/widgets/ThemeCustomizationWidget.cpp index eafcf482..5fb5bd4e 100644 --- a/launcher/ui/widgets/ThemeCustomizationWidget.cpp +++ b/launcher/ui/widgets/ThemeCustomizationWidget.cpp @@ -20,6 +20,7 @@ #include "Application.h" #include "ui/themes/ITheme.h" +#include "ui/themes/ThemeManager.h" ThemeCustomizationWidget::ThemeCustomizationWidget(QWidget *parent) : QWidget(parent), ui(new Ui::ThemeCustomizationWidget) { @@ -72,8 +73,7 @@ void ThemeCustomizationWidget::showFeatures(ThemeFields features) { void ThemeCustomizationWidget::applyIconTheme(int index) { auto settings = APPLICATION->settings(); auto original = settings->get("IconTheme").toString(); - // FIXME: make generic - settings->set("IconTheme", m_iconThemeOptions[index]); + settings->set("IconTheme", m_iconThemeOptions[index].first); if (original != settings->get("IconTheme")) { APPLICATION->applyCurrentlySelectedTheme(); @@ -96,7 +96,7 @@ void ThemeCustomizationWidget::applyWidgetTheme(int index) { void ThemeCustomizationWidget::applyCatTheme(int index) { auto settings = APPLICATION->settings(); - settings->set("BackgroundCat", m_catOptions[index]); + settings->set("BackgroundCat", m_catOptions[index].first); emit currentCatChanged(index); } @@ -111,9 +111,13 @@ void ThemeCustomizationWidget::loadSettings() { auto settings = APPLICATION->settings(); - // FIXME: make generic auto iconTheme = settings->get("IconTheme").toString(); - ui->iconsComboBox->setCurrentIndex(m_iconThemeOptions.indexOf(iconTheme)); + for (auto& iconThemeFromList : m_iconThemeOptions) { + ui->iconsComboBox->addItem(QIcon(QString(":/icons/%1/scalable/settings").arg(iconThemeFromList.first)), iconThemeFromList.second); + if (iconTheme == iconThemeFromList.first) { + ui->iconsComboBox->setCurrentIndex(ui->iconsComboBox->count() - 1); + } + } { auto currentTheme = settings->get("ApplicationTheme").toString(); @@ -129,7 +133,13 @@ void ThemeCustomizationWidget::loadSettings() } auto cat = settings->get("BackgroundCat").toString(); - ui->backgroundCatComboBox->setCurrentIndex(m_catOptions.indexOf(cat)); + for (auto& catFromList : m_catOptions) { + ui->backgroundCatComboBox->addItem(QIcon(QString(":/backgrounds/%1").arg(ThemeManager::getCatImage(catFromList.first))), + catFromList.second); + if (cat == catFromList.first) { + ui->backgroundCatComboBox->setCurrentIndex(ui->backgroundCatComboBox->count() - 1); + } + } } void ThemeCustomizationWidget::retranslate() diff --git a/launcher/ui/widgets/ThemeCustomizationWidget.h b/launcher/ui/widgets/ThemeCustomizationWidget.h index 653e89e7..d450e8df 100644 --- a/launcher/ui/widgets/ThemeCustomizationWidget.h +++ b/launcher/ui/widgets/ThemeCustomizationWidget.h @@ -18,25 +18,19 @@ #pragma once #include -#include +#include "translations/TranslationsModel.h" -enum ThemeFields { - NONE = 0b0000, - ICONS = 0b0001, - WIDGETS = 0b0010, - CAT = 0b0100 -}; +enum ThemeFields { NONE = 0b0000, ICONS = 0b0001, WIDGETS = 0b0010, CAT = 0b0100 }; namespace Ui { class ThemeCustomizationWidget; } -class ThemeCustomizationWidget : public QWidget -{ +class ThemeCustomizationWidget : public QWidget { Q_OBJECT -public: - explicit ThemeCustomizationWidget(QWidget *parent = nullptr); + public: + explicit ThemeCustomizationWidget(QWidget* parent = nullptr); ~ThemeCustomizationWidget(); void showFeatures(ThemeFields features); @@ -45,21 +39,39 @@ public: void loadSettings(); void retranslate(); - - Ui::ThemeCustomizationWidget *ui; -private slots: + private slots: void applyIconTheme(int index); void applyWidgetTheme(int index); void applyCatTheme(int index); -signals: + signals: int currentIconThemeChanged(int index); int currentWidgetThemeChanged(int index); int currentCatChanged(int index); -private: + private: + Ui::ThemeCustomizationWidget* ui; - QStringList m_iconThemeOptions{ "pe_colored", "pe_light", "pe_dark", "pe_blue", "breeze_light", "breeze_dark", "OSX", "iOS", "flat", "flat_white", "multimc", "custom" }; - QStringList m_catOptions{ "kitteh", "rory", "rory-flat" }; -}; + //TODO finish implementing + QList> m_iconThemeOptions{ + { "pe_colored", QObject::tr("Simple (Colored Icons)") }, + { "pe_light", QObject::tr("Simple (Light Icons)") }, + { "pe_dark", QObject::tr("Simple (Dark Icons)") }, + { "pe_blue", QObject::tr("Simple (Blue Icons)") }, + { "breeze_light", QObject::tr("Breeze Light") }, + { "breeze_dark", QObject::tr("Breeze Dark") }, + { "OSX", QObject::tr("OSX") }, + { "iOS", QObject::tr("iOS") }, + { "flat", QObject::tr("Flat") }, + { "flat_white", QObject::tr("Flat (White)") }, + { "multimc", QObject::tr("Legacy") }, + { "custom", QObject::tr("Custom") } + }; + QList> m_catOptions{ + { "kitteh", QObject::tr("Background Cat (from MultiMC)") }, + { "rory", QObject::tr("Rory ID 11 (drawn by Ashtaka)") }, + { "rory-flat", QObject::tr("Rory ID 11 (flat edition, drawn by Ashtaka)") }, + { "teawie", QObject::tr("Teawie (drawn by SympathyTea)") } + }; +}; \ No newline at end of file diff --git a/launcher/ui/widgets/ThemeCustomizationWidget.ui b/launcher/ui/widgets/ThemeCustomizationWidget.ui index 9cc5cc76..15ba831e 100644 --- a/launcher/ui/widgets/ThemeCustomizationWidget.ui +++ b/launcher/ui/widgets/ThemeCustomizationWidget.ui @@ -50,66 +50,6 @@ Qt::StrongFocus - - - Simple (Colored Icons) - - - - - Simple (Light Icons) - - - - - Simple (Dark Icons) - - - - - Simple (Blue Icons) - - - - - Breeze Light - - - - - Breeze Dark - - - - - OSX - - - - - iOS - - - - - Flat - - - - - Flat (White) - - - - - Legacy - - - - - Custom - - @@ -156,26 +96,6 @@ Qt::StrongFocus - - - Background Cat (from MultiMC) - - - - - Rory ID 11 (drawn by Ashtaka) - - - - - Rory ID 11 (flat edition, drawn by Ashtaka) - - - - - Teawie (drawn by SympathyTea) - - From 7d440402ade59fd38b6f1d6b70fb51449cc57e5d Mon Sep 17 00:00:00 2001 From: Tayou <31988415+TayouVR@users.noreply.github.com> Date: Wed, 4 Jan 2023 14:30:25 +0100 Subject: [PATCH 069/152] Update launcher/Application.cpp with suggestion from scrumplex Co-authored-by: Sefa Eyeoglu Signed-off-by: Tayou --- launcher/Application.cpp | 2 +- launcher/ui/themes/ThemeManager.cpp | 4 ++-- launcher/ui/themes/ThemeManager.h | 5 +++++ 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/launcher/Application.cpp b/launcher/Application.cpp index f2cc7bfb..ed8d8d2c 100644 --- a/launcher/Application.cpp +++ b/launcher/Application.cpp @@ -498,7 +498,7 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv) // Theming m_settings->registerSetting("IconTheme", QString("pe_colored")); - m_settings->registerSetting("ApplicationTheme"); + m_settings->registerSetting("ApplicationTheme", QString()); m_settings->registerSetting("BackgroundCat", QString("kitteh")); // Remembered state diff --git a/launcher/ui/themes/ThemeManager.cpp b/launcher/ui/themes/ThemeManager.cpp index 44c13f40..7ccc946a 100644 --- a/launcher/ui/themes/ThemeManager.cpp +++ b/launcher/ui/themes/ThemeManager.cpp @@ -143,7 +143,7 @@ QString ThemeManager::getCatImage(QString catName) QDateTime birthday(QDate(now.date().year(), 11, 30), QTime(0, 0)); QDateTime xmas(QDate(now.date().year(), 12, 25), QTime(0, 0)); QDateTime halloween(QDate(now.date().year(), 10, 31), QTime(0, 0)); - QString cat = catName == "" ? APPLICATION->settings()->get("BackgroundCat").toString() : catName; + QString cat = !catName.isEmpty() ? catName : APPLICATION->settings()->get("BackgroundCat").toString(); if (std::abs(now.daysTo(xmas)) <= 4) { cat += "-xmas"; } else if (std::abs(now.daysTo(halloween)) <= 4) { @@ -152,4 +152,4 @@ QString ThemeManager::getCatImage(QString catName) cat += "-bday"; } return cat; -} \ No newline at end of file +} diff --git a/launcher/ui/themes/ThemeManager.h b/launcher/ui/themes/ThemeManager.h index 4f36bffa..d5e73bb8 100644 --- a/launcher/ui/themes/ThemeManager.h +++ b/launcher/ui/themes/ThemeManager.h @@ -40,6 +40,11 @@ class ThemeManager { void applyCurrentlySelectedTheme(); void setApplicationTheme(const QString& name); + /// + /// Returns the cat based on selected cat and with events (Birthday, XMas, etc.) + /// + /// Optional, if you need a specific cat. + /// static QString getCatImage(QString catName = ""); private: From 689fe1e2c76b8065b9769b4304b1c9b4d81215b1 Mon Sep 17 00:00:00 2001 From: Tayou Date: Mon, 9 Jan 2023 17:01:33 +0100 Subject: [PATCH 070/152] CRLF -> LF damn you visual studio for creating CRLF files everywhere... Signed-off-by: Tayou --- launcher/ui/setupwizard/ThemeWizardPage.cpp | 140 ++-- launcher/ui/setupwizard/ThemeWizardPage.h | 86 +-- launcher/ui/setupwizard/ThemeWizardPage.ui | 716 +++++++++--------- launcher/ui/themes/ThemeManager.cpp | 310 ++++---- launcher/ui/themes/ThemeManager.h | 114 +-- .../ui/widgets/ThemeCustomizationWidget.cpp | 296 ++++---- .../ui/widgets/ThemeCustomizationWidget.h | 152 ++-- .../ui/widgets/ThemeCustomizationWidget.ui | 210 ++--- 8 files changed, 1012 insertions(+), 1012 deletions(-) diff --git a/launcher/ui/setupwizard/ThemeWizardPage.cpp b/launcher/ui/setupwizard/ThemeWizardPage.cpp index cc2d335b..42826aba 100644 --- a/launcher/ui/setupwizard/ThemeWizardPage.cpp +++ b/launcher/ui/setupwizard/ThemeWizardPage.cpp @@ -1,70 +1,70 @@ -// SPDX-License-Identifier: GPL-3.0-only -/* - * Prism Launcher - Minecraft Launcher - * Copyright (C) 2022 Tayou - * - * 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 "ThemeWizardPage.h" -#include "ui_ThemeWizardPage.h" - -#include "Application.h" -#include "ui/themes/ITheme.h" -#include "ui/themes/ThemeManager.h" -#include "ui/widgets/ThemeCustomizationWidget.h" -#include "ui_ThemeCustomizationWidget.h" - -ThemeWizardPage::ThemeWizardPage(QWidget* parent) : BaseWizardPage(parent), ui(new Ui::ThemeWizardPage) -{ - ui->setupUi(this); - - connect(ui->themeCustomizationWidget, &ThemeCustomizationWidget::currentIconThemeChanged, this, &ThemeWizardPage::updateIcons); - connect(ui->themeCustomizationWidget, &ThemeCustomizationWidget::currentCatChanged, this, &ThemeWizardPage::updateCat); - - updateIcons(); - updateCat(); -} - -ThemeWizardPage::~ThemeWizardPage() -{ - delete ui; -} - -void ThemeWizardPage::updateIcons() -{ - qDebug() << "Setting Icons"; - ui->previewIconButton0->setIcon(APPLICATION->getThemedIcon("new")); - ui->previewIconButton1->setIcon(APPLICATION->getThemedIcon("centralmods")); - ui->previewIconButton2->setIcon(APPLICATION->getThemedIcon("viewfolder")); - ui->previewIconButton3->setIcon(APPLICATION->getThemedIcon("launch")); - ui->previewIconButton4->setIcon(APPLICATION->getThemedIcon("copy")); - ui->previewIconButton5->setIcon(APPLICATION->getThemedIcon("export")); - ui->previewIconButton6->setIcon(APPLICATION->getThemedIcon("delete")); - ui->previewIconButton7->setIcon(APPLICATION->getThemedIcon("about")); - ui->previewIconButton8->setIcon(APPLICATION->getThemedIcon("settings")); - ui->previewIconButton9->setIcon(APPLICATION->getThemedIcon("cat")); - update(); - repaint(); - parentWidget()->update(); -} - -void ThemeWizardPage::updateCat() -{ - qDebug() << "Setting Cat"; - ui->catImagePreviewButton->setIcon(QIcon(QString(R"(:/backgrounds/%1)").arg(ThemeManager::getCatImage()))); -} - -void ThemeWizardPage::retranslate() -{ - ui->retranslateUi(this); -} +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 Tayou + * + * 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 "ThemeWizardPage.h" +#include "ui_ThemeWizardPage.h" + +#include "Application.h" +#include "ui/themes/ITheme.h" +#include "ui/themes/ThemeManager.h" +#include "ui/widgets/ThemeCustomizationWidget.h" +#include "ui_ThemeCustomizationWidget.h" + +ThemeWizardPage::ThemeWizardPage(QWidget* parent) : BaseWizardPage(parent), ui(new Ui::ThemeWizardPage) +{ + ui->setupUi(this); + + connect(ui->themeCustomizationWidget, &ThemeCustomizationWidget::currentIconThemeChanged, this, &ThemeWizardPage::updateIcons); + connect(ui->themeCustomizationWidget, &ThemeCustomizationWidget::currentCatChanged, this, &ThemeWizardPage::updateCat); + + updateIcons(); + updateCat(); +} + +ThemeWizardPage::~ThemeWizardPage() +{ + delete ui; +} + +void ThemeWizardPage::updateIcons() +{ + qDebug() << "Setting Icons"; + ui->previewIconButton0->setIcon(APPLICATION->getThemedIcon("new")); + ui->previewIconButton1->setIcon(APPLICATION->getThemedIcon("centralmods")); + ui->previewIconButton2->setIcon(APPLICATION->getThemedIcon("viewfolder")); + ui->previewIconButton3->setIcon(APPLICATION->getThemedIcon("launch")); + ui->previewIconButton4->setIcon(APPLICATION->getThemedIcon("copy")); + ui->previewIconButton5->setIcon(APPLICATION->getThemedIcon("export")); + ui->previewIconButton6->setIcon(APPLICATION->getThemedIcon("delete")); + ui->previewIconButton7->setIcon(APPLICATION->getThemedIcon("about")); + ui->previewIconButton8->setIcon(APPLICATION->getThemedIcon("settings")); + ui->previewIconButton9->setIcon(APPLICATION->getThemedIcon("cat")); + update(); + repaint(); + parentWidget()->update(); +} + +void ThemeWizardPage::updateCat() +{ + qDebug() << "Setting Cat"; + ui->catImagePreviewButton->setIcon(QIcon(QString(R"(:/backgrounds/%1)").arg(ThemeManager::getCatImage()))); +} + +void ThemeWizardPage::retranslate() +{ + ui->retranslateUi(this); +} diff --git a/launcher/ui/setupwizard/ThemeWizardPage.h b/launcher/ui/setupwizard/ThemeWizardPage.h index 992ba2ca..61a3d0c0 100644 --- a/launcher/ui/setupwizard/ThemeWizardPage.h +++ b/launcher/ui/setupwizard/ThemeWizardPage.h @@ -1,43 +1,43 @@ -// SPDX-License-Identifier: GPL-3.0-only -/* - * Prism Launcher - Minecraft Launcher - * Copyright (C) 2022 Tayou - * - * 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 "BaseWizardPage.h" - -namespace Ui { -class ThemeWizardPage; -} - -class ThemeWizardPage : public BaseWizardPage { - Q_OBJECT - - public: - explicit ThemeWizardPage(QWidget* parent = nullptr); - ~ThemeWizardPage(); - - bool validatePage() override { return true; }; - void retranslate() override; - - private slots: - void updateIcons(); - void updateCat(); - - private: - Ui::ThemeWizardPage* ui; -}; +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 Tayou + * + * 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 "BaseWizardPage.h" + +namespace Ui { +class ThemeWizardPage; +} + +class ThemeWizardPage : public BaseWizardPage { + Q_OBJECT + + public: + explicit ThemeWizardPage(QWidget* parent = nullptr); + ~ThemeWizardPage(); + + bool validatePage() override { return true; }; + void retranslate() override; + + private slots: + void updateIcons(); + void updateCat(); + + private: + Ui::ThemeWizardPage* ui; +}; diff --git a/launcher/ui/setupwizard/ThemeWizardPage.ui b/launcher/ui/setupwizard/ThemeWizardPage.ui index 95b0f805..1ab04fc8 100644 --- a/launcher/ui/setupwizard/ThemeWizardPage.ui +++ b/launcher/ui/setupwizard/ThemeWizardPage.ui @@ -1,358 +1,358 @@ - - - ThemeWizardPage - - - - 0 - 0 - 510 - 552 - - - - WizardPage - - - - - - Select the Theme you wish to use - - - - - - - - 0 - 100 - - - - - - - - Qt::Horizontal - - - - - - - Icon Preview: - - - - - - - - - - 0 - 0 - - - - - 30 - 30 - - - - - .. - - - false - - - true - - - - - - - - 0 - 0 - - - - - 30 - 30 - - - - - .. - - - false - - - true - - - - - - - - 0 - 0 - - - - - 30 - 30 - - - - - .. - - - false - - - true - - - - - - - - 0 - 0 - - - - - 30 - 30 - - - - - .. - - - false - - - true - - - - - - - - 0 - 0 - - - - - 30 - 30 - - - - - .. - - - false - - - true - - - - - - - - 0 - 0 - - - - - 30 - 30 - - - - - .. - - - false - - - true - - - - - - - - 0 - 0 - - - - - 30 - 30 - - - - - .. - - - false - - - true - - - - - - - - 0 - 0 - - - - - 30 - 30 - - - - - .. - - - false - - - true - - - - - - - - 0 - 0 - - - - - 30 - 30 - - - - - .. - - - false - - - true - - - - - - - - 0 - 0 - - - - - 30 - 30 - - - - - .. - - - false - - - true - - - - - - - - - - 0 - 256 - - - - - - - - 256 - 256 - - - - true - - - - - - - Qt::Vertical - - - - 20 - 193 - - - - - - - - - ThemeCustomizationWidget - QWidget -
ui/widgets/ThemeCustomizationWidget.h
-
-
- - -
+ + + ThemeWizardPage + + + + 0 + 0 + 510 + 552 + + + + WizardPage + + + + + + Select the Theme you wish to use + + + + + + + + 0 + 100 + + + + + + + + Qt::Horizontal + + + + + + + Icon Preview: + + + + + + + + + + 0 + 0 + + + + + 30 + 30 + + + + + .. + + + false + + + true + + + + + + + + 0 + 0 + + + + + 30 + 30 + + + + + .. + + + false + + + true + + + + + + + + 0 + 0 + + + + + 30 + 30 + + + + + .. + + + false + + + true + + + + + + + + 0 + 0 + + + + + 30 + 30 + + + + + .. + + + false + + + true + + + + + + + + 0 + 0 + + + + + 30 + 30 + + + + + .. + + + false + + + true + + + + + + + + 0 + 0 + + + + + 30 + 30 + + + + + .. + + + false + + + true + + + + + + + + 0 + 0 + + + + + 30 + 30 + + + + + .. + + + false + + + true + + + + + + + + 0 + 0 + + + + + 30 + 30 + + + + + .. + + + false + + + true + + + + + + + + 0 + 0 + + + + + 30 + 30 + + + + + .. + + + false + + + true + + + + + + + + 0 + 0 + + + + + 30 + 30 + + + + + .. + + + false + + + true + + + + + + + + + + 0 + 256 + + + + + + + + 256 + 256 + + + + true + + + + + + + Qt::Vertical + + + + 20 + 193 + + + + + + + + + ThemeCustomizationWidget + QWidget +
ui/widgets/ThemeCustomizationWidget.h
+
+
+ + +
diff --git a/launcher/ui/themes/ThemeManager.cpp b/launcher/ui/themes/ThemeManager.cpp index 7ccc946a..13406485 100644 --- a/launcher/ui/themes/ThemeManager.cpp +++ b/launcher/ui/themes/ThemeManager.cpp @@ -1,155 +1,155 @@ -// SPDX-License-Identifier: GPL-3.0-only -/* - * Prism Launcher - Minecraft Launcher - * Copyright (C) 2022 Tayou - * - * 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 "ThemeManager.h" - -#include -#include -#include -#include -#include "ui/themes/BrightTheme.h" -#include "ui/themes/CustomTheme.h" -#include "ui/themes/DarkTheme.h" -#include "ui/themes/SystemTheme.h" - -#include "Application.h" - -ThemeManager::ThemeManager(MainWindow* mainWindow) -{ - m_mainWindow = mainWindow; - initializeThemes(); -} - -/// @brief Adds the Theme to the list of themes -/// @param theme The Theme to add -/// @return Theme ID -QString ThemeManager::addTheme(std::unique_ptr theme) -{ - QString id = theme->id(); - m_themes.emplace(id, std::move(theme)); - return id; -} - -/// @brief Gets the Theme from the List via ID -/// @param themeId Theme ID of theme to fetch -/// @return Theme at themeId -ITheme* ThemeManager::getTheme(QString themeId) -{ - return m_themes[themeId].get(); -} - -void ThemeManager::initializeThemes() -{ - // Icon themes - { - // TODO: icon themes and instance icons do not mesh well together. Rearrange and fix discrepancies! - // set icon theme search path! - auto searchPaths = QIcon::themeSearchPaths(); - searchPaths.append("iconthemes"); - QIcon::setThemeSearchPaths(searchPaths); - themeDebugLog() << "<> Icon themes initialized."; - } - - // Initialize widget themes - { - themeDebugLog() << "<> Initializing Widget Themes"; - themeDebugLog() << "Loading Built-in Theme:" << addTheme(std::make_unique()); - auto darkThemeId = addTheme(std::make_unique()); - themeDebugLog() << "Loading Built-in Theme:" << darkThemeId; - themeDebugLog() << "Loading Built-in Theme:" << addTheme(std::make_unique()); - - // TODO: need some way to differentiate same name themes in different subdirectories (maybe smaller grey text next to theme name in - // dropdown?) - QString themeFolder = QDir("./themes/").absoluteFilePath(""); - themeDebugLog() << "Theme Folder Path: " << themeFolder; - - QDirIterator directoryIterator(themeFolder, QDir::Dirs | QDir::NoDotAndDotDot, QDirIterator::Subdirectories); - while (directoryIterator.hasNext()) { - QDir dir(directoryIterator.next()); - QFileInfo themeJson(dir.absoluteFilePath("theme.json")); - if (themeJson.exists()) { - // Load "theme.json" based themes - themeDebugLog() << "Loading JSON Theme from:" << themeJson.absoluteFilePath(); - addTheme(std::make_unique(getTheme(darkThemeId), themeJson, true)); - } else { - // Load pure QSS Themes - QDirIterator stylesheetFileIterator(dir.absoluteFilePath(""), { "*.qss", "*.css" }, QDir::Files); - while (stylesheetFileIterator.hasNext()) { - QFile customThemeFile(stylesheetFileIterator.next()); - QFileInfo customThemeFileInfo(customThemeFile); - themeDebugLog() << "Loading QSS Theme from:" << customThemeFileInfo.absoluteFilePath(); - addTheme(std::make_unique(getTheme(darkThemeId), customThemeFileInfo, false)); - } - } - } - - themeDebugLog() << "<> Widget themes initialized."; - } -} - -QList ThemeManager::getValidApplicationThemes() -{ - QList ret; - ret.reserve(m_themes.size()); - for (auto&& [id, theme] : m_themes) { - ret.append(theme.get()); - } - return ret; -} - -void ThemeManager::setIconTheme(const QString& name) -{ - QIcon::setThemeName(name); -} - -void ThemeManager::applyCurrentlySelectedTheme() -{ - setIconTheme(APPLICATION->settings()->get("IconTheme").toString()); - themeDebugLog() << "<> Icon theme set."; - setApplicationTheme(APPLICATION->settings()->get("ApplicationTheme").toString()); - themeDebugLog() << "<> Application theme set."; -} - -void ThemeManager::setApplicationTheme(const QString& name) -{ - auto systemPalette = qApp->palette(); - auto themeIter = m_themes.find(name); - if (themeIter != m_themes.end()) { - auto& theme = themeIter->second; - themeDebugLog() << "applying theme" << theme->name(); - theme->apply(); - } else { - themeWarningLog() << "Tried to set invalid theme:" << name; - } -} - -QString ThemeManager::getCatImage(QString catName) -{ - QDateTime now = QDateTime::currentDateTime(); - QDateTime birthday(QDate(now.date().year(), 11, 30), QTime(0, 0)); - QDateTime xmas(QDate(now.date().year(), 12, 25), QTime(0, 0)); - QDateTime halloween(QDate(now.date().year(), 10, 31), QTime(0, 0)); - QString cat = !catName.isEmpty() ? catName : APPLICATION->settings()->get("BackgroundCat").toString(); - if (std::abs(now.daysTo(xmas)) <= 4) { - cat += "-xmas"; - } else if (std::abs(now.daysTo(halloween)) <= 4) { - cat += "-spooky"; - } else if (std::abs(now.daysTo(birthday)) <= 12) { - cat += "-bday"; - } - return cat; -} +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 Tayou + * + * 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 "ThemeManager.h" + +#include +#include +#include +#include +#include "ui/themes/BrightTheme.h" +#include "ui/themes/CustomTheme.h" +#include "ui/themes/DarkTheme.h" +#include "ui/themes/SystemTheme.h" + +#include "Application.h" + +ThemeManager::ThemeManager(MainWindow* mainWindow) +{ + m_mainWindow = mainWindow; + initializeThemes(); +} + +/// @brief Adds the Theme to the list of themes +/// @param theme The Theme to add +/// @return Theme ID +QString ThemeManager::addTheme(std::unique_ptr theme) +{ + QString id = theme->id(); + m_themes.emplace(id, std::move(theme)); + return id; +} + +/// @brief Gets the Theme from the List via ID +/// @param themeId Theme ID of theme to fetch +/// @return Theme at themeId +ITheme* ThemeManager::getTheme(QString themeId) +{ + return m_themes[themeId].get(); +} + +void ThemeManager::initializeThemes() +{ + // Icon themes + { + // TODO: icon themes and instance icons do not mesh well together. Rearrange and fix discrepancies! + // set icon theme search path! + auto searchPaths = QIcon::themeSearchPaths(); + searchPaths.append("iconthemes"); + QIcon::setThemeSearchPaths(searchPaths); + themeDebugLog() << "<> Icon themes initialized."; + } + + // Initialize widget themes + { + themeDebugLog() << "<> Initializing Widget Themes"; + themeDebugLog() << "Loading Built-in Theme:" << addTheme(std::make_unique()); + auto darkThemeId = addTheme(std::make_unique()); + themeDebugLog() << "Loading Built-in Theme:" << darkThemeId; + themeDebugLog() << "Loading Built-in Theme:" << addTheme(std::make_unique()); + + // TODO: need some way to differentiate same name themes in different subdirectories (maybe smaller grey text next to theme name in + // dropdown?) + QString themeFolder = QDir("./themes/").absoluteFilePath(""); + themeDebugLog() << "Theme Folder Path: " << themeFolder; + + QDirIterator directoryIterator(themeFolder, QDir::Dirs | QDir::NoDotAndDotDot, QDirIterator::Subdirectories); + while (directoryIterator.hasNext()) { + QDir dir(directoryIterator.next()); + QFileInfo themeJson(dir.absoluteFilePath("theme.json")); + if (themeJson.exists()) { + // Load "theme.json" based themes + themeDebugLog() << "Loading JSON Theme from:" << themeJson.absoluteFilePath(); + addTheme(std::make_unique(getTheme(darkThemeId), themeJson, true)); + } else { + // Load pure QSS Themes + QDirIterator stylesheetFileIterator(dir.absoluteFilePath(""), { "*.qss", "*.css" }, QDir::Files); + while (stylesheetFileIterator.hasNext()) { + QFile customThemeFile(stylesheetFileIterator.next()); + QFileInfo customThemeFileInfo(customThemeFile); + themeDebugLog() << "Loading QSS Theme from:" << customThemeFileInfo.absoluteFilePath(); + addTheme(std::make_unique(getTheme(darkThemeId), customThemeFileInfo, false)); + } + } + } + + themeDebugLog() << "<> Widget themes initialized."; + } +} + +QList ThemeManager::getValidApplicationThemes() +{ + QList ret; + ret.reserve(m_themes.size()); + for (auto&& [id, theme] : m_themes) { + ret.append(theme.get()); + } + return ret; +} + +void ThemeManager::setIconTheme(const QString& name) +{ + QIcon::setThemeName(name); +} + +void ThemeManager::applyCurrentlySelectedTheme() +{ + setIconTheme(APPLICATION->settings()->get("IconTheme").toString()); + themeDebugLog() << "<> Icon theme set."; + setApplicationTheme(APPLICATION->settings()->get("ApplicationTheme").toString()); + themeDebugLog() << "<> Application theme set."; +} + +void ThemeManager::setApplicationTheme(const QString& name) +{ + auto systemPalette = qApp->palette(); + auto themeIter = m_themes.find(name); + if (themeIter != m_themes.end()) { + auto& theme = themeIter->second; + themeDebugLog() << "applying theme" << theme->name(); + theme->apply(); + } else { + themeWarningLog() << "Tried to set invalid theme:" << name; + } +} + +QString ThemeManager::getCatImage(QString catName) +{ + QDateTime now = QDateTime::currentDateTime(); + QDateTime birthday(QDate(now.date().year(), 11, 30), QTime(0, 0)); + QDateTime xmas(QDate(now.date().year(), 12, 25), QTime(0, 0)); + QDateTime halloween(QDate(now.date().year(), 10, 31), QTime(0, 0)); + QString cat = !catName.isEmpty() ? catName : APPLICATION->settings()->get("BackgroundCat").toString(); + if (std::abs(now.daysTo(xmas)) <= 4) { + cat += "-xmas"; + } else if (std::abs(now.daysTo(halloween)) <= 4) { + cat += "-spooky"; + } else if (std::abs(now.daysTo(birthday)) <= 12) { + cat += "-bday"; + } + return cat; +} diff --git a/launcher/ui/themes/ThemeManager.h b/launcher/ui/themes/ThemeManager.h index d5e73bb8..9af44b5a 100644 --- a/launcher/ui/themes/ThemeManager.h +++ b/launcher/ui/themes/ThemeManager.h @@ -1,57 +1,57 @@ -// SPDX-License-Identifier: GPL-3.0-only -/* - * Prism Launcher - Minecraft Launcher - * Copyright (C) 2022 Tayou - * - * 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 "ui/MainWindow.h" -#include "ui/themes/ITheme.h" - -inline auto themeDebugLog() -{ - return qDebug() << "[Theme]"; -} -inline auto themeWarningLog() -{ - return qWarning() << "[Theme]"; -} - -class ThemeManager { - public: - ThemeManager(MainWindow* mainWindow); - - QList getValidApplicationThemes(); - void setIconTheme(const QString& name); - void applyCurrentlySelectedTheme(); - void setApplicationTheme(const QString& name); - - /// - /// Returns the cat based on selected cat and with events (Birthday, XMas, etc.) - /// - /// Optional, if you need a specific cat. - /// - static QString getCatImage(QString catName = ""); - - private: - std::map> m_themes; - MainWindow* m_mainWindow; - - void initializeThemes(); - QString addTheme(std::unique_ptr theme); - ITheme* getTheme(QString themeId); -}; +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 Tayou + * + * 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 "ui/MainWindow.h" +#include "ui/themes/ITheme.h" + +inline auto themeDebugLog() +{ + return qDebug() << "[Theme]"; +} +inline auto themeWarningLog() +{ + return qWarning() << "[Theme]"; +} + +class ThemeManager { + public: + ThemeManager(MainWindow* mainWindow); + + QList getValidApplicationThemes(); + void setIconTheme(const QString& name); + void applyCurrentlySelectedTheme(); + void setApplicationTheme(const QString& name); + + /// + /// Returns the cat based on selected cat and with events (Birthday, XMas, etc.) + /// + /// Optional, if you need a specific cat. + /// + static QString getCatImage(QString catName = ""); + + private: + std::map> m_themes; + MainWindow* m_mainWindow; + + void initializeThemes(); + QString addTheme(std::unique_ptr theme); + ITheme* getTheme(QString themeId); +}; diff --git a/launcher/ui/widgets/ThemeCustomizationWidget.cpp b/launcher/ui/widgets/ThemeCustomizationWidget.cpp index 5fb5bd4e..d0b5be21 100644 --- a/launcher/ui/widgets/ThemeCustomizationWidget.cpp +++ b/launcher/ui/widgets/ThemeCustomizationWidget.cpp @@ -1,148 +1,148 @@ -// SPDX-License-Identifier: GPL-3.0-only -/* - * Prism Launcher - Minecraft Launcher - * Copyright (C) 2022 Tayou - * - * 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 "ThemeCustomizationWidget.h" -#include "ui_ThemeCustomizationWidget.h" - -#include "Application.h" -#include "ui/themes/ITheme.h" -#include "ui/themes/ThemeManager.h" - -ThemeCustomizationWidget::ThemeCustomizationWidget(QWidget *parent) : QWidget(parent), ui(new Ui::ThemeCustomizationWidget) -{ - ui->setupUi(this); - 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); -} - -ThemeCustomizationWidget::~ThemeCustomizationWidget() -{ - delete ui; -} - -/// -/// The layout was not quite right, so currently this just disables the UI elements, which should be hidden instead -/// TODO FIXME -/// -/// Original Method One: -/// ui->iconsComboBox->setVisible(features& ThemeFields::ICONS); -/// ui->iconsLabel->setVisible(features& ThemeFields::ICONS); -/// ui->widgetStyleComboBox->setVisible(features& ThemeFields::WIDGETS); -/// ui->widgetThemeLabel->setVisible(features& ThemeFields::WIDGETS); -/// ui->backgroundCatComboBox->setVisible(features& ThemeFields::CAT); -/// ui->backgroundCatLabel->setVisible(features& ThemeFields::CAT); -/// -/// original Method Two: -/// if (!(features & ThemeFields::ICONS)) { -/// ui->formLayout->setRowVisible(0, false); -/// } -/// if (!(features & ThemeFields::WIDGETS)) { -/// ui->formLayout->setRowVisible(1, false); -/// } -/// if (!(features & ThemeFields::CAT)) { -/// ui->formLayout->setRowVisible(2, false); -/// } -/// -/// -void ThemeCustomizationWidget::showFeatures(ThemeFields features) { - ui->iconsComboBox->setEnabled(features & ThemeFields::ICONS); - ui->iconsLabel->setEnabled(features & ThemeFields::ICONS); - ui->widgetStyleComboBox->setEnabled(features & ThemeFields::WIDGETS); - ui->widgetThemeLabel->setEnabled(features & ThemeFields::WIDGETS); - ui->backgroundCatComboBox->setEnabled(features & ThemeFields::CAT); - ui->backgroundCatLabel->setEnabled(features & ThemeFields::CAT); -} - -void ThemeCustomizationWidget::applyIconTheme(int index) { - auto settings = APPLICATION->settings(); - auto original = settings->get("IconTheme").toString(); - settings->set("IconTheme", m_iconThemeOptions[index].first); - - if (original != settings->get("IconTheme")) { - APPLICATION->applyCurrentlySelectedTheme(); - } - - emit currentIconThemeChanged(index); -} - -void ThemeCustomizationWidget::applyWidgetTheme(int index) { - auto settings = APPLICATION->settings(); - auto originalAppTheme = settings->get("ApplicationTheme").toString(); - auto newAppTheme = ui->widgetStyleComboBox->currentData().toString(); - if (originalAppTheme != newAppTheme) { - settings->set("ApplicationTheme", newAppTheme); - APPLICATION->applyCurrentlySelectedTheme(); - } - - emit currentWidgetThemeChanged(index); -} - -void ThemeCustomizationWidget::applyCatTheme(int index) { - auto settings = APPLICATION->settings(); - settings->set("BackgroundCat", m_catOptions[index].first); - - emit currentCatChanged(index); -} - -void ThemeCustomizationWidget::applySettings() -{ - applyIconTheme(ui->iconsComboBox->currentIndex()); - applyWidgetTheme(ui->widgetStyleComboBox->currentIndex()); - applyCatTheme(ui->backgroundCatComboBox->currentIndex()); -} -void ThemeCustomizationWidget::loadSettings() -{ - auto settings = APPLICATION->settings(); - - auto iconTheme = settings->get("IconTheme").toString(); - for (auto& iconThemeFromList : m_iconThemeOptions) { - ui->iconsComboBox->addItem(QIcon(QString(":/icons/%1/scalable/settings").arg(iconThemeFromList.first)), iconThemeFromList.second); - if (iconTheme == iconThemeFromList.first) { - ui->iconsComboBox->setCurrentIndex(ui->iconsComboBox->count() - 1); - } - } - - { - auto currentTheme = settings->get("ApplicationTheme").toString(); - auto themes = APPLICATION->getValidApplicationThemes(); - int idx = 0; - for (auto& theme : themes) { - ui->widgetStyleComboBox->addItem(theme->name(), theme->id()); - if (currentTheme == theme->id()) { - ui->widgetStyleComboBox->setCurrentIndex(idx); - } - idx++; - } - } - - auto cat = settings->get("BackgroundCat").toString(); - for (auto& catFromList : m_catOptions) { - ui->backgroundCatComboBox->addItem(QIcon(QString(":/backgrounds/%1").arg(ThemeManager::getCatImage(catFromList.first))), - catFromList.second); - if (cat == catFromList.first) { - ui->backgroundCatComboBox->setCurrentIndex(ui->backgroundCatComboBox->count() - 1); - } - } -} - -void ThemeCustomizationWidget::retranslate() -{ - ui->retranslateUi(this); -} +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 Tayou + * + * 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 "ThemeCustomizationWidget.h" +#include "ui_ThemeCustomizationWidget.h" + +#include "Application.h" +#include "ui/themes/ITheme.h" +#include "ui/themes/ThemeManager.h" + +ThemeCustomizationWidget::ThemeCustomizationWidget(QWidget *parent) : QWidget(parent), ui(new Ui::ThemeCustomizationWidget) +{ + ui->setupUi(this); + 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); +} + +ThemeCustomizationWidget::~ThemeCustomizationWidget() +{ + delete ui; +} + +/// +/// The layout was not quite right, so currently this just disables the UI elements, which should be hidden instead +/// TODO FIXME +/// +/// Original Method One: +/// ui->iconsComboBox->setVisible(features& ThemeFields::ICONS); +/// ui->iconsLabel->setVisible(features& ThemeFields::ICONS); +/// ui->widgetStyleComboBox->setVisible(features& ThemeFields::WIDGETS); +/// ui->widgetThemeLabel->setVisible(features& ThemeFields::WIDGETS); +/// ui->backgroundCatComboBox->setVisible(features& ThemeFields::CAT); +/// ui->backgroundCatLabel->setVisible(features& ThemeFields::CAT); +/// +/// original Method Two: +/// if (!(features & ThemeFields::ICONS)) { +/// ui->formLayout->setRowVisible(0, false); +/// } +/// if (!(features & ThemeFields::WIDGETS)) { +/// ui->formLayout->setRowVisible(1, false); +/// } +/// if (!(features & ThemeFields::CAT)) { +/// ui->formLayout->setRowVisible(2, false); +/// } +/// +/// +void ThemeCustomizationWidget::showFeatures(ThemeFields features) { + ui->iconsComboBox->setEnabled(features & ThemeFields::ICONS); + ui->iconsLabel->setEnabled(features & ThemeFields::ICONS); + ui->widgetStyleComboBox->setEnabled(features & ThemeFields::WIDGETS); + ui->widgetThemeLabel->setEnabled(features & ThemeFields::WIDGETS); + ui->backgroundCatComboBox->setEnabled(features & ThemeFields::CAT); + ui->backgroundCatLabel->setEnabled(features & ThemeFields::CAT); +} + +void ThemeCustomizationWidget::applyIconTheme(int index) { + auto settings = APPLICATION->settings(); + auto original = settings->get("IconTheme").toString(); + settings->set("IconTheme", m_iconThemeOptions[index].first); + + if (original != settings->get("IconTheme")) { + APPLICATION->applyCurrentlySelectedTheme(); + } + + emit currentIconThemeChanged(index); +} + +void ThemeCustomizationWidget::applyWidgetTheme(int index) { + auto settings = APPLICATION->settings(); + auto originalAppTheme = settings->get("ApplicationTheme").toString(); + auto newAppTheme = ui->widgetStyleComboBox->currentData().toString(); + if (originalAppTheme != newAppTheme) { + settings->set("ApplicationTheme", newAppTheme); + APPLICATION->applyCurrentlySelectedTheme(); + } + + emit currentWidgetThemeChanged(index); +} + +void ThemeCustomizationWidget::applyCatTheme(int index) { + auto settings = APPLICATION->settings(); + settings->set("BackgroundCat", m_catOptions[index].first); + + emit currentCatChanged(index); +} + +void ThemeCustomizationWidget::applySettings() +{ + applyIconTheme(ui->iconsComboBox->currentIndex()); + applyWidgetTheme(ui->widgetStyleComboBox->currentIndex()); + applyCatTheme(ui->backgroundCatComboBox->currentIndex()); +} +void ThemeCustomizationWidget::loadSettings() +{ + auto settings = APPLICATION->settings(); + + auto iconTheme = settings->get("IconTheme").toString(); + for (auto& iconThemeFromList : m_iconThemeOptions) { + ui->iconsComboBox->addItem(QIcon(QString(":/icons/%1/scalable/settings").arg(iconThemeFromList.first)), iconThemeFromList.second); + if (iconTheme == iconThemeFromList.first) { + ui->iconsComboBox->setCurrentIndex(ui->iconsComboBox->count() - 1); + } + } + + { + auto currentTheme = settings->get("ApplicationTheme").toString(); + auto themes = APPLICATION->getValidApplicationThemes(); + int idx = 0; + for (auto& theme : themes) { + ui->widgetStyleComboBox->addItem(theme->name(), theme->id()); + if (currentTheme == theme->id()) { + ui->widgetStyleComboBox->setCurrentIndex(idx); + } + idx++; + } + } + + auto cat = settings->get("BackgroundCat").toString(); + for (auto& catFromList : m_catOptions) { + ui->backgroundCatComboBox->addItem(QIcon(QString(":/backgrounds/%1").arg(ThemeManager::getCatImage(catFromList.first))), + catFromList.second); + if (cat == catFromList.first) { + ui->backgroundCatComboBox->setCurrentIndex(ui->backgroundCatComboBox->count() - 1); + } + } +} + +void ThemeCustomizationWidget::retranslate() +{ + ui->retranslateUi(this); +} diff --git a/launcher/ui/widgets/ThemeCustomizationWidget.h b/launcher/ui/widgets/ThemeCustomizationWidget.h index d450e8df..be2c4492 100644 --- a/launcher/ui/widgets/ThemeCustomizationWidget.h +++ b/launcher/ui/widgets/ThemeCustomizationWidget.h @@ -1,77 +1,77 @@ -// SPDX-License-Identifier: GPL-3.0-only -/* - * Prism Launcher - Minecraft Launcher - * Copyright (C) 2022 Tayou - * - * 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 "translations/TranslationsModel.h" - -enum ThemeFields { NONE = 0b0000, ICONS = 0b0001, WIDGETS = 0b0010, CAT = 0b0100 }; - -namespace Ui { -class ThemeCustomizationWidget; -} - -class ThemeCustomizationWidget : public QWidget { - Q_OBJECT - - public: - explicit ThemeCustomizationWidget(QWidget* parent = nullptr); - ~ThemeCustomizationWidget(); - - void showFeatures(ThemeFields features); - - void applySettings(); - - void loadSettings(); - void retranslate(); - - private slots: - void applyIconTheme(int index); - void applyWidgetTheme(int index); - void applyCatTheme(int index); - - signals: - int currentIconThemeChanged(int index); - int currentWidgetThemeChanged(int index); - int currentCatChanged(int index); - - private: - Ui::ThemeCustomizationWidget* ui; - - //TODO finish implementing - QList> m_iconThemeOptions{ - { "pe_colored", QObject::tr("Simple (Colored Icons)") }, - { "pe_light", QObject::tr("Simple (Light Icons)") }, - { "pe_dark", QObject::tr("Simple (Dark Icons)") }, - { "pe_blue", QObject::tr("Simple (Blue Icons)") }, - { "breeze_light", QObject::tr("Breeze Light") }, - { "breeze_dark", QObject::tr("Breeze Dark") }, - { "OSX", QObject::tr("OSX") }, - { "iOS", QObject::tr("iOS") }, - { "flat", QObject::tr("Flat") }, - { "flat_white", QObject::tr("Flat (White)") }, - { "multimc", QObject::tr("Legacy") }, - { "custom", QObject::tr("Custom") } - }; - QList> m_catOptions{ - { "kitteh", QObject::tr("Background Cat (from MultiMC)") }, - { "rory", QObject::tr("Rory ID 11 (drawn by Ashtaka)") }, - { "rory-flat", QObject::tr("Rory ID 11 (flat edition, drawn by Ashtaka)") }, - { "teawie", QObject::tr("Teawie (drawn by SympathyTea)") } - }; +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 Tayou + * + * 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 "translations/TranslationsModel.h" + +enum ThemeFields { NONE = 0b0000, ICONS = 0b0001, WIDGETS = 0b0010, CAT = 0b0100 }; + +namespace Ui { +class ThemeCustomizationWidget; +} + +class ThemeCustomizationWidget : public QWidget { + Q_OBJECT + + public: + explicit ThemeCustomizationWidget(QWidget* parent = nullptr); + ~ThemeCustomizationWidget(); + + void showFeatures(ThemeFields features); + + void applySettings(); + + void loadSettings(); + void retranslate(); + + private slots: + void applyIconTheme(int index); + void applyWidgetTheme(int index); + void applyCatTheme(int index); + + signals: + int currentIconThemeChanged(int index); + int currentWidgetThemeChanged(int index); + int currentCatChanged(int index); + + private: + Ui::ThemeCustomizationWidget* ui; + + //TODO finish implementing + QList> m_iconThemeOptions{ + { "pe_colored", QObject::tr("Simple (Colored Icons)") }, + { "pe_light", QObject::tr("Simple (Light Icons)") }, + { "pe_dark", QObject::tr("Simple (Dark Icons)") }, + { "pe_blue", QObject::tr("Simple (Blue Icons)") }, + { "breeze_light", QObject::tr("Breeze Light") }, + { "breeze_dark", QObject::tr("Breeze Dark") }, + { "OSX", QObject::tr("OSX") }, + { "iOS", QObject::tr("iOS") }, + { "flat", QObject::tr("Flat") }, + { "flat_white", QObject::tr("Flat (White)") }, + { "multimc", QObject::tr("Legacy") }, + { "custom", QObject::tr("Custom") } + }; + QList> m_catOptions{ + { "kitteh", QObject::tr("Background Cat (from MultiMC)") }, + { "rory", QObject::tr("Rory ID 11 (drawn by Ashtaka)") }, + { "rory-flat", QObject::tr("Rory ID 11 (flat edition, drawn by Ashtaka)") }, + { "teawie", QObject::tr("Teawie (drawn by SympathyTea)") } + }; }; \ No newline at end of file diff --git a/launcher/ui/widgets/ThemeCustomizationWidget.ui b/launcher/ui/widgets/ThemeCustomizationWidget.ui index 15ba831e..b2772983 100644 --- a/launcher/ui/widgets/ThemeCustomizationWidget.ui +++ b/launcher/ui/widgets/ThemeCustomizationWidget.ui @@ -1,105 +1,105 @@ - - - ThemeCustomizationWidget - - - - 0 - 0 - 400 - 311 - - - - Form - - - - QLayout::SetMinimumSize - - - 0 - - - 0 - - - 0 - - - 0 - - - - - &Icons - - - iconsComboBox - - - - - - - - 0 - 0 - - - - Qt::StrongFocus - - - - - - - &Colors - - - widgetStyleComboBox - - - - - - - - 0 - 0 - - - - Qt::StrongFocus - - - - - - - C&at - - - backgroundCatComboBox - - - - - - - - 0 - 0 - - - - Qt::StrongFocus - - - - - - - - + + + ThemeCustomizationWidget + + + + 0 + 0 + 400 + 311 + + + + Form + + + + QLayout::SetMinimumSize + + + 0 + + + 0 + + + 0 + + + 0 + + + + + &Icons + + + iconsComboBox + + + + + + + + 0 + 0 + + + + Qt::StrongFocus + + + + + + + &Colors + + + widgetStyleComboBox + + + + + + + + 0 + 0 + + + + Qt::StrongFocus + + + + + + + C&at + + + backgroundCatComboBox + + + + + + + + 0 + 0 + + + + Qt::StrongFocus + + + + + + + + From 5c48f0b458c8b4e9306b6791b228285b6c7f4586 Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Sat, 7 Jan 2023 17:40:29 +0100 Subject: [PATCH 071/152] fix: set minimum size for setup wizard Signed-off-by: Sefa Eyeoglu --- launcher/ui/setupwizard/SetupWizard.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/launcher/ui/setupwizard/SetupWizard.cpp b/launcher/ui/setupwizard/SetupWizard.cpp index 3c8b5d39..3fd9bb23 100644 --- a/launcher/ui/setupwizard/SetupWizard.cpp +++ b/launcher/ui/setupwizard/SetupWizard.cpp @@ -13,7 +13,8 @@ SetupWizard::SetupWizard(QWidget *parent) : QWizard(parent) { setObjectName(QStringLiteral("SetupWizard")); - resize(615, 659); + resize(620, 660); + setMinimumSize(300, 400); // make it ugly everywhere to avoid variability in theming setWizardStyle(QWizard::ClassicStyle); setOptions(QWizard::NoCancelButton | QWizard::IndependentPages | QWizard::HaveCustomButton1); From 668b19d11948bedeff6908d76d63f5a5fad4eb02 Mon Sep 17 00:00:00 2001 From: Tayou Date: Mon, 9 Jan 2023 18:40:58 +0100 Subject: [PATCH 072/152] Add hint about Cat Signed-off-by: Tayou --- launcher/ui/setupwizard/ThemeWizardPage.ui | 15 +++++- .../ui/widgets/ThemeCustomizationWidget.cpp | 14 ++--- .../ui/widgets/ThemeCustomizationWidget.h | 2 +- .../ui/widgets/ThemeCustomizationWidget.ui | 51 ++++++++++++++----- 4 files changed, 62 insertions(+), 20 deletions(-) diff --git a/launcher/ui/setupwizard/ThemeWizardPage.ui b/launcher/ui/setupwizard/ThemeWizardPage.ui index 1ab04fc8..01394ea4 100644 --- a/launcher/ui/setupwizard/ThemeWizardPage.ui +++ b/launcher/ui/setupwizard/ThemeWizardPage.ui @@ -31,6 +31,16 @@ + + + + Hint: 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. + + + true + + + @@ -41,7 +51,7 @@ - Icon Preview: + Preview: @@ -317,6 +327,9 @@ 256 + + The cat appears in the background and does not serve a purpose, it is purely visual. + diff --git a/launcher/ui/widgets/ThemeCustomizationWidget.cpp b/launcher/ui/widgets/ThemeCustomizationWidget.cpp index d0b5be21..dcf13303 100644 --- a/launcher/ui/widgets/ThemeCustomizationWidget.cpp +++ b/launcher/ui/widgets/ThemeCustomizationWidget.cpp @@ -72,10 +72,11 @@ void ThemeCustomizationWidget::showFeatures(ThemeFields features) { void ThemeCustomizationWidget::applyIconTheme(int index) { auto settings = APPLICATION->settings(); - auto original = settings->get("IconTheme").toString(); - settings->set("IconTheme", m_iconThemeOptions[index].first); + auto originalIconTheme = settings->get("IconTheme").toString(); + auto& newIconTheme = m_iconThemeOptions[index].first; + settings->set("IconTheme", newIconTheme); - if (original != settings->get("IconTheme")) { + if (originalIconTheme != newIconTheme) { APPLICATION->applyCurrentlySelectedTheme(); } @@ -113,7 +114,8 @@ void ThemeCustomizationWidget::loadSettings() auto iconTheme = settings->get("IconTheme").toString(); for (auto& iconThemeFromList : m_iconThemeOptions) { - ui->iconsComboBox->addItem(QIcon(QString(":/icons/%1/scalable/settings").arg(iconThemeFromList.first)), iconThemeFromList.second); + QIcon iconForComboBox = QIcon(QString(":/icons/%1/scalable/settings").arg(iconThemeFromList.first)); + ui->iconsComboBox->addItem(iconForComboBox, iconThemeFromList.second); if (iconTheme == iconThemeFromList.first) { ui->iconsComboBox->setCurrentIndex(ui->iconsComboBox->count() - 1); } @@ -134,8 +136,8 @@ void ThemeCustomizationWidget::loadSettings() auto cat = settings->get("BackgroundCat").toString(); for (auto& catFromList : m_catOptions) { - ui->backgroundCatComboBox->addItem(QIcon(QString(":/backgrounds/%1").arg(ThemeManager::getCatImage(catFromList.first))), - catFromList.second); + QIcon catIcon = QIcon(QString(":/backgrounds/%1").arg(ThemeManager::getCatImage(catFromList.first))); + ui->backgroundCatComboBox->addItem(catIcon, catFromList.second); if (cat == catFromList.first) { ui->backgroundCatComboBox->setCurrentIndex(ui->backgroundCatComboBox->count() - 1); } diff --git a/launcher/ui/widgets/ThemeCustomizationWidget.h b/launcher/ui/widgets/ThemeCustomizationWidget.h index be2c4492..d955a266 100644 --- a/launcher/ui/widgets/ThemeCustomizationWidget.h +++ b/launcher/ui/widgets/ThemeCustomizationWidget.h @@ -74,4 +74,4 @@ class ThemeCustomizationWidget : public QWidget { { "rory-flat", QObject::tr("Rory ID 11 (flat edition, drawn by Ashtaka)") }, { "teawie", QObject::tr("Teawie (drawn by SympathyTea)") } }; -}; \ No newline at end of file +}; diff --git a/launcher/ui/widgets/ThemeCustomizationWidget.ui b/launcher/ui/widgets/ThemeCustomizationWidget.ui index b2772983..f216a610 100644 --- a/launcher/ui/widgets/ThemeCustomizationWidget.ui +++ b/launcher/ui/widgets/ThemeCustomizationWidget.ui @@ -7,7 +7,7 @@ 0 0 400 - 311 + 191 @@ -77,6 +77,9 @@ + + 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 @@ -86,17 +89,41 @@ - - - - 0 - 0 - - - - Qt::StrongFocus - - + + + + + + 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. + + + + + + + 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. + + + + + + + .. + + + true + + + + From d45a62b3a61e3da8b113251f7dff55c4f7634197 Mon Sep 17 00:00:00 2001 From: DioEgizio <83089242+DioEgizio@users.noreply.github.com> Date: Tue, 10 Jan 2023 16:47:00 +0100 Subject: [PATCH 073/152] Revert "Merge pull request #729 from DioEgizio/fix-mac-openssl3-failing" it was necessary :/ This reverts commit 976e550aa7291f22f5011178ab824a937f89d11a, reversing changes made to 61144f7a219995fa29531683ed36e8e4002848b5. Signed-off-by: DioEgizio <83089242+DioEgizio@users.noreply.github.com> --- .github/workflows/build.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e0a80f20..9d75a457 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -342,8 +342,9 @@ jobs: if: matrix.name == 'macOS' run: | if [ '${{ secrets.SPARKLE_ED25519_KEY }}' != '' ]; then + brew install openssl@3 echo '${{ secrets.SPARKLE_ED25519_KEY }}' > ed25519-priv.pem - signature=$(openssl pkeyutl -sign -rawin -in ${{ github.workspace }}/PrismLauncher.tar.gz -inkey ed25519-priv.pem | openssl base64 | tr -d \\n) + signature=$(/usr/local/opt/openssl@3/bin/openssl pkeyutl -sign -rawin -in ${{ github.workspace }}/PrismLauncher.tar.gz -inkey ed25519-priv.pem | openssl base64 | tr -d \\n) rm ed25519-priv.pem cat >> $GITHUB_STEP_SUMMARY << EOF ### Artifact Information :information_source: From 391ef64c22f1e106983abe51027096f3bf772c15 Mon Sep 17 00:00:00 2001 From: flow Date: Tue, 10 Jan 2023 12:50:56 -0300 Subject: [PATCH 074/152] fix(FileSystem): don't attempt to trash items on Windows Server For some reason this makes some of our CI test runs super slow, and sometimes fail miserably. Signed-off-by: flow --- launcher/FileSystem.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/launcher/FileSystem.cpp b/launcher/FileSystem.cpp index 7a135811..aee5245d 100644 --- a/launcher/FileSystem.cpp +++ b/launcher/FileSystem.cpp @@ -57,6 +57,7 @@ #include #include #include +#include #include #include #include @@ -251,6 +252,10 @@ bool trash(QString path, QString *pathInTrash) // FIXME: Figure out trash in Flatpak. Qt seemingly doesn't use the Trash portal if (DesktopServices::isFlatpak()) return false; +#if defined Q_OS_WIN32 + if (IsWindowsServer()) + return false; +#endif return QFile::moveToTrash(path, pathInTrash); #endif } From 14278a9e354bd2aef3f9690c8fac32e2ae1ae0ad Mon Sep 17 00:00:00 2001 From: DioEgizio <83089242+DioEgizio@users.noreply.github.com> Date: Tue, 10 Jan 2023 19:56:59 +0100 Subject: [PATCH 075/152] fix: set HOMEBREW_NO_INSTALLED_DEPENDENTS_CHECK to 1 should fix some random failures Signed-off-by: DioEgizio <83089242+DioEgizio@users.noreply.github.com> --- .github/workflows/build.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9d75a457..d074863d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -105,6 +105,7 @@ jobs: INSTALL_APPIMAGE_DIR: "install-appdir" BUILD_DIR: "build" CCACHE_VAR: "" + HOMEBREW_NO_INSTALLED_DEPENDENTS_CHECK: 1 steps: ## From 24a4bd3a1c33702946b88a3d8017268fb8134210 Mon Sep 17 00:00:00 2001 From: Joshua Goins Date: Fri, 6 Jan 2023 15:26:26 -0500 Subject: [PATCH 076/152] refactor: replace hoedown markdown parser with cmark Signed-off-by: Joshua Goins --- launcher/CMakeLists.txt | 4 +- launcher/HoeDown.h | 76 ------------------- launcher/Markdown.h | 34 +++++++++ launcher/ui/dialogs/AboutDialog.cpp | 6 +- launcher/ui/dialogs/ModUpdateDialog.cpp | 11 +-- launcher/ui/dialogs/UpdateDialog.cpp | 5 +- .../ui/pages/instance/ManagedPackPage.cpp | 6 +- launcher/ui/pages/modplatform/ModPage.cpp | 11 +-- launcher/ui/pages/modplatform/ftb/FtbPage.cpp | 5 +- .../modplatform/modrinth/ModrinthPage.cpp | 6 +- 10 files changed, 50 insertions(+), 114 deletions(-) delete mode 100644 launcher/HoeDown.h create mode 100644 launcher/Markdown.h diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index 6ca88ec6..7dc744aa 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -617,7 +617,7 @@ SET(LAUNCHER_SOURCES DesktopServices.cpp VersionProxyModel.h VersionProxyModel.cpp - HoeDown.h + Markdown.h # Super secret! KonamiCode.h @@ -1043,7 +1043,7 @@ target_link_libraries(Launcher_logic ) target_link_libraries(Launcher_logic QuaZip::QuaZip - hoedown + cmark LocalPeer Launcher_rainbow ) diff --git a/launcher/HoeDown.h b/launcher/HoeDown.h deleted file mode 100644 index cb62de6c..00000000 --- a/launcher/HoeDown.h +++ /dev/null @@ -1,76 +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 -#include -#include -#include - -/** - * hoedown wrapper, because dealing with resource lifetime in C is stupid - */ -class HoeDown -{ -public: - class buffer - { - public: - buffer(size_t unit = 4096) - { - buf = hoedown_buffer_new(unit); - } - ~buffer() - { - hoedown_buffer_free(buf); - } - const char * cstr() - { - return hoedown_buffer_cstr(buf); - } - void put(QByteArray input) - { - hoedown_buffer_put(buf, reinterpret_cast(input.data()), input.size()); - } - const uint8_t * data() const - { - return buf->data; - } - size_t size() const - { - return buf->size; - } - hoedown_buffer * buf; - } ib, ob; - HoeDown() - { - renderer = hoedown_html_renderer_new((hoedown_html_flags) 0,0); - document = hoedown_document_new(renderer, (hoedown_extensions) 0, 8); - } - ~HoeDown() - { - hoedown_document_free(document); - hoedown_html_renderer_free(renderer); - } - QString process(QByteArray input) - { - ib.put(input); - hoedown_document_render(document, ob.buf, ib.data(), ib.size()); - return ob.cstr(); - } -private: - hoedown_document * document; - hoedown_renderer * renderer; -}; diff --git a/launcher/Markdown.h b/launcher/Markdown.h new file mode 100644 index 00000000..f115dd57 --- /dev/null +++ b/launcher/Markdown.h @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2023 Joshua Goins + * + * 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 + +static QString markdownToHTML(const QString& markdown) +{ + const QByteArray markdownData = markdown.toUtf8(); + char* buffer = cmark_markdown_to_html(markdownData.constData(), markdownData.length(), CMARK_OPT_NOBREAKS | CMARK_OPT_UNSAFE); + + QString htmlStr(buffer); + + free(buffer); + + return htmlStr; +} \ No newline at end of file diff --git a/launcher/ui/dialogs/AboutDialog.cpp b/launcher/ui/dialogs/AboutDialog.cpp index a36e4a3d..76e3d8ed 100644 --- a/launcher/ui/dialogs/AboutDialog.cpp +++ b/launcher/ui/dialogs/AboutDialog.cpp @@ -39,12 +39,11 @@ #include #include "Application.h" #include "BuildConfig.h" +#include "Markdown.h" #include #include -#include "HoeDown.h" - namespace { QString getLink(QString link, QString name) { return QString("<
%2>").arg(link).arg(name); @@ -114,10 +113,9 @@ QString getCreditsHtml() QString getLicenseHtml() { - HoeDown hoedown; QFile dataFile(":/documents/COPYING.md"); dataFile.open(QIODevice::ReadOnly); - QString output = hoedown.process(dataFile.readAll()); + QString output = markdownToHTML(dataFile.readAll()); return output; } diff --git a/launcher/ui/dialogs/ModUpdateDialog.cpp b/launcher/ui/dialogs/ModUpdateDialog.cpp index cedd4a96..2704243e 100644 --- a/launcher/ui/dialogs/ModUpdateDialog.cpp +++ b/launcher/ui/dialogs/ModUpdateDialog.cpp @@ -7,6 +7,7 @@ #include "FileSystem.h" #include "Json.h" +#include "Markdown.h" #include "tasks/ConcurrentTask.h" @@ -17,7 +18,6 @@ #include "modplatform/flame/FlameCheckUpdate.h" #include "modplatform/modrinth/ModrinthCheckUpdate.h" -#include #include #include @@ -369,14 +369,7 @@ void ModUpdateDialog::appendMod(CheckUpdateTask::UpdatableMod const& info) QString text = info.changelog; switch (info.provider) { case ModPlatform::Provider::MODRINTH: { - HoeDown h; - // HoeDown bug?: \n aren't converted to
- text = h.process(info.changelog.toUtf8()); - - // Don't convert if there's an HTML tag right after (Qt rendering weirdness) - text.remove(QRegularExpression("(\n+)(?=<)")); - text.replace('\n', "
"); - + text = markdownToHTML(info.changelog.toUtf8()); break; } default: diff --git a/launcher/ui/dialogs/UpdateDialog.cpp b/launcher/ui/dialogs/UpdateDialog.cpp index 9e82531a..349d768f 100644 --- a/launcher/ui/dialogs/UpdateDialog.cpp +++ b/launcher/ui/dialogs/UpdateDialog.cpp @@ -41,7 +41,7 @@ #include #include "BuildConfig.h" -#include "HoeDown.h" +#include "Markdown.h" UpdateDialog::UpdateDialog(bool hasUpdate, QWidget *parent) : QDialog(parent), ui(new Ui::UpdateDialog) { @@ -89,8 +89,7 @@ void UpdateDialog::loadChangelog() QString reprocessMarkdown(QByteArray markdown) { - HoeDown hoedown; - QString output = hoedown.process(markdown); + QString output = markdownToHTML(markdown); // HACK: easier than customizing hoedown output.replace(QRegularExpression("GH-([0-9]+)"), "GH-\\1"); diff --git a/launcher/ui/pages/instance/ManagedPackPage.cpp b/launcher/ui/pages/instance/ManagedPackPage.cpp index 4de80468..8d56d894 100644 --- a/launcher/ui/pages/instance/ManagedPackPage.cpp +++ b/launcher/ui/pages/instance/ManagedPackPage.cpp @@ -9,14 +9,13 @@ #include #include -#include - #include "Application.h" #include "BuildConfig.h" #include "InstanceImportTask.h" #include "InstanceList.h" #include "InstanceTask.h" #include "Json.h" +#include "Markdown.h" #include "modplatform/modrinth/ModrinthPackManifest.h" @@ -263,8 +262,7 @@ void ModrinthManagedPackPage::suggestVersion() auto index = ui->versionsComboBox->currentIndex(); auto version = m_pack.versions.at(index); - HoeDown md_parser; - ui->changelogTextBrowser->setHtml(md_parser.process(version.changelog.toUtf8())); + ui->changelogTextBrowser->setHtml(markdownToHTML(version.changelog.toUtf8())); ManagedPackPage::suggestVersion(); } diff --git a/launcher/ui/pages/modplatform/ModPage.cpp b/launcher/ui/pages/modplatform/ModPage.cpp index 75be25b2..0f30689e 100644 --- a/launcher/ui/pages/modplatform/ModPage.cpp +++ b/launcher/ui/pages/modplatform/ModPage.cpp @@ -43,13 +43,11 @@ #include #include -#include - #include "minecraft/MinecraftInstance.h" #include "minecraft/PackProfile.h" #include "ui/dialogs/ModDownloadDialog.h" #include "ui/widgets/ProjectItem.h" - +#include "Markdown.h" ModPage::ModPage(ModDownloadDialog* dialog, BaseInstance* instance, ModAPI* api) : QWidget(dialog) @@ -427,11 +425,6 @@ void ModPage::updateUi() text += "
"; - HoeDown h; - - // hoedown bug: it doesn't handle markdown surrounded by block tags (like center, div) so strip them - current.extraData.body.remove(QRegularExpression("<[^>]*(?:center|div)\\W*>")); - - ui->packDescription->setHtml(text + (current.extraData.body.isEmpty() ? current.description : h.process(current.extraData.body.toUtf8()))); + ui->packDescription->setHtml(text + (current.extraData.body.isEmpty() ? current.description : markdownToHTML(current.extraData.body))); ui->packDescription->flush(); } diff --git a/launcher/ui/pages/modplatform/ftb/FtbPage.cpp b/launcher/ui/pages/modplatform/ftb/FtbPage.cpp index b08f3bc4..7d59a6ae 100644 --- a/launcher/ui/pages/modplatform/ftb/FtbPage.cpp +++ b/launcher/ui/pages/modplatform/ftb/FtbPage.cpp @@ -43,7 +43,7 @@ #include "ui/dialogs/NewInstanceDialog.h" #include "modplatform/modpacksch/FTBPackInstallTask.h" -#include "HoeDown.h" +#include "Markdown.h" FtbPage::FtbPage(NewInstanceDialog* dialog, QWidget *parent) : QWidget(parent), ui(new Ui::FtbPage), dialog(dialog) @@ -175,8 +175,7 @@ void FtbPage::onSelectionChanged(QModelIndex first, QModelIndex second) selected = filterModel->data(first, Qt::UserRole).value(); - HoeDown hoedown; - QString output = hoedown.process(selected.description.toUtf8()); + QString output = markdownToHTML(selected.description.toUtf8()); ui->packDescription->setHtml(output); // reverse foreach, so that the newest versions are first diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp index 8ab2ad1d..0bb11d83 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp @@ -42,11 +42,10 @@ #include "BuildConfig.h" #include "InstanceImportTask.h" #include "Json.h" +#include "Markdown.h" #include "ui/widgets/ProjectItem.h" -#include - #include #include #include @@ -280,8 +279,7 @@ void ModrinthPage::updateUI() text += "
"; - HoeDown h; - text += h.process(current.extra.body.toUtf8()); + text += markdownToHTML(current.extra.body.toUtf8()); ui->packDescription->setHtml(text + current.description); ui->packDescription->flush(); From aa7c910e262d4c3d655a9a7f853b52d7cd0641a9 Mon Sep 17 00:00:00 2001 From: Joshua Goins Date: Fri, 6 Jan 2023 15:47:14 -0500 Subject: [PATCH 077/152] build: remove hoedown vendored source Signed-off-by: Joshua Goins --- CMakeLists.txt | 1 - COPYING.md | 39 +- libraries/README.md | 8 +- libraries/hoedown/CMakeLists.txt | 26 - libraries/hoedown/LICENSE | 15 - libraries/hoedown/README.md | 9 - libraries/hoedown/include/hoedown/autolink.h | 46 - libraries/hoedown/include/hoedown/buffer.h | 134 - libraries/hoedown/include/hoedown/document.h | 172 - libraries/hoedown/include/hoedown/escape.h | 28 - libraries/hoedown/include/hoedown/html.h | 84 - libraries/hoedown/include/hoedown/stack.h | 52 - libraries/hoedown/include/hoedown/version.h | 33 - libraries/hoedown/src/autolink.c | 281 -- libraries/hoedown/src/buffer.c | 308 -- libraries/hoedown/src/document.c | 2958 ------------------ libraries/hoedown/src/escape.c | 188 -- libraries/hoedown/src/html.c | 754 ----- libraries/hoedown/src/html_blocks.c | 240 -- libraries/hoedown/src/html_smartypants.c | 435 --- libraries/hoedown/src/stack.c | 79 - libraries/hoedown/src/version.c | 9 - 22 files changed, 30 insertions(+), 5869 deletions(-) delete mode 100644 libraries/hoedown/CMakeLists.txt delete mode 100644 libraries/hoedown/LICENSE delete mode 100644 libraries/hoedown/README.md delete mode 100644 libraries/hoedown/include/hoedown/autolink.h delete mode 100644 libraries/hoedown/include/hoedown/buffer.h delete mode 100644 libraries/hoedown/include/hoedown/document.h delete mode 100644 libraries/hoedown/include/hoedown/escape.h delete mode 100644 libraries/hoedown/include/hoedown/html.h delete mode 100644 libraries/hoedown/include/hoedown/stack.h delete mode 100644 libraries/hoedown/include/hoedown/version.h delete mode 100644 libraries/hoedown/src/autolink.c delete mode 100644 libraries/hoedown/src/buffer.c delete mode 100644 libraries/hoedown/src/document.c delete mode 100644 libraries/hoedown/src/escape.c delete mode 100644 libraries/hoedown/src/html.c delete mode 100644 libraries/hoedown/src/html_blocks.c delete mode 100644 libraries/hoedown/src/html_smartypants.c delete mode 100644 libraries/hoedown/src/stack.c delete mode 100644 libraries/hoedown/src/version.c diff --git a/CMakeLists.txt b/CMakeLists.txt index c7ba9e9f..f235a2ac 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -374,7 +374,6 @@ option(NBT_BUILD_TESTS "Build NBT library tests" OFF) #FIXME: fix unit tests. add_subdirectory(libraries/libnbtplusplus) add_subdirectory(libraries/systeminfo) # system information library -add_subdirectory(libraries/hoedown) # markdown parser add_subdirectory(libraries/launcher) # java based launcher part for Minecraft add_subdirectory(libraries/javacheck) # java compatibility checker if(NOT ZLIB_FOUND) diff --git a/COPYING.md b/COPYING.md index 75a5c0eb..79290654 100644 --- a/COPYING.md +++ b/COPYING.md @@ -156,23 +156,34 @@ the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -## Hoedown +## cmark - Copyright (c) 2008, Natacha Porté - Copyright (c) 2011, Vicent Martí - Copyright (c) 2014, Xavier Mendez, Devin Torres and the Hoedown authors + Copyright (c) 2014, John MacFarlane - Permission to use, copy, modify, and distribute this software for any - purpose with or without fee is hereby granted, provided that the above - copyright notice and this permission notice appear in all copies. + All rights reserved. - THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES - WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF - MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR - ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES - WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN - ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF - OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ## Batch icon set diff --git a/libraries/README.md b/libraries/README.md index ac5a3618..95be8740 100644 --- a/libraries/README.md +++ b/libraries/README.md @@ -18,11 +18,13 @@ See [github repo](https://github.com/FeralInteractive/gamemode). BSD-3-Clause licensed -## hoedown +## cmark -Hoedown is a revived fork of Sundown, the Markdown parser based on the original code of the Upskirt library by Natacha Porté. +The C reference implementation of CommonMark, a standardized Markdown spec. -See [github repo](https://github.com/hoedown/hoedown). +See [github_repo](https://github.com/commonmark/cmark). + +BSD2 licensed. ## javacheck diff --git a/libraries/hoedown/CMakeLists.txt b/libraries/hoedown/CMakeLists.txt deleted file mode 100644 index 7902e734..00000000 --- a/libraries/hoedown/CMakeLists.txt +++ /dev/null @@ -1,26 +0,0 @@ -# hoedown 3.0.2 - https://github.com/hoedown/hoedown/archive/3.0.2.tar.gz -project(hoedown LANGUAGES C VERSION 3.0.2) - -set(HOEDOWN_SOURCES -include/hoedown/autolink.h -include/hoedown/buffer.h -include/hoedown/document.h -include/hoedown/escape.h -include/hoedown/html.h -include/hoedown/stack.h -include/hoedown/version.h -src/autolink.c -src/buffer.c -src/document.c -src/escape.c -src/html.c -src/html_blocks.c -src/html_smartypants.c -src/stack.c -src/version.c -) - -# Include self. -add_library(hoedown STATIC ${HOEDOWN_SOURCES}) - -target_include_directories(hoedown PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include) diff --git a/libraries/hoedown/LICENSE b/libraries/hoedown/LICENSE deleted file mode 100644 index 4e75de4d..00000000 --- a/libraries/hoedown/LICENSE +++ /dev/null @@ -1,15 +0,0 @@ -Copyright (c) 2008, Natacha Porté -Copyright (c) 2011, Vicent Martí -Copyright (c) 2014, Xavier Mendez, Devin Torres and the Hoedown authors - -Permission to use, copy, modify, and distribute this software for any -purpose with or without fee is hereby granted, provided that the above -copyright notice and this permission notice appear in all copies. - -THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES -WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF -MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR -ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES -WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN -ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF -OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. diff --git a/libraries/hoedown/README.md b/libraries/hoedown/README.md deleted file mode 100644 index abe2b6ca..00000000 --- a/libraries/hoedown/README.md +++ /dev/null @@ -1,9 +0,0 @@ -Hoedown -======= - -This is Hoedown 3.0.2, taken from [the hoedown github repo](https://github.com/hoedown/hoedown). - -`Hoedown` is a revived fork of [Sundown](https://github.com/vmg/sundown), -the Markdown parser based on the original code of the -[Upskirt library](http://fossil.instinctive.eu/libupskirt/index) -by Natacha Porté. diff --git a/libraries/hoedown/include/hoedown/autolink.h b/libraries/hoedown/include/hoedown/autolink.h deleted file mode 100644 index 953e7807..00000000 --- a/libraries/hoedown/include/hoedown/autolink.h +++ /dev/null @@ -1,46 +0,0 @@ -/* autolink.h - versatile autolinker */ - -#ifndef HOEDOWN_AUTOLINK_H -#define HOEDOWN_AUTOLINK_H - -#include "buffer.h" - -#ifdef __cplusplus -extern "C" { -#endif - - -/************* - * CONSTANTS * - *************/ - -typedef enum hoedown_autolink_flags { - HOEDOWN_AUTOLINK_SHORT_DOMAINS = (1 << 0) -} hoedown_autolink_flags; - - -/************* - * FUNCTIONS * - *************/ - -/* hoedown_autolink_is_safe: verify that a URL has a safe protocol */ -int hoedown_autolink_is_safe(const uint8_t *data, size_t size); - -/* hoedown_autolink__www: search for the next www link in data */ -size_t hoedown_autolink__www(size_t *rewind_p, hoedown_buffer *link, - uint8_t *data, size_t offset, size_t size, hoedown_autolink_flags flags); - -/* hoedown_autolink__email: search for the next email in data */ -size_t hoedown_autolink__email(size_t *rewind_p, hoedown_buffer *link, - uint8_t *data, size_t offset, size_t size, hoedown_autolink_flags flags); - -/* hoedown_autolink__url: search for the next URL in data */ -size_t hoedown_autolink__url(size_t *rewind_p, hoedown_buffer *link, - uint8_t *data, size_t offset, size_t size, hoedown_autolink_flags flags); - - -#ifdef __cplusplus -} -#endif - -#endif /** HOEDOWN_AUTOLINK_H **/ diff --git a/libraries/hoedown/include/hoedown/buffer.h b/libraries/hoedown/include/hoedown/buffer.h deleted file mode 100644 index 062d86ce..00000000 --- a/libraries/hoedown/include/hoedown/buffer.h +++ /dev/null @@ -1,134 +0,0 @@ -/* buffer.h - simple, fast buffers */ - -#ifndef HOEDOWN_BUFFER_H -#define HOEDOWN_BUFFER_H - -#include -#include -#include -#include -#include - -#ifdef __cplusplus -extern "C" { -#endif - -#if defined(_MSC_VER) -#define __attribute__(x) -#define inline __inline -#define __builtin_expect(x,n) x -#endif - - -/********* - * TYPES * - *********/ - -typedef void *(*hoedown_realloc_callback)(void *, size_t); -typedef void (*hoedown_free_callback)(void *); - -struct hoedown_buffer { - uint8_t *data; /* actual character data */ - size_t size; /* size of the string */ - size_t asize; /* allocated size (0 = volatile buffer) */ - size_t unit; /* reallocation unit size (0 = read-only buffer) */ - - hoedown_realloc_callback data_realloc; - hoedown_free_callback data_free; - hoedown_free_callback buffer_free; -}; - -typedef struct hoedown_buffer hoedown_buffer; - - -/************* - * FUNCTIONS * - *************/ - -/* allocation wrappers */ -void *hoedown_malloc(size_t size) __attribute__ ((malloc)); -void *hoedown_calloc(size_t nmemb, size_t size) __attribute__ ((malloc)); -void *hoedown_realloc(void *ptr, size_t size) __attribute__ ((malloc)); - -/* hoedown_buffer_init: initialize a buffer with custom allocators */ -void hoedown_buffer_init( - hoedown_buffer *buffer, - size_t unit, - hoedown_realloc_callback data_realloc, - hoedown_free_callback data_free, - hoedown_free_callback buffer_free -); - -/* hoedown_buffer_uninit: uninitialize an existing buffer */ -void hoedown_buffer_uninit(hoedown_buffer *buf); - -/* hoedown_buffer_new: allocate a new buffer */ -hoedown_buffer *hoedown_buffer_new(size_t unit) __attribute__ ((malloc)); - -/* hoedown_buffer_reset: free internal data of the buffer */ -void hoedown_buffer_reset(hoedown_buffer *buf); - -/* hoedown_buffer_grow: increase the allocated size to the given value */ -void hoedown_buffer_grow(hoedown_buffer *buf, size_t neosz); - -/* hoedown_buffer_put: append raw data to a buffer */ -void hoedown_buffer_put(hoedown_buffer *buf, const uint8_t *data, size_t size); - -/* hoedown_buffer_puts: append a NUL-terminated string to a buffer */ -void hoedown_buffer_puts(hoedown_buffer *buf, const char *str); - -/* hoedown_buffer_putc: append a single char to a buffer */ -void hoedown_buffer_putc(hoedown_buffer *buf, uint8_t c); - -/* hoedown_buffer_putf: read from a file and append to a buffer, until EOF or error */ -int hoedown_buffer_putf(hoedown_buffer *buf, FILE* file); - -/* hoedown_buffer_set: replace the buffer's contents with raw data */ -void hoedown_buffer_set(hoedown_buffer *buf, const uint8_t *data, size_t size); - -/* hoedown_buffer_sets: replace the buffer's contents with a NUL-terminated string */ -void hoedown_buffer_sets(hoedown_buffer *buf, const char *str); - -/* hoedown_buffer_eq: compare a buffer's data with other data for equality */ -int hoedown_buffer_eq(const hoedown_buffer *buf, const uint8_t *data, size_t size); - -/* hoedown_buffer_eq: compare a buffer's data with NUL-terminated string for equality */ -int hoedown_buffer_eqs(const hoedown_buffer *buf, const char *str); - -/* hoedown_buffer_prefix: compare the beginning of a buffer with a string */ -int hoedown_buffer_prefix(const hoedown_buffer *buf, const char *prefix); - -/* hoedown_buffer_slurp: remove a given number of bytes from the head of the buffer */ -void hoedown_buffer_slurp(hoedown_buffer *buf, size_t size); - -/* hoedown_buffer_cstr: NUL-termination of the string array (making a C-string) */ -const char *hoedown_buffer_cstr(hoedown_buffer *buf); - -/* hoedown_buffer_printf: formatted printing to a buffer */ -void hoedown_buffer_printf(hoedown_buffer *buf, const char *fmt, ...) __attribute__ ((format (printf, 2, 3))); - -/* hoedown_buffer_put_utf8: put a Unicode character encoded as UTF-8 */ -void hoedown_buffer_put_utf8(hoedown_buffer *buf, unsigned int codepoint); - -/* hoedown_buffer_free: free the buffer */ -void hoedown_buffer_free(hoedown_buffer *buf); - - -/* HOEDOWN_BUFPUTSL: optimized hoedown_buffer_puts of a string literal */ -#define HOEDOWN_BUFPUTSL(output, literal) \ - hoedown_buffer_put(output, (const uint8_t *)literal, sizeof(literal) - 1) - -/* HOEDOWN_BUFSETSL: optimized hoedown_buffer_sets of a string literal */ -#define HOEDOWN_BUFSETSL(output, literal) \ - hoedown_buffer_set(output, (const uint8_t *)literal, sizeof(literal) - 1) - -/* HOEDOWN_BUFEQSL: optimized hoedown_buffer_eqs of a string literal */ -#define HOEDOWN_BUFEQSL(output, literal) \ - hoedown_buffer_eq(output, (const uint8_t *)literal, sizeof(literal) - 1) - - -#ifdef __cplusplus -} -#endif - -#endif /** HOEDOWN_BUFFER_H **/ diff --git a/libraries/hoedown/include/hoedown/document.h b/libraries/hoedown/include/hoedown/document.h deleted file mode 100644 index 210c565e..00000000 --- a/libraries/hoedown/include/hoedown/document.h +++ /dev/null @@ -1,172 +0,0 @@ -/* document.h - generic markdown parser */ - -#ifndef HOEDOWN_DOCUMENT_H -#define HOEDOWN_DOCUMENT_H - -#include "buffer.h" -#include "autolink.h" - -#ifdef __cplusplus -extern "C" { -#endif - - -/************* - * CONSTANTS * - *************/ - -typedef enum hoedown_extensions { - /* block-level extensions */ - HOEDOWN_EXT_TABLES = (1 << 0), - HOEDOWN_EXT_FENCED_CODE = (1 << 1), - HOEDOWN_EXT_FOOTNOTES = (1 << 2), - - /* span-level extensions */ - HOEDOWN_EXT_AUTOLINK = (1 << 3), - HOEDOWN_EXT_STRIKETHROUGH = (1 << 4), - HOEDOWN_EXT_UNDERLINE = (1 << 5), - HOEDOWN_EXT_HIGHLIGHT = (1 << 6), - HOEDOWN_EXT_QUOTE = (1 << 7), - HOEDOWN_EXT_SUPERSCRIPT = (1 << 8), - HOEDOWN_EXT_MATH = (1 << 9), - - /* other flags */ - HOEDOWN_EXT_NO_INTRA_EMPHASIS = (1 << 11), - HOEDOWN_EXT_SPACE_HEADERS = (1 << 12), - HOEDOWN_EXT_MATH_EXPLICIT = (1 << 13), - - /* negative flags */ - HOEDOWN_EXT_DISABLE_INDENTED_CODE = (1 << 14) -} hoedown_extensions; - -#define HOEDOWN_EXT_BLOCK (\ - HOEDOWN_EXT_TABLES |\ - HOEDOWN_EXT_FENCED_CODE |\ - HOEDOWN_EXT_FOOTNOTES ) - -#define HOEDOWN_EXT_SPAN (\ - HOEDOWN_EXT_AUTOLINK |\ - HOEDOWN_EXT_STRIKETHROUGH |\ - HOEDOWN_EXT_UNDERLINE |\ - HOEDOWN_EXT_HIGHLIGHT |\ - HOEDOWN_EXT_QUOTE |\ - HOEDOWN_EXT_SUPERSCRIPT |\ - HOEDOWN_EXT_MATH ) - -#define HOEDOWN_EXT_FLAGS (\ - HOEDOWN_EXT_NO_INTRA_EMPHASIS |\ - HOEDOWN_EXT_SPACE_HEADERS |\ - HOEDOWN_EXT_MATH_EXPLICIT ) - -#define HOEDOWN_EXT_NEGATIVE (\ - HOEDOWN_EXT_DISABLE_INDENTED_CODE ) - -typedef enum hoedown_list_flags { - HOEDOWN_LIST_ORDERED = (1 << 0), - HOEDOWN_LI_BLOCK = (1 << 1) /*
  • containing block data */ -} hoedown_list_flags; - -typedef enum hoedown_table_flags { - HOEDOWN_TABLE_ALIGN_LEFT = 1, - HOEDOWN_TABLE_ALIGN_RIGHT = 2, - HOEDOWN_TABLE_ALIGN_CENTER = 3, - HOEDOWN_TABLE_ALIGNMASK = 3, - HOEDOWN_TABLE_HEADER = 4 -} hoedown_table_flags; - -typedef enum hoedown_autolink_type { - HOEDOWN_AUTOLINK_NONE, /* used internally when it is not an autolink*/ - HOEDOWN_AUTOLINK_NORMAL, /* normal http/http/ftp/mailto/etc link */ - HOEDOWN_AUTOLINK_EMAIL /* e-mail link without explit mailto: */ -} hoedown_autolink_type; - - -/********* - * TYPES * - *********/ - -struct hoedown_document; -typedef struct hoedown_document hoedown_document; - -struct hoedown_renderer_data { - void *opaque; -}; -typedef struct hoedown_renderer_data hoedown_renderer_data; - -/* hoedown_renderer - functions for rendering parsed data */ -struct hoedown_renderer { - /* state object */ - void *opaque; - - /* block level callbacks - NULL skips the block */ - void (*blockcode)(hoedown_buffer *ob, const hoedown_buffer *text, const hoedown_buffer *lang, const hoedown_renderer_data *data); - void (*blockquote)(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data); - void (*header)(hoedown_buffer *ob, const hoedown_buffer *content, int level, const hoedown_renderer_data *data); - void (*hrule)(hoedown_buffer *ob, const hoedown_renderer_data *data); - void (*list)(hoedown_buffer *ob, const hoedown_buffer *content, hoedown_list_flags flags, const hoedown_renderer_data *data); - void (*listitem)(hoedown_buffer *ob, const hoedown_buffer *content, hoedown_list_flags flags, const hoedown_renderer_data *data); - void (*paragraph)(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data); - void (*table)(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data); - void (*table_header)(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data); - void (*table_body)(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data); - void (*table_row)(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data); - void (*table_cell)(hoedown_buffer *ob, const hoedown_buffer *content, hoedown_table_flags flags, const hoedown_renderer_data *data); - void (*footnotes)(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data); - void (*footnote_def)(hoedown_buffer *ob, const hoedown_buffer *content, unsigned int num, const hoedown_renderer_data *data); - void (*blockhtml)(hoedown_buffer *ob, const hoedown_buffer *text, const hoedown_renderer_data *data); - - /* span level callbacks - NULL or return 0 prints the span verbatim */ - int (*autolink)(hoedown_buffer *ob, const hoedown_buffer *link, hoedown_autolink_type type, const hoedown_renderer_data *data); - int (*codespan)(hoedown_buffer *ob, const hoedown_buffer *text, const hoedown_renderer_data *data); - int (*double_emphasis)(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data); - int (*emphasis)(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data); - int (*underline)(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data); - int (*highlight)(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data); - int (*quote)(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data); - int (*image)(hoedown_buffer *ob, const hoedown_buffer *link, const hoedown_buffer *title, const hoedown_buffer *alt, const hoedown_renderer_data *data); - int (*linebreak)(hoedown_buffer *ob, const hoedown_renderer_data *data); - int (*link)(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_buffer *link, const hoedown_buffer *title, const hoedown_renderer_data *data); - int (*triple_emphasis)(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data); - int (*strikethrough)(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data); - int (*superscript)(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data); - int (*footnote_ref)(hoedown_buffer *ob, unsigned int num, const hoedown_renderer_data *data); - int (*math)(hoedown_buffer *ob, const hoedown_buffer *text, int displaymode, const hoedown_renderer_data *data); - int (*raw_html)(hoedown_buffer *ob, const hoedown_buffer *text, const hoedown_renderer_data *data); - - /* low level callbacks - NULL copies input directly into the output */ - void (*entity)(hoedown_buffer *ob, const hoedown_buffer *text, const hoedown_renderer_data *data); - void (*normal_text)(hoedown_buffer *ob, const hoedown_buffer *text, const hoedown_renderer_data *data); - - /* miscellaneous callbacks */ - void (*doc_header)(hoedown_buffer *ob, int inline_render, const hoedown_renderer_data *data); - void (*doc_footer)(hoedown_buffer *ob, int inline_render, const hoedown_renderer_data *data); -}; -typedef struct hoedown_renderer hoedown_renderer; - - -/************* - * FUNCTIONS * - *************/ - -/* hoedown_document_new: allocate a new document processor instance */ -hoedown_document *hoedown_document_new( - const hoedown_renderer *renderer, - hoedown_extensions extensions, - size_t max_nesting -) __attribute__ ((malloc)); - -/* hoedown_document_render: render regular Markdown using the document processor */ -void hoedown_document_render(hoedown_document *doc, hoedown_buffer *ob, const uint8_t *data, size_t size); - -/* hoedown_document_render_inline: render inline Markdown using the document processor */ -void hoedown_document_render_inline(hoedown_document *doc, hoedown_buffer *ob, const uint8_t *data, size_t size); - -/* hoedown_document_free: deallocate a document processor instance */ -void hoedown_document_free(hoedown_document *doc); - - -#ifdef __cplusplus -} -#endif - -#endif /** HOEDOWN_DOCUMENT_H **/ diff --git a/libraries/hoedown/include/hoedown/escape.h b/libraries/hoedown/include/hoedown/escape.h deleted file mode 100644 index d7659c27..00000000 --- a/libraries/hoedown/include/hoedown/escape.h +++ /dev/null @@ -1,28 +0,0 @@ -/* escape.h - escape utilities */ - -#ifndef HOEDOWN_ESCAPE_H -#define HOEDOWN_ESCAPE_H - -#include "buffer.h" - -#ifdef __cplusplus -extern "C" { -#endif - - -/************* - * FUNCTIONS * - *************/ - -/* hoedown_escape_href: escape (part of) a URL inside HTML */ -void hoedown_escape_href(hoedown_buffer *ob, const uint8_t *data, size_t size); - -/* hoedown_escape_html: escape HTML */ -void hoedown_escape_html(hoedown_buffer *ob, const uint8_t *data, size_t size, int secure); - - -#ifdef __cplusplus -} -#endif - -#endif /** HOEDOWN_ESCAPE_H **/ diff --git a/libraries/hoedown/include/hoedown/html.h b/libraries/hoedown/include/hoedown/html.h deleted file mode 100644 index 7c68809a..00000000 --- a/libraries/hoedown/include/hoedown/html.h +++ /dev/null @@ -1,84 +0,0 @@ -/* html.h - HTML renderer and utilities */ - -#ifndef HOEDOWN_HTML_H -#define HOEDOWN_HTML_H - -#include "document.h" -#include "buffer.h" - -#ifdef __cplusplus -extern "C" { -#endif - - -/************* - * CONSTANTS * - *************/ - -typedef enum hoedown_html_flags { - HOEDOWN_HTML_SKIP_HTML = (1 << 0), - HOEDOWN_HTML_ESCAPE = (1 << 1), - HOEDOWN_HTML_HARD_WRAP = (1 << 2), - HOEDOWN_HTML_USE_XHTML = (1 << 3) -} hoedown_html_flags; - -typedef enum hoedown_html_tag { - HOEDOWN_HTML_TAG_NONE = 0, - HOEDOWN_HTML_TAG_OPEN, - HOEDOWN_HTML_TAG_CLOSE -} hoedown_html_tag; - - -/********* - * TYPES * - *********/ - -struct hoedown_html_renderer_state { - void *opaque; - - struct { - int header_count; - int current_level; - int level_offset; - int nesting_level; - } toc_data; - - hoedown_html_flags flags; - - /* extra callbacks */ - void (*link_attributes)(hoedown_buffer *ob, const hoedown_buffer *url, const hoedown_renderer_data *data); -}; -typedef struct hoedown_html_renderer_state hoedown_html_renderer_state; - - -/************* - * FUNCTIONS * - *************/ - -/* hoedown_html_smartypants: process an HTML snippet using SmartyPants for smart punctuation */ -void hoedown_html_smartypants(hoedown_buffer *ob, const uint8_t *data, size_t size); - -/* hoedown_html_is_tag: checks if data starts with a specific tag, returns the tag type or NONE */ -hoedown_html_tag hoedown_html_is_tag(const uint8_t *data, size_t size, const char *tagname); - - -/* hoedown_html_renderer_new: allocates a regular HTML renderer */ -hoedown_renderer *hoedown_html_renderer_new( - hoedown_html_flags render_flags, - int nesting_level -) __attribute__ ((malloc)); - -/* hoedown_html_toc_renderer_new: like hoedown_html_renderer_new, but the returned renderer produces the Table of Contents */ -hoedown_renderer *hoedown_html_toc_renderer_new( - int nesting_level -) __attribute__ ((malloc)); - -/* hoedown_html_renderer_free: deallocate an HTML renderer */ -void hoedown_html_renderer_free(hoedown_renderer *renderer); - - -#ifdef __cplusplus -} -#endif - -#endif /** HOEDOWN_HTML_H **/ diff --git a/libraries/hoedown/include/hoedown/stack.h b/libraries/hoedown/include/hoedown/stack.h deleted file mode 100644 index d1855f4f..00000000 --- a/libraries/hoedown/include/hoedown/stack.h +++ /dev/null @@ -1,52 +0,0 @@ -/* stack.h - simple stacking */ - -#ifndef HOEDOWN_STACK_H -#define HOEDOWN_STACK_H - -#include - -#ifdef __cplusplus -extern "C" { -#endif - - -/********* - * TYPES * - *********/ - -struct hoedown_stack { - void **item; - size_t size; - size_t asize; -}; -typedef struct hoedown_stack hoedown_stack; - - -/************* - * FUNCTIONS * - *************/ - -/* hoedown_stack_init: initialize a stack */ -void hoedown_stack_init(hoedown_stack *st, size_t initial_size); - -/* hoedown_stack_uninit: free internal data of the stack */ -void hoedown_stack_uninit(hoedown_stack *st); - -/* hoedown_stack_grow: increase the allocated size to the given value */ -void hoedown_stack_grow(hoedown_stack *st, size_t neosz); - -/* hoedown_stack_push: push an item to the top of the stack */ -void hoedown_stack_push(hoedown_stack *st, void *item); - -/* hoedown_stack_pop: retrieve and remove the item at the top of the stack */ -void *hoedown_stack_pop(hoedown_stack *st); - -/* hoedown_stack_top: retrieve the item at the top of the stack */ -void *hoedown_stack_top(const hoedown_stack *st); - - -#ifdef __cplusplus -} -#endif - -#endif /** HOEDOWN_STACK_H **/ diff --git a/libraries/hoedown/include/hoedown/version.h b/libraries/hoedown/include/hoedown/version.h deleted file mode 100644 index 4938cae5..00000000 --- a/libraries/hoedown/include/hoedown/version.h +++ /dev/null @@ -1,33 +0,0 @@ -/* version.h - holds Hoedown's version */ - -#ifndef HOEDOWN_VERSION_H -#define HOEDOWN_VERSION_H - -#ifdef __cplusplus -extern "C" { -#endif - - -/************* - * CONSTANTS * - *************/ - -#define HOEDOWN_VERSION "3.0.2" -#define HOEDOWN_VERSION_MAJOR 3 -#define HOEDOWN_VERSION_MINOR 0 -#define HOEDOWN_VERSION_REVISION 2 - - -/************* - * FUNCTIONS * - *************/ - -/* hoedown_version: retrieve Hoedown's version numbers */ -void hoedown_version(int *major, int *minor, int *revision); - - -#ifdef __cplusplus -} -#endif - -#endif /** HOEDOWN_VERSION_H **/ diff --git a/libraries/hoedown/src/autolink.c b/libraries/hoedown/src/autolink.c deleted file mode 100644 index 3592b8e3..00000000 --- a/libraries/hoedown/src/autolink.c +++ /dev/null @@ -1,281 +0,0 @@ -#include "hoedown/autolink.h" - -#include -#include -#include -#include - -#ifndef _MSC_VER -#include -#else -#define strncasecmp _strnicmp -#endif - -int -hoedown_autolink_is_safe(const uint8_t *data, size_t size) -{ - static const size_t valid_uris_count = 6; - static const char *valid_uris[] = { - "http://", "https://", "/", "#", "ftp://", "mailto:" - }; - static const size_t valid_uris_size[] = { 7, 8, 1, 1, 6, 7 }; - size_t i; - - for (i = 0; i < valid_uris_count; ++i) { - size_t len = valid_uris_size[i]; - - if (size > len && - strncasecmp((char *)data, valid_uris[i], len) == 0 && - isalnum(data[len])) - return 1; - } - - return 0; -} - -static size_t -autolink_delim(uint8_t *data, size_t link_end, size_t max_rewind, size_t size) -{ - uint8_t cclose, copen = 0; - size_t i; - - for (i = 0; i < link_end; ++i) - if (data[i] == '<') { - link_end = i; - break; - } - - while (link_end > 0) { - if (strchr("?!.,:", data[link_end - 1]) != NULL) - link_end--; - - else if (data[link_end - 1] == ';') { - size_t new_end = link_end - 2; - - while (new_end > 0 && isalpha(data[new_end])) - new_end--; - - if (new_end < link_end - 2 && data[new_end] == '&') - link_end = new_end; - else - link_end--; - } - else break; - } - - if (link_end == 0) - return 0; - - cclose = data[link_end - 1]; - - switch (cclose) { - case '"': copen = '"'; break; - case '\'': copen = '\''; break; - case ')': copen = '('; break; - case ']': copen = '['; break; - case '}': copen = '{'; break; - } - - if (copen != 0) { - size_t closing = 0; - size_t opening = 0; - size_t i = 0; - - /* Try to close the final punctuation sign in this same line; - * if we managed to close it outside of the URL, that means that it's - * not part of the URL. If it closes inside the URL, that means it - * is part of the URL. - * - * Examples: - * - * foo http://www.pokemon.com/Pikachu_(Electric) bar - * => http://www.pokemon.com/Pikachu_(Electric) - * - * foo (http://www.pokemon.com/Pikachu_(Electric)) bar - * => http://www.pokemon.com/Pikachu_(Electric) - * - * foo http://www.pokemon.com/Pikachu_(Electric)) bar - * => http://www.pokemon.com/Pikachu_(Electric)) - * - * (foo http://www.pokemon.com/Pikachu_(Electric)) bar - * => foo http://www.pokemon.com/Pikachu_(Electric) - */ - - while (i < link_end) { - if (data[i] == copen) - opening++; - else if (data[i] == cclose) - closing++; - - i++; - } - - if (closing != opening) - link_end--; - } - - return link_end; -} - -static size_t -check_domain(uint8_t *data, size_t size, int allow_short) -{ - size_t i, np = 0; - - if (!isalnum(data[0])) - return 0; - - for (i = 1; i < size - 1; ++i) { - if (strchr(".:", data[i]) != NULL) np++; - else if (!isalnum(data[i]) && data[i] != '-') break; - } - - if (allow_short) { - /* We don't need a valid domain in the strict sense (with - * least one dot; so just make sure it's composed of valid - * domain characters and return the length of the the valid - * sequence. */ - return i; - } else { - /* a valid domain needs to have at least a dot. - * that's as far as we get */ - return np ? i : 0; - } -} - -size_t -hoedown_autolink__www( - size_t *rewind_p, - hoedown_buffer *link, - uint8_t *data, - size_t max_rewind, - size_t size, - hoedown_autolink_flags flags) -{ - size_t link_end; - - if (max_rewind > 0 && !ispunct(data[-1]) && !isspace(data[-1])) - return 0; - - if (size < 4 || memcmp(data, "www.", strlen("www.")) != 0) - return 0; - - link_end = check_domain(data, size, 0); - - if (link_end == 0) - return 0; - - while (link_end < size && !isspace(data[link_end])) - link_end++; - - link_end = autolink_delim(data, link_end, max_rewind, size); - - if (link_end == 0) - return 0; - - hoedown_buffer_put(link, data, link_end); - *rewind_p = 0; - - return (int)link_end; -} - -size_t -hoedown_autolink__email( - size_t *rewind_p, - hoedown_buffer *link, - uint8_t *data, - size_t max_rewind, - size_t size, - hoedown_autolink_flags flags) -{ - size_t link_end, rewind; - int nb = 0, np = 0; - - for (rewind = 0; rewind < max_rewind; ++rewind) { - uint8_t c = data[-1 - rewind]; - - if (isalnum(c)) - continue; - - if (strchr(".+-_", c) != NULL) - continue; - - break; - } - - if (rewind == 0) - return 0; - - for (link_end = 0; link_end < size; ++link_end) { - uint8_t c = data[link_end]; - - if (isalnum(c)) - continue; - - if (c == '@') - nb++; - else if (c == '.' && link_end < size - 1) - np++; - else if (c != '-' && c != '_') - break; - } - - if (link_end < 2 || nb != 1 || np == 0 || - !isalpha(data[link_end - 1])) - return 0; - - link_end = autolink_delim(data, link_end, max_rewind, size); - - if (link_end == 0) - return 0; - - hoedown_buffer_put(link, data - rewind, link_end + rewind); - *rewind_p = rewind; - - return link_end; -} - -size_t -hoedown_autolink__url( - size_t *rewind_p, - hoedown_buffer *link, - uint8_t *data, - size_t max_rewind, - size_t size, - hoedown_autolink_flags flags) -{ - size_t link_end, rewind = 0, domain_len; - - if (size < 4 || data[1] != '/' || data[2] != '/') - return 0; - - while (rewind < max_rewind && isalpha(data[-1 - rewind])) - rewind++; - - if (!hoedown_autolink_is_safe(data - rewind, size + rewind)) - return 0; - - link_end = strlen("://"); - - domain_len = check_domain( - data + link_end, - size - link_end, - flags & HOEDOWN_AUTOLINK_SHORT_DOMAINS); - - if (domain_len == 0) - return 0; - - link_end += domain_len; - while (link_end < size && !isspace(data[link_end])) - link_end++; - - link_end = autolink_delim(data, link_end, max_rewind, size); - - if (link_end == 0) - return 0; - - hoedown_buffer_put(link, data - rewind, link_end + rewind); - *rewind_p = rewind; - - return link_end; -} diff --git a/libraries/hoedown/src/buffer.c b/libraries/hoedown/src/buffer.c deleted file mode 100644 index 024a8bcc..00000000 --- a/libraries/hoedown/src/buffer.c +++ /dev/null @@ -1,308 +0,0 @@ -#include "hoedown/buffer.h" - -#include -#include -#include -#include - -void * -hoedown_malloc(size_t size) -{ - void *ret = malloc(size); - - if (!ret) { - fprintf(stderr, "Allocation failed.\n"); - abort(); - } - - return ret; -} - -void * -hoedown_calloc(size_t nmemb, size_t size) -{ - void *ret = calloc(nmemb, size); - - if (!ret) { - fprintf(stderr, "Allocation failed.\n"); - abort(); - } - - return ret; -} - -void * -hoedown_realloc(void *ptr, size_t size) -{ - void *ret = realloc(ptr, size); - - if (!ret) { - fprintf(stderr, "Allocation failed.\n"); - abort(); - } - - return ret; -} - -void -hoedown_buffer_init( - hoedown_buffer *buf, - size_t unit, - hoedown_realloc_callback data_realloc, - hoedown_free_callback data_free, - hoedown_free_callback buffer_free) -{ - assert(buf); - - buf->data = NULL; - buf->size = buf->asize = 0; - buf->unit = unit; - buf->data_realloc = data_realloc; - buf->data_free = data_free; - buf->buffer_free = buffer_free; -} - -void -hoedown_buffer_uninit(hoedown_buffer *buf) -{ - assert(buf && buf->unit); - buf->data_free(buf->data); -} - -hoedown_buffer * -hoedown_buffer_new(size_t unit) -{ - hoedown_buffer *ret = hoedown_malloc(sizeof (hoedown_buffer)); - hoedown_buffer_init(ret, unit, hoedown_realloc, free, free); - return ret; -} - -void -hoedown_buffer_free(hoedown_buffer *buf) -{ - if (!buf) return; - assert(buf && buf->unit); - - buf->data_free(buf->data); - - if (buf->buffer_free) - buf->buffer_free(buf); -} - -void -hoedown_buffer_reset(hoedown_buffer *buf) -{ - assert(buf && buf->unit); - - buf->data_free(buf->data); - buf->data = NULL; - buf->size = buf->asize = 0; -} - -void -hoedown_buffer_grow(hoedown_buffer *buf, size_t neosz) -{ - size_t neoasz; - assert(buf && buf->unit); - - if (buf->asize >= neosz) - return; - - neoasz = buf->asize + buf->unit; - while (neoasz < neosz) - neoasz += buf->unit; - - buf->data = (uint8_t *) buf->data_realloc(buf->data, neoasz); - buf->asize = neoasz; -} - -void -hoedown_buffer_put(hoedown_buffer *buf, const uint8_t *data, size_t size) -{ - assert(buf && buf->unit); - - if (buf->size + size > buf->asize) - hoedown_buffer_grow(buf, buf->size + size); - - memcpy(buf->data + buf->size, data, size); - buf->size += size; -} - -void -hoedown_buffer_puts(hoedown_buffer *buf, const char *str) -{ - hoedown_buffer_put(buf, (const uint8_t *)str, strlen(str)); -} - -void -hoedown_buffer_putc(hoedown_buffer *buf, uint8_t c) -{ - assert(buf && buf->unit); - - if (buf->size >= buf->asize) - hoedown_buffer_grow(buf, buf->size + 1); - - buf->data[buf->size] = c; - buf->size += 1; -} - -int -hoedown_buffer_putf(hoedown_buffer *buf, FILE *file) -{ - assert(buf && buf->unit); - - while (!(feof(file) || ferror(file))) { - hoedown_buffer_grow(buf, buf->size + buf->unit); - buf->size += fread(buf->data + buf->size, 1, buf->unit, file); - } - - return ferror(file); -} - -void -hoedown_buffer_set(hoedown_buffer *buf, const uint8_t *data, size_t size) -{ - assert(buf && buf->unit); - - if (size > buf->asize) - hoedown_buffer_grow(buf, size); - - memcpy(buf->data, data, size); - buf->size = size; -} - -void -hoedown_buffer_sets(hoedown_buffer *buf, const char *str) -{ - hoedown_buffer_set(buf, (const uint8_t *)str, strlen(str)); -} - -int -hoedown_buffer_eq(const hoedown_buffer *buf, const uint8_t *data, size_t size) -{ - if (buf->size != size) return 0; - return memcmp(buf->data, data, size) == 0; -} - -int -hoedown_buffer_eqs(const hoedown_buffer *buf, const char *str) -{ - return hoedown_buffer_eq(buf, (const uint8_t *)str, strlen(str)); -} - -int -hoedown_buffer_prefix(const hoedown_buffer *buf, const char *prefix) -{ - size_t i; - - for (i = 0; i < buf->size; ++i) { - if (prefix[i] == 0) - return 0; - - if (buf->data[i] != prefix[i]) - return buf->data[i] - prefix[i]; - } - - return 0; -} - -void -hoedown_buffer_slurp(hoedown_buffer *buf, size_t size) -{ - assert(buf && buf->unit); - - if (size >= buf->size) { - buf->size = 0; - return; - } - - buf->size -= size; - memmove(buf->data, buf->data + size, buf->size); -} - -const char * -hoedown_buffer_cstr(hoedown_buffer *buf) -{ - assert(buf && buf->unit); - - if (buf->size < buf->asize && buf->data[buf->size] == 0) - return (char *)buf->data; - - hoedown_buffer_grow(buf, buf->size + 1); - buf->data[buf->size] = 0; - - return (char *)buf->data; -} - -void -hoedown_buffer_printf(hoedown_buffer *buf, const char *fmt, ...) -{ - va_list ap; - int n; - - assert(buf && buf->unit); - - if (buf->size >= buf->asize) - hoedown_buffer_grow(buf, buf->size + 1); - - va_start(ap, fmt); - n = vsnprintf((char *)buf->data + buf->size, buf->asize - buf->size, fmt, ap); - va_end(ap); - - if (n < 0) { -#ifndef _MSC_VER - return; -#else - va_start(ap, fmt); - n = _vscprintf(fmt, ap); - va_end(ap); -#endif - } - - if ((size_t)n >= buf->asize - buf->size) { - hoedown_buffer_grow(buf, buf->size + n + 1); - - va_start(ap, fmt); - n = vsnprintf((char *)buf->data + buf->size, buf->asize - buf->size, fmt, ap); - va_end(ap); - } - - if (n < 0) - return; - - buf->size += n; -} - -void hoedown_buffer_put_utf8(hoedown_buffer *buf, unsigned int c) { - unsigned char unichar[4]; - - assert(buf && buf->unit); - - if (c < 0x80) { - hoedown_buffer_putc(buf, c); - } - else if (c < 0x800) { - unichar[0] = 192 + (c / 64); - unichar[1] = 128 + (c % 64); - hoedown_buffer_put(buf, unichar, 2); - } - else if (c - 0xd800u < 0x800) { - HOEDOWN_BUFPUTSL(buf, "\xef\xbf\xbd"); - } - else if (c < 0x10000) { - unichar[0] = 224 + (c / 4096); - unichar[1] = 128 + (c / 64) % 64; - unichar[2] = 128 + (c % 64); - hoedown_buffer_put(buf, unichar, 3); - } - else if (c < 0x110000) { - unichar[0] = 240 + (c / 262144); - unichar[1] = 128 + (c / 4096) % 64; - unichar[2] = 128 + (c / 64) % 64; - unichar[3] = 128 + (c % 64); - hoedown_buffer_put(buf, unichar, 4); - } - else { - HOEDOWN_BUFPUTSL(buf, "\xef\xbf\xbd"); - } -} diff --git a/libraries/hoedown/src/document.c b/libraries/hoedown/src/document.c deleted file mode 100644 index e9e2ab11..00000000 --- a/libraries/hoedown/src/document.c +++ /dev/null @@ -1,2958 +0,0 @@ -#include "hoedown/document.h" - -#include -#include -#include -#include - -#include "hoedown/stack.h" - -#ifndef _MSC_VER -#include -#else -#define strncasecmp _strnicmp -#endif - -#define REF_TABLE_SIZE 8 - -#define BUFFER_BLOCK 0 -#define BUFFER_SPAN 1 - -#define HOEDOWN_LI_END 8 /* internal list flag */ - -const char *hoedown_find_block_tag(const char *str, unsigned int len); - -/*************** - * LOCAL TYPES * - ***************/ - -/* link_ref: reference to a link */ -struct link_ref { - unsigned int id; - - hoedown_buffer *link; - hoedown_buffer *title; - - struct link_ref *next; -}; - -/* footnote_ref: reference to a footnote */ -struct footnote_ref { - unsigned int id; - - int is_used; - unsigned int num; - - hoedown_buffer *contents; -}; - -/* footnote_item: an item in a footnote_list */ -struct footnote_item { - struct footnote_ref *ref; - struct footnote_item *next; -}; - -/* footnote_list: linked list of footnote_item */ -struct footnote_list { - unsigned int count; - struct footnote_item *head; - struct footnote_item *tail; -}; - -/* char_trigger: function pointer to render active chars */ -/* returns the number of chars taken care of */ -/* data is the pointer of the beginning of the span */ -/* offset is the number of valid chars before data */ -typedef size_t -(*char_trigger)(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t offset, size_t size); - -static size_t char_emphasis(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t offset, size_t size); -static size_t char_quote(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t offset, size_t size); -static size_t char_linebreak(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t offset, size_t size); -static size_t char_codespan(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t offset, size_t size); -static size_t char_escape(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t offset, size_t size); -static size_t char_entity(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t offset, size_t size); -static size_t char_langle_tag(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t offset, size_t size); -static size_t char_autolink_url(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t offset, size_t size); -static size_t char_autolink_email(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t offset, size_t size); -static size_t char_autolink_www(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t offset, size_t size); -static size_t char_link(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t offset, size_t size); -static size_t char_superscript(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t offset, size_t size); -static size_t char_math(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t offset, size_t size); - -enum markdown_char_t { - MD_CHAR_NONE = 0, - MD_CHAR_EMPHASIS, - MD_CHAR_CODESPAN, - MD_CHAR_LINEBREAK, - MD_CHAR_LINK, - MD_CHAR_LANGLE, - MD_CHAR_ESCAPE, - MD_CHAR_ENTITY, - MD_CHAR_AUTOLINK_URL, - MD_CHAR_AUTOLINK_EMAIL, - MD_CHAR_AUTOLINK_WWW, - MD_CHAR_SUPERSCRIPT, - MD_CHAR_QUOTE, - MD_CHAR_MATH -}; - -static char_trigger markdown_char_ptrs[] = { - NULL, - &char_emphasis, - &char_codespan, - &char_linebreak, - &char_link, - &char_langle_tag, - &char_escape, - &char_entity, - &char_autolink_url, - &char_autolink_email, - &char_autolink_www, - &char_superscript, - &char_quote, - &char_math -}; - -struct hoedown_document { - hoedown_renderer md; - hoedown_renderer_data data; - - struct link_ref *refs[REF_TABLE_SIZE]; - struct footnote_list footnotes_found; - struct footnote_list footnotes_used; - uint8_t active_char[256]; - hoedown_stack work_bufs[2]; - hoedown_extensions ext_flags; - size_t max_nesting; - int in_link_body; -}; - -/*************************** - * HELPER FUNCTIONS * - ***************************/ - -static hoedown_buffer * -newbuf(hoedown_document *doc, int type) -{ - static const size_t buf_size[2] = {256, 64}; - hoedown_buffer *work = NULL; - hoedown_stack *pool = &doc->work_bufs[type]; - - if (pool->size < pool->asize && - pool->item[pool->size] != NULL) { - work = pool->item[pool->size++]; - work->size = 0; - } else { - work = hoedown_buffer_new(buf_size[type]); - hoedown_stack_push(pool, work); - } - - return work; -} - -static void -popbuf(hoedown_document *doc, int type) -{ - doc->work_bufs[type].size--; -} - -static void -unscape_text(hoedown_buffer *ob, hoedown_buffer *src) -{ - size_t i = 0, org; - while (i < src->size) { - org = i; - while (i < src->size && src->data[i] != '\\') - i++; - - if (i > org) - hoedown_buffer_put(ob, src->data + org, i - org); - - if (i + 1 >= src->size) - break; - - hoedown_buffer_putc(ob, src->data[i + 1]); - i += 2; - } -} - -static unsigned int -hash_link_ref(const uint8_t *link_ref, size_t length) -{ - size_t i; - unsigned int hash = 0; - - for (i = 0; i < length; ++i) - hash = tolower(link_ref[i]) + (hash << 6) + (hash << 16) - hash; - - return hash; -} - -static struct link_ref * -add_link_ref( - struct link_ref **references, - const uint8_t *name, size_t name_size) -{ - struct link_ref *ref = hoedown_calloc(1, sizeof(struct link_ref)); - - ref->id = hash_link_ref(name, name_size); - ref->next = references[ref->id % REF_TABLE_SIZE]; - - references[ref->id % REF_TABLE_SIZE] = ref; - return ref; -} - -static struct link_ref * -find_link_ref(struct link_ref **references, uint8_t *name, size_t length) -{ - unsigned int hash = hash_link_ref(name, length); - struct link_ref *ref = NULL; - - ref = references[hash % REF_TABLE_SIZE]; - - while (ref != NULL) { - if (ref->id == hash) - return ref; - - ref = ref->next; - } - - return NULL; -} - -static void -free_link_refs(struct link_ref **references) -{ - size_t i; - - for (i = 0; i < REF_TABLE_SIZE; ++i) { - struct link_ref *r = references[i]; - struct link_ref *next; - - while (r) { - next = r->next; - hoedown_buffer_free(r->link); - hoedown_buffer_free(r->title); - free(r); - r = next; - } - } -} - -static struct footnote_ref * -create_footnote_ref(struct footnote_list *list, const uint8_t *name, size_t name_size) -{ - struct footnote_ref *ref = hoedown_calloc(1, sizeof(struct footnote_ref)); - - ref->id = hash_link_ref(name, name_size); - - return ref; -} - -static int -add_footnote_ref(struct footnote_list *list, struct footnote_ref *ref) -{ - struct footnote_item *item = hoedown_calloc(1, sizeof(struct footnote_item)); - if (!item) - return 0; - item->ref = ref; - - if (list->head == NULL) { - list->head = list->tail = item; - } else { - list->tail->next = item; - list->tail = item; - } - list->count++; - - return 1; -} - -static struct footnote_ref * -find_footnote_ref(struct footnote_list *list, uint8_t *name, size_t length) -{ - unsigned int hash = hash_link_ref(name, length); - struct footnote_item *item = NULL; - - item = list->head; - - while (item != NULL) { - if (item->ref->id == hash) - return item->ref; - item = item->next; - } - - return NULL; -} - -static void -free_footnote_ref(struct footnote_ref *ref) -{ - hoedown_buffer_free(ref->contents); - free(ref); -} - -static void -free_footnote_list(struct footnote_list *list, int free_refs) -{ - struct footnote_item *item = list->head; - struct footnote_item *next; - - while (item) { - next = item->next; - if (free_refs) - free_footnote_ref(item->ref); - free(item); - item = next; - } -} - - -/* - * Check whether a char is a Markdown spacing char. - - * Right now we only consider spaces the actual - * space and a newline: tabs and carriage returns - * are filtered out during the preprocessing phase. - * - * If we wanted to actually be UTF-8 compliant, we - * should instead extract an Unicode codepoint from - * this character and check for space properties. - */ -static int -_isspace(int c) -{ - return c == ' ' || c == '\n'; -} - -/* is_empty_all: verify that all the data is spacing */ -static int -is_empty_all(const uint8_t *data, size_t size) -{ - size_t i = 0; - while (i < size && _isspace(data[i])) i++; - return i == size; -} - -/* - * Replace all spacing characters in data with spaces. As a special - * case, this collapses a newline with the previous space, if possible. - */ -static void -replace_spacing(hoedown_buffer *ob, const uint8_t *data, size_t size) -{ - size_t i = 0, mark; - hoedown_buffer_grow(ob, size); - while (1) { - mark = i; - while (i < size && data[i] != '\n') i++; - hoedown_buffer_put(ob, data + mark, i - mark); - - if (i >= size) break; - - if (!(i > 0 && data[i-1] == ' ')) - hoedown_buffer_putc(ob, ' '); - i++; - } -} - -/**************************** - * INLINE PARSING FUNCTIONS * - ****************************/ - -/* is_mail_autolink • looks for the address part of a mail autolink and '>' */ -/* this is less strict than the original markdown e-mail address matching */ -static size_t -is_mail_autolink(uint8_t *data, size_t size) -{ - size_t i = 0, nb = 0; - - /* address is assumed to be: [-@._a-zA-Z0-9]+ with exactly one '@' */ - for (i = 0; i < size; ++i) { - if (isalnum(data[i])) - continue; - - switch (data[i]) { - case '@': - nb++; - - case '-': - case '.': - case '_': - break; - - case '>': - return (nb == 1) ? i + 1 : 0; - - default: - return 0; - } - } - - return 0; -} - -/* tag_length • returns the length of the given tag, or 0 is it's not valid */ -static size_t -tag_length(uint8_t *data, size_t size, hoedown_autolink_type *autolink) -{ - size_t i, j; - - /* a valid tag can't be shorter than 3 chars */ - if (size < 3) return 0; - - /* begins with a '<' optionally followed by '/', followed by letter or number */ - if (data[0] != '<') return 0; - i = (data[1] == '/') ? 2 : 1; - - if (!isalnum(data[i])) - return 0; - - /* scheme test */ - *autolink = HOEDOWN_AUTOLINK_NONE; - - /* try to find the beginning of an URI */ - while (i < size && (isalnum(data[i]) || data[i] == '.' || data[i] == '+' || data[i] == '-')) - i++; - - if (i > 1 && data[i] == '@') { - if ((j = is_mail_autolink(data + i, size - i)) != 0) { - *autolink = HOEDOWN_AUTOLINK_EMAIL; - return i + j; - } - } - - if (i > 2 && data[i] == ':') { - *autolink = HOEDOWN_AUTOLINK_NORMAL; - i++; - } - - /* completing autolink test: no spacing or ' or " */ - if (i >= size) - *autolink = HOEDOWN_AUTOLINK_NONE; - - else if (*autolink) { - j = i; - - while (i < size) { - if (data[i] == '\\') i += 2; - else if (data[i] == '>' || data[i] == '\'' || - data[i] == '"' || data[i] == ' ' || data[i] == '\n') - break; - else i++; - } - - if (i >= size) return 0; - if (i > j && data[i] == '>') return i + 1; - /* one of the forbidden chars has been found */ - *autolink = HOEDOWN_AUTOLINK_NONE; - } - - /* looking for something looking like a tag end */ - while (i < size && data[i] != '>') i++; - if (i >= size) return 0; - return i + 1; -} - -/* parse_inline • parses inline markdown elements */ -static void -parse_inline(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t size) -{ - size_t i = 0, end = 0, consumed = 0; - hoedown_buffer work = { 0, 0, 0, 0, NULL, NULL, NULL }; - uint8_t *active_char = doc->active_char; - - if (doc->work_bufs[BUFFER_SPAN].size + - doc->work_bufs[BUFFER_BLOCK].size > doc->max_nesting) - return; - - while (i < size) { - /* copying inactive chars into the output */ - while (end < size && active_char[data[end]] == 0) - end++; - - if (doc->md.normal_text) { - work.data = data + i; - work.size = end - i; - doc->md.normal_text(ob, &work, &doc->data); - } - else - hoedown_buffer_put(ob, data + i, end - i); - - if (end >= size) break; - i = end; - - end = markdown_char_ptrs[ (int)active_char[data[end]] ](ob, doc, data + i, i - consumed, size - i); - if (!end) /* no action from the callback */ - end = i + 1; - else { - i += end; - end = i; - consumed = i; - } - } -} - -/* is_escaped • returns whether special char at data[loc] is escaped by '\\' */ -static int -is_escaped(uint8_t *data, size_t loc) -{ - size_t i = loc; - while (i >= 1 && data[i - 1] == '\\') - i--; - - /* odd numbers of backslashes escapes data[loc] */ - return (loc - i) % 2; -} - -/* find_emph_char • looks for the next emph uint8_t, skipping other constructs */ -static size_t -find_emph_char(uint8_t *data, size_t size, uint8_t c) -{ - size_t i = 0; - - while (i < size) { - while (i < size && data[i] != c && data[i] != '[' && data[i] != '`') - i++; - - if (i == size) - return 0; - - /* not counting escaped chars */ - if (is_escaped(data, i)) { - i++; continue; - } - - if (data[i] == c) - return i; - - /* skipping a codespan */ - if (data[i] == '`') { - size_t span_nb = 0, bt; - size_t tmp_i = 0; - - /* counting the number of opening backticks */ - while (i < size && data[i] == '`') { - i++; span_nb++; - } - - if (i >= size) return 0; - - /* finding the matching closing sequence */ - bt = 0; - while (i < size && bt < span_nb) { - if (!tmp_i && data[i] == c) tmp_i = i; - if (data[i] == '`') bt++; - else bt = 0; - i++; - } - - /* not a well-formed codespan; use found matching emph char */ - if (i >= size) return tmp_i; - } - /* skipping a link */ - else if (data[i] == '[') { - size_t tmp_i = 0; - uint8_t cc; - - i++; - while (i < size && data[i] != ']') { - if (!tmp_i && data[i] == c) tmp_i = i; - i++; - } - - i++; - while (i < size && _isspace(data[i])) - i++; - - if (i >= size) - return tmp_i; - - switch (data[i]) { - case '[': - cc = ']'; break; - - case '(': - cc = ')'; break; - - default: - if (tmp_i) - return tmp_i; - else - continue; - } - - i++; - while (i < size && data[i] != cc) { - if (!tmp_i && data[i] == c) tmp_i = i; - i++; - } - - if (i >= size) - return tmp_i; - - i++; - } - } - - return 0; -} - -/* parse_emph1 • parsing single emphase */ -/* closed by a symbol not preceded by spacing and not followed by symbol */ -static size_t -parse_emph1(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t size, uint8_t c) -{ - size_t i = 0, len; - hoedown_buffer *work = 0; - int r; - - /* skipping one symbol if coming from emph3 */ - if (size > 1 && data[0] == c && data[1] == c) i = 1; - - while (i < size) { - len = find_emph_char(data + i, size - i, c); - if (!len) return 0; - i += len; - if (i >= size) return 0; - - if (data[i] == c && !_isspace(data[i - 1])) { - - if (doc->ext_flags & HOEDOWN_EXT_NO_INTRA_EMPHASIS) { - if (i + 1 < size && isalnum(data[i + 1])) - continue; - } - - work = newbuf(doc, BUFFER_SPAN); - parse_inline(work, doc, data, i); - - if (doc->ext_flags & HOEDOWN_EXT_UNDERLINE && c == '_') - r = doc->md.underline(ob, work, &doc->data); - else - r = doc->md.emphasis(ob, work, &doc->data); - - popbuf(doc, BUFFER_SPAN); - return r ? i + 1 : 0; - } - } - - return 0; -} - -/* parse_emph2 • parsing single emphase */ -static size_t -parse_emph2(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t size, uint8_t c) -{ - size_t i = 0, len; - hoedown_buffer *work = 0; - int r; - - while (i < size) { - len = find_emph_char(data + i, size - i, c); - if (!len) return 0; - i += len; - - if (i + 1 < size && data[i] == c && data[i + 1] == c && i && !_isspace(data[i - 1])) { - work = newbuf(doc, BUFFER_SPAN); - parse_inline(work, doc, data, i); - - if (c == '~') - r = doc->md.strikethrough(ob, work, &doc->data); - else if (c == '=') - r = doc->md.highlight(ob, work, &doc->data); - else - r = doc->md.double_emphasis(ob, work, &doc->data); - - popbuf(doc, BUFFER_SPAN); - return r ? i + 2 : 0; - } - i++; - } - return 0; -} - -/* parse_emph3 • parsing single emphase */ -/* finds the first closing tag, and delegates to the other emph */ -static size_t -parse_emph3(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t size, uint8_t c) -{ - size_t i = 0, len; - int r; - - while (i < size) { - len = find_emph_char(data + i, size - i, c); - if (!len) return 0; - i += len; - - /* skip spacing preceded symbols */ - if (data[i] != c || _isspace(data[i - 1])) - continue; - - if (i + 2 < size && data[i + 1] == c && data[i + 2] == c && doc->md.triple_emphasis) { - /* triple symbol found */ - hoedown_buffer *work = newbuf(doc, BUFFER_SPAN); - - parse_inline(work, doc, data, i); - r = doc->md.triple_emphasis(ob, work, &doc->data); - popbuf(doc, BUFFER_SPAN); - return r ? i + 3 : 0; - - } else if (i + 1 < size && data[i + 1] == c) { - /* double symbol found, handing over to emph1 */ - len = parse_emph1(ob, doc, data - 2, size + 2, c); - if (!len) return 0; - else return len - 2; - - } else { - /* single symbol found, handing over to emph2 */ - len = parse_emph2(ob, doc, data - 1, size + 1, c); - if (!len) return 0; - else return len - 1; - } - } - return 0; -} - -/* parse_math • parses a math span until the given ending delimiter */ -static size_t -parse_math(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t offset, size_t size, const char *end, size_t delimsz, int displaymode) -{ - hoedown_buffer text = { NULL, 0, 0, 0, NULL, NULL, NULL }; - size_t i = delimsz; - - if (!doc->md.math) - return 0; - - /* find ending delimiter */ - while (1) { - while (i < size && data[i] != (uint8_t)end[0]) - i++; - - if (i >= size) - return 0; - - if (!is_escaped(data, i) && !(i + delimsz > size) - && memcmp(data + i, end, delimsz) == 0) - break; - - i++; - } - - /* prepare buffers */ - text.data = data + delimsz; - text.size = i - delimsz; - - /* if this is a $$ and MATH_EXPLICIT is not active, - * guess whether displaymode should be enabled from the context */ - i += delimsz; - if (delimsz == 2 && !(doc->ext_flags & HOEDOWN_EXT_MATH_EXPLICIT)) - displaymode = is_empty_all(data - offset, offset) && is_empty_all(data + i, size - i); - - /* call callback */ - if (doc->md.math(ob, &text, displaymode, &doc->data)) - return i; - - return 0; -} - -/* char_emphasis • single and double emphasis parsing */ -static size_t -char_emphasis(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t offset, size_t size) -{ - uint8_t c = data[0]; - size_t ret; - - if (doc->ext_flags & HOEDOWN_EXT_NO_INTRA_EMPHASIS) { - if (offset > 0 && !_isspace(data[-1]) && data[-1] != '>' && data[-1] != '(') - return 0; - } - - if (size > 2 && data[1] != c) { - /* spacing cannot follow an opening emphasis; - * strikethrough and highlight only takes two characters '~~' */ - if (c == '~' || c == '=' || _isspace(data[1]) || (ret = parse_emph1(ob, doc, data + 1, size - 1, c)) == 0) - return 0; - - return ret + 1; - } - - if (size > 3 && data[1] == c && data[2] != c) { - if (_isspace(data[2]) || (ret = parse_emph2(ob, doc, data + 2, size - 2, c)) == 0) - return 0; - - return ret + 2; - } - - if (size > 4 && data[1] == c && data[2] == c && data[3] != c) { - if (c == '~' || c == '=' || _isspace(data[3]) || (ret = parse_emph3(ob, doc, data + 3, size - 3, c)) == 0) - return 0; - - return ret + 3; - } - - return 0; -} - - -/* char_linebreak • '\n' preceded by two spaces (assuming linebreak != 0) */ -static size_t -char_linebreak(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t offset, size_t size) -{ - if (offset < 2 || data[-1] != ' ' || data[-2] != ' ') - return 0; - - /* removing the last space from ob and rendering */ - while (ob->size && ob->data[ob->size - 1] == ' ') - ob->size--; - - return doc->md.linebreak(ob, &doc->data) ? 1 : 0; -} - - -/* char_codespan • '`' parsing a code span (assuming codespan != 0) */ -static size_t -char_codespan(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t offset, size_t size) -{ - hoedown_buffer work = { NULL, 0, 0, 0, NULL, NULL, NULL }; - size_t end, nb = 0, i, f_begin, f_end; - - /* counting the number of backticks in the delimiter */ - while (nb < size && data[nb] == '`') - nb++; - - /* finding the next delimiter */ - i = 0; - for (end = nb; end < size && i < nb; end++) { - if (data[end] == '`') i++; - else i = 0; - } - - if (i < nb && end >= size) - return 0; /* no matching delimiter */ - - /* trimming outside spaces */ - f_begin = nb; - while (f_begin < end && data[f_begin] == ' ') - f_begin++; - - f_end = end - nb; - while (f_end > nb && data[f_end-1] == ' ') - f_end--; - - /* real code span */ - if (f_begin < f_end) { - work.data = data + f_begin; - work.size = f_end - f_begin; - - if (!doc->md.codespan(ob, &work, &doc->data)) - end = 0; - } else { - if (!doc->md.codespan(ob, 0, &doc->data)) - end = 0; - } - - return end; -} - -/* char_quote • '"' parsing a quote */ -static size_t -char_quote(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t offset, size_t size) -{ - size_t end, nq = 0, i, f_begin, f_end; - - /* counting the number of quotes in the delimiter */ - while (nq < size && data[nq] == '"') - nq++; - - /* finding the next delimiter */ - end = nq; - while (1) { - i = end; - end += find_emph_char(data + end, size - end, '"'); - if (end == i) return 0; /* no matching delimiter */ - i = end; - while (end < size && data[end] == '"' && end - i < nq) end++; - if (end - i >= nq) break; - } - - /* trimming outside spaces */ - f_begin = nq; - while (f_begin < end && data[f_begin] == ' ') - f_begin++; - - f_end = end - nq; - while (f_end > nq && data[f_end-1] == ' ') - f_end--; - - /* real quote */ - if (f_begin < f_end) { - hoedown_buffer *work = newbuf(doc, BUFFER_SPAN); - parse_inline(work, doc, data + f_begin, f_end - f_begin); - - if (!doc->md.quote(ob, work, &doc->data)) - end = 0; - popbuf(doc, BUFFER_SPAN); - } else { - if (!doc->md.quote(ob, 0, &doc->data)) - end = 0; - } - - return end; -} - - -/* char_escape • '\\' backslash escape */ -static size_t -char_escape(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t offset, size_t size) -{ - static const char *escape_chars = "\\`*_{}[]()#+-.!:|&<>^~=\"$"; - hoedown_buffer work = { 0, 0, 0, 0, NULL, NULL, NULL }; - size_t w; - - if (size > 1) { - if (data[1] == '\\' && (doc->ext_flags & HOEDOWN_EXT_MATH) && - size > 2 && (data[2] == '(' || data[2] == '[')) { - const char *end = (data[2] == '[') ? "\\\\]" : "\\\\)"; - w = parse_math(ob, doc, data, offset, size, end, 3, data[2] == '['); - if (w) return w; - } - - if (strchr(escape_chars, data[1]) == NULL) - return 0; - - if (doc->md.normal_text) { - work.data = data + 1; - work.size = 1; - doc->md.normal_text(ob, &work, &doc->data); - } - else hoedown_buffer_putc(ob, data[1]); - } else if (size == 1) { - hoedown_buffer_putc(ob, data[0]); - } - - return 2; -} - -/* char_entity • '&' escaped when it doesn't belong to an entity */ -/* valid entities are assumed to be anything matching &#?[A-Za-z0-9]+; */ -static size_t -char_entity(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t offset, size_t size) -{ - size_t end = 1; - hoedown_buffer work = { 0, 0, 0, 0, NULL, NULL, NULL }; - - if (end < size && data[end] == '#') - end++; - - while (end < size && isalnum(data[end])) - end++; - - if (end < size && data[end] == ';') - end++; /* real entity */ - else - return 0; /* lone '&' */ - - if (doc->md.entity) { - work.data = data; - work.size = end; - doc->md.entity(ob, &work, &doc->data); - } - else hoedown_buffer_put(ob, data, end); - - return end; -} - -/* char_langle_tag • '<' when tags or autolinks are allowed */ -static size_t -char_langle_tag(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t offset, size_t size) -{ - hoedown_buffer work = { NULL, 0, 0, 0, NULL, NULL, NULL }; - hoedown_autolink_type altype = HOEDOWN_AUTOLINK_NONE; - size_t end = tag_length(data, size, &altype); - int ret = 0; - - work.data = data; - work.size = end; - - if (end > 2) { - if (doc->md.autolink && altype != HOEDOWN_AUTOLINK_NONE) { - hoedown_buffer *u_link = newbuf(doc, BUFFER_SPAN); - work.data = data + 1; - work.size = end - 2; - unscape_text(u_link, &work); - ret = doc->md.autolink(ob, u_link, altype, &doc->data); - popbuf(doc, BUFFER_SPAN); - } - else if (doc->md.raw_html) - ret = doc->md.raw_html(ob, &work, &doc->data); - } - - if (!ret) return 0; - else return end; -} - -static size_t -char_autolink_www(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t offset, size_t size) -{ - hoedown_buffer *link, *link_url, *link_text; - size_t link_len, rewind; - - if (!doc->md.link || doc->in_link_body) - return 0; - - link = newbuf(doc, BUFFER_SPAN); - - if ((link_len = hoedown_autolink__www(&rewind, link, data, offset, size, HOEDOWN_AUTOLINK_SHORT_DOMAINS)) > 0) { - link_url = newbuf(doc, BUFFER_SPAN); - HOEDOWN_BUFPUTSL(link_url, "http://"); - hoedown_buffer_put(link_url, link->data, link->size); - - ob->size -= rewind; - if (doc->md.normal_text) { - link_text = newbuf(doc, BUFFER_SPAN); - doc->md.normal_text(link_text, link, &doc->data); - doc->md.link(ob, link_text, link_url, NULL, &doc->data); - popbuf(doc, BUFFER_SPAN); - } else { - doc->md.link(ob, link, link_url, NULL, &doc->data); - } - popbuf(doc, BUFFER_SPAN); - } - - popbuf(doc, BUFFER_SPAN); - return link_len; -} - -static size_t -char_autolink_email(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t offset, size_t size) -{ - hoedown_buffer *link; - size_t link_len, rewind; - - if (!doc->md.autolink || doc->in_link_body) - return 0; - - link = newbuf(doc, BUFFER_SPAN); - - if ((link_len = hoedown_autolink__email(&rewind, link, data, offset, size, 0)) > 0) { - ob->size -= rewind; - doc->md.autolink(ob, link, HOEDOWN_AUTOLINK_EMAIL, &doc->data); - } - - popbuf(doc, BUFFER_SPAN); - return link_len; -} - -static size_t -char_autolink_url(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t offset, size_t size) -{ - hoedown_buffer *link; - size_t link_len, rewind; - - if (!doc->md.autolink || doc->in_link_body) - return 0; - - link = newbuf(doc, BUFFER_SPAN); - - if ((link_len = hoedown_autolink__url(&rewind, link, data, offset, size, 0)) > 0) { - ob->size -= rewind; - doc->md.autolink(ob, link, HOEDOWN_AUTOLINK_NORMAL, &doc->data); - } - - popbuf(doc, BUFFER_SPAN); - return link_len; -} - -/* char_link • '[': parsing a link, a footnote or an image */ -static size_t -char_link(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t offset, size_t size) -{ - int is_img = (offset && data[-1] == '!' && !is_escaped(data - offset, offset - 1)); - int is_footnote = (doc->ext_flags & HOEDOWN_EXT_FOOTNOTES && data[1] == '^'); - size_t i = 1, txt_e, link_b = 0, link_e = 0, title_b = 0, title_e = 0; - hoedown_buffer *content = NULL; - hoedown_buffer *link = NULL; - hoedown_buffer *title = NULL; - hoedown_buffer *u_link = NULL; - size_t org_work_size = doc->work_bufs[BUFFER_SPAN].size; - int ret = 0, in_title = 0, qtype = 0; - - /* checking whether the correct renderer exists */ - if ((is_footnote && !doc->md.footnote_ref) || (is_img && !doc->md.image) - || (!is_img && !is_footnote && !doc->md.link)) - goto cleanup; - - /* looking for the matching closing bracket */ - i += find_emph_char(data + i, size - i, ']'); - txt_e = i; - - if (i < size && data[i] == ']') i++; - else goto cleanup; - - /* footnote link */ - if (is_footnote) { - hoedown_buffer id = { NULL, 0, 0, 0, NULL, NULL, NULL }; - struct footnote_ref *fr; - - if (txt_e < 3) - goto cleanup; - - id.data = data + 2; - id.size = txt_e - 2; - - fr = find_footnote_ref(&doc->footnotes_found, id.data, id.size); - - /* mark footnote used */ - if (fr && !fr->is_used) { - if(!add_footnote_ref(&doc->footnotes_used, fr)) - goto cleanup; - fr->is_used = 1; - fr->num = doc->footnotes_used.count; - - /* render */ - if (doc->md.footnote_ref) - ret = doc->md.footnote_ref(ob, fr->num, &doc->data); - } - - goto cleanup; - } - - /* skip any amount of spacing */ - /* (this is much more laxist than original markdown syntax) */ - while (i < size && _isspace(data[i])) - i++; - - /* inline style link */ - if (i < size && data[i] == '(') { - size_t nb_p; - - /* skipping initial spacing */ - i++; - - while (i < size && _isspace(data[i])) - i++; - - link_b = i; - - /* looking for link end: ' " ) */ - /* Count the number of open parenthesis */ - nb_p = 0; - - while (i < size) { - if (data[i] == '\\') i += 2; - else if (data[i] == '(' && i != 0) { - nb_p++; i++; - } - else if (data[i] == ')') { - if (nb_p == 0) break; - else nb_p--; i++; - } else if (i >= 1 && _isspace(data[i-1]) && (data[i] == '\'' || data[i] == '"')) break; - else i++; - } - - if (i >= size) goto cleanup; - link_e = i; - - /* looking for title end if present */ - if (data[i] == '\'' || data[i] == '"') { - qtype = data[i]; - in_title = 1; - i++; - title_b = i; - - while (i < size) { - if (data[i] == '\\') i += 2; - else if (data[i] == qtype) {in_title = 0; i++;} - else if ((data[i] == ')') && !in_title) break; - else i++; - } - - if (i >= size) goto cleanup; - - /* skipping spacing after title */ - title_e = i - 1; - while (title_e > title_b && _isspace(data[title_e])) - title_e--; - - /* checking for closing quote presence */ - if (data[title_e] != '\'' && data[title_e] != '"') { - title_b = title_e = 0; - link_e = i; - } - } - - /* remove spacing at the end of the link */ - while (link_e > link_b && _isspace(data[link_e - 1])) - link_e--; - - /* remove optional angle brackets around the link */ - if (data[link_b] == '<') link_b++; - if (data[link_e - 1] == '>') link_e--; - - /* building escaped link and title */ - if (link_e > link_b) { - link = newbuf(doc, BUFFER_SPAN); - hoedown_buffer_put(link, data + link_b, link_e - link_b); - } - - if (title_e > title_b) { - title = newbuf(doc, BUFFER_SPAN); - hoedown_buffer_put(title, data + title_b, title_e - title_b); - } - - i++; - } - - /* reference style link */ - else if (i < size && data[i] == '[') { - hoedown_buffer *id = newbuf(doc, BUFFER_SPAN); - struct link_ref *lr; - - /* looking for the id */ - i++; - link_b = i; - while (i < size && data[i] != ']') i++; - if (i >= size) goto cleanup; - link_e = i; - - /* finding the link_ref */ - if (link_b == link_e) - replace_spacing(id, data + 1, txt_e - 1); - else - hoedown_buffer_put(id, data + link_b, link_e - link_b); - - lr = find_link_ref(doc->refs, id->data, id->size); - if (!lr) - goto cleanup; - - /* keeping link and title from link_ref */ - link = lr->link; - title = lr->title; - i++; - } - - /* shortcut reference style link */ - else { - hoedown_buffer *id = newbuf(doc, BUFFER_SPAN); - struct link_ref *lr; - - /* crafting the id */ - replace_spacing(id, data + 1, txt_e - 1); - - /* finding the link_ref */ - lr = find_link_ref(doc->refs, id->data, id->size); - if (!lr) - goto cleanup; - - /* keeping link and title from link_ref */ - link = lr->link; - title = lr->title; - - /* rewinding the spacing */ - i = txt_e + 1; - } - - /* building content: img alt is kept, only link content is parsed */ - if (txt_e > 1) { - content = newbuf(doc, BUFFER_SPAN); - if (is_img) { - hoedown_buffer_put(content, data + 1, txt_e - 1); - } else { - /* disable autolinking when parsing inline the - * content of a link */ - doc->in_link_body = 1; - parse_inline(content, doc, data + 1, txt_e - 1); - doc->in_link_body = 0; - } - } - - if (link) { - u_link = newbuf(doc, BUFFER_SPAN); - unscape_text(u_link, link); - } - - /* calling the relevant rendering function */ - if (is_img) { - if (ob->size && ob->data[ob->size - 1] == '!') - ob->size -= 1; - - ret = doc->md.image(ob, u_link, title, content, &doc->data); - } else { - ret = doc->md.link(ob, content, u_link, title, &doc->data); - } - - /* cleanup */ -cleanup: - doc->work_bufs[BUFFER_SPAN].size = (int)org_work_size; - return ret ? i : 0; -} - -static size_t -char_superscript(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t offset, size_t size) -{ - size_t sup_start, sup_len; - hoedown_buffer *sup; - - if (!doc->md.superscript) - return 0; - - if (size < 2) - return 0; - - if (data[1] == '(') { - sup_start = 2; - sup_len = find_emph_char(data + 2, size - 2, ')') + 2; - - if (sup_len == size) - return 0; - } else { - sup_start = sup_len = 1; - - while (sup_len < size && !_isspace(data[sup_len])) - sup_len++; - } - - if (sup_len - sup_start == 0) - return (sup_start == 2) ? 3 : 0; - - sup = newbuf(doc, BUFFER_SPAN); - parse_inline(sup, doc, data + sup_start, sup_len - sup_start); - doc->md.superscript(ob, sup, &doc->data); - popbuf(doc, BUFFER_SPAN); - - return (sup_start == 2) ? sup_len + 1 : sup_len; -} - -static size_t -char_math(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t offset, size_t size) -{ - /* double dollar */ - if (size > 1 && data[1] == '$') - return parse_math(ob, doc, data, offset, size, "$$", 2, 1); - - /* single dollar allowed only with MATH_EXPLICIT flag */ - if (doc->ext_flags & HOEDOWN_EXT_MATH_EXPLICIT) - return parse_math(ob, doc, data, offset, size, "$", 1, 0); - - return 0; -} - -/********************************* - * BLOCK-LEVEL PARSING FUNCTIONS * - *********************************/ - -/* is_empty • returns the line length when it is empty, 0 otherwise */ -static size_t -is_empty(const uint8_t *data, size_t size) -{ - size_t i; - - for (i = 0; i < size && data[i] != '\n'; i++) - if (data[i] != ' ') - return 0; - - return i + 1; -} - -/* is_hrule • returns whether a line is a horizontal rule */ -static int -is_hrule(uint8_t *data, size_t size) -{ - size_t i = 0, n = 0; - uint8_t c; - - /* skipping initial spaces */ - if (size < 3) return 0; - if (data[0] == ' ') { i++; - if (data[1] == ' ') { i++; - if (data[2] == ' ') { i++; } } } - - /* looking at the hrule uint8_t */ - if (i + 2 >= size - || (data[i] != '*' && data[i] != '-' && data[i] != '_')) - return 0; - c = data[i]; - - /* the whole line must be the char or space */ - while (i < size && data[i] != '\n') { - if (data[i] == c) n++; - else if (data[i] != ' ') - return 0; - - i++; - } - - return n >= 3; -} - -/* check if a line is a code fence; return the - * end of the code fence. if passed, width of - * the fence rule and character will be returned */ -static size_t -is_codefence(uint8_t *data, size_t size, size_t *width, uint8_t *chr) -{ - size_t i = 0, n = 1; - uint8_t c; - - /* skipping initial spaces */ - if (size < 3) - return 0; - - if (data[0] == ' ') { i++; - if (data[1] == ' ') { i++; - if (data[2] == ' ') { i++; } } } - - /* looking at the hrule uint8_t */ - c = data[i]; - if (i + 2 >= size || !(c=='~' || c=='`')) - return 0; - - /* the fence must be that same character */ - while (++i < size && data[i] == c) - ++n; - - if (n < 3) - return 0; - - if (width) *width = n; - if (chr) *chr = c; - return i; -} - -/* expects single line, checks if it's a codefence and extracts language */ -static size_t -parse_codefence(uint8_t *data, size_t size, hoedown_buffer *lang, size_t *width, uint8_t *chr) -{ - size_t i, w, lang_start; - - i = w = is_codefence(data, size, width, chr); - if (i == 0) - return 0; - - while (i < size && _isspace(data[i])) - i++; - - lang_start = i; - - while (i < size && !_isspace(data[i])) - i++; - - lang->data = data + lang_start; - lang->size = i - lang_start; - - /* Avoid parsing a codespan as a fence */ - i = lang_start + 2; - while (i < size && !(data[i] == *chr && data[i-1] == *chr && data[i-2] == *chr)) i++; - if (i < size) return 0; - - return w; -} - -/* is_atxheader • returns whether the line is a hash-prefixed header */ -static int -is_atxheader(hoedown_document *doc, uint8_t *data, size_t size) -{ - if (data[0] != '#') - return 0; - - if (doc->ext_flags & HOEDOWN_EXT_SPACE_HEADERS) { - size_t level = 0; - - while (level < size && level < 6 && data[level] == '#') - level++; - - if (level < size && data[level] != ' ') - return 0; - } - - return 1; -} - -/* is_headerline • returns whether the line is a setext-style hdr underline */ -static int -is_headerline(uint8_t *data, size_t size) -{ - size_t i = 0; - - /* test of level 1 header */ - if (data[i] == '=') { - for (i = 1; i < size && data[i] == '='; i++); - while (i < size && data[i] == ' ') i++; - return (i >= size || data[i] == '\n') ? 1 : 0; } - - /* test of level 2 header */ - if (data[i] == '-') { - for (i = 1; i < size && data[i] == '-'; i++); - while (i < size && data[i] == ' ') i++; - return (i >= size || data[i] == '\n') ? 2 : 0; } - - return 0; -} - -static int -is_next_headerline(uint8_t *data, size_t size) -{ - size_t i = 0; - - while (i < size && data[i] != '\n') - i++; - - if (++i >= size) - return 0; - - return is_headerline(data + i, size - i); -} - -/* prefix_quote • returns blockquote prefix length */ -static size_t -prefix_quote(uint8_t *data, size_t size) -{ - size_t i = 0; - if (i < size && data[i] == ' ') i++; - if (i < size && data[i] == ' ') i++; - if (i < size && data[i] == ' ') i++; - - if (i < size && data[i] == '>') { - if (i + 1 < size && data[i + 1] == ' ') - return i + 2; - - return i + 1; - } - - return 0; -} - -/* prefix_code • returns prefix length for block code*/ -static size_t -prefix_code(uint8_t *data, size_t size) -{ - if (size > 3 && data[0] == ' ' && data[1] == ' ' - && data[2] == ' ' && data[3] == ' ') return 4; - - return 0; -} - -/* prefix_oli • returns ordered list item prefix */ -static size_t -prefix_oli(uint8_t *data, size_t size) -{ - size_t i = 0; - - if (i < size && data[i] == ' ') i++; - if (i < size && data[i] == ' ') i++; - if (i < size && data[i] == ' ') i++; - - if (i >= size || data[i] < '0' || data[i] > '9') - return 0; - - while (i < size && data[i] >= '0' && data[i] <= '9') - i++; - - if (i + 1 >= size || data[i] != '.' || data[i + 1] != ' ') - return 0; - - if (is_next_headerline(data + i, size - i)) - return 0; - - return i + 2; -} - -/* prefix_uli • returns ordered list item prefix */ -static size_t -prefix_uli(uint8_t *data, size_t size) -{ - size_t i = 0; - - if (i < size && data[i] == ' ') i++; - if (i < size && data[i] == ' ') i++; - if (i < size && data[i] == ' ') i++; - - if (i + 1 >= size || - (data[i] != '*' && data[i] != '+' && data[i] != '-') || - data[i + 1] != ' ') - return 0; - - if (is_next_headerline(data + i, size - i)) - return 0; - - return i + 2; -} - - -/* parse_block • parsing of one block, returning next uint8_t to parse */ -static void parse_block(hoedown_buffer *ob, hoedown_document *doc, - uint8_t *data, size_t size); - - -/* parse_blockquote • handles parsing of a blockquote fragment */ -static size_t -parse_blockquote(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t size) -{ - size_t beg, end = 0, pre, work_size = 0; - uint8_t *work_data = 0; - hoedown_buffer *out = 0; - - out = newbuf(doc, BUFFER_BLOCK); - beg = 0; - while (beg < size) { - for (end = beg + 1; end < size && data[end - 1] != '\n'; end++); - - pre = prefix_quote(data + beg, end - beg); - - if (pre) - beg += pre; /* skipping prefix */ - - /* empty line followed by non-quote line */ - else if (is_empty(data + beg, end - beg) && - (end >= size || (prefix_quote(data + end, size - end) == 0 && - !is_empty(data + end, size - end)))) - break; - - if (beg < end) { /* copy into the in-place working buffer */ - /* hoedown_buffer_put(work, data + beg, end - beg); */ - if (!work_data) - work_data = data + beg; - else if (data + beg != work_data + work_size) - memmove(work_data + work_size, data + beg, end - beg); - work_size += end - beg; - } - beg = end; - } - - parse_block(out, doc, work_data, work_size); - if (doc->md.blockquote) - doc->md.blockquote(ob, out, &doc->data); - popbuf(doc, BUFFER_BLOCK); - return end; -} - -static size_t -parse_htmlblock(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t size, int do_render); - -/* parse_blockquote • handles parsing of a regular paragraph */ -static size_t -parse_paragraph(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t size) -{ - hoedown_buffer work = { NULL, 0, 0, 0, NULL, NULL, NULL }; - size_t i = 0, end = 0; - int level = 0; - - work.data = data; - - while (i < size) { - for (end = i + 1; end < size && data[end - 1] != '\n'; end++) /* empty */; - - if (is_empty(data + i, size - i)) - break; - - if ((level = is_headerline(data + i, size - i)) != 0) - break; - - if (is_atxheader(doc, data + i, size - i) || - is_hrule(data + i, size - i) || - prefix_quote(data + i, size - i)) { - end = i; - break; - } - - i = end; - } - - work.size = i; - while (work.size && data[work.size - 1] == '\n') - work.size--; - - if (!level) { - hoedown_buffer *tmp = newbuf(doc, BUFFER_BLOCK); - parse_inline(tmp, doc, work.data, work.size); - if (doc->md.paragraph) - doc->md.paragraph(ob, tmp, &doc->data); - popbuf(doc, BUFFER_BLOCK); - } else { - hoedown_buffer *header_work; - - if (work.size) { - size_t beg; - i = work.size; - work.size -= 1; - - while (work.size && data[work.size] != '\n') - work.size -= 1; - - beg = work.size + 1; - while (work.size && data[work.size - 1] == '\n') - work.size -= 1; - - if (work.size > 0) { - hoedown_buffer *tmp = newbuf(doc, BUFFER_BLOCK); - parse_inline(tmp, doc, work.data, work.size); - - if (doc->md.paragraph) - doc->md.paragraph(ob, tmp, &doc->data); - - popbuf(doc, BUFFER_BLOCK); - work.data += beg; - work.size = i - beg; - } - else work.size = i; - } - - header_work = newbuf(doc, BUFFER_SPAN); - parse_inline(header_work, doc, work.data, work.size); - - if (doc->md.header) - doc->md.header(ob, header_work, (int)level, &doc->data); - - popbuf(doc, BUFFER_SPAN); - } - - return end; -} - -/* parse_fencedcode • handles parsing of a block-level code fragment */ -static size_t -parse_fencedcode(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t size) -{ - hoedown_buffer text = { 0, 0, 0, 0, NULL, NULL, NULL }; - hoedown_buffer lang = { 0, 0, 0, 0, NULL, NULL, NULL }; - size_t i = 0, text_start, line_start; - size_t w, w2; - size_t width, width2; - uint8_t chr, chr2; - - /* parse codefence line */ - while (i < size && data[i] != '\n') - i++; - - w = parse_codefence(data, i, &lang, &width, &chr); - if (!w) - return 0; - - /* search for end */ - i++; - text_start = i; - while ((line_start = i) < size) { - while (i < size && data[i] != '\n') - i++; - - w2 = is_codefence(data + line_start, i - line_start, &width2, &chr2); - if (w == w2 && width == width2 && chr == chr2 && - is_empty(data + (line_start+w), i - (line_start+w))) - break; - - i++; - } - - text.data = data + text_start; - text.size = line_start - text_start; - - if (doc->md.blockcode) - doc->md.blockcode(ob, text.size ? &text : NULL, lang.size ? &lang : NULL, &doc->data); - - return i; -} - -static size_t -parse_blockcode(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t size) -{ - size_t beg, end, pre; - hoedown_buffer *work = 0; - - work = newbuf(doc, BUFFER_BLOCK); - - beg = 0; - while (beg < size) { - for (end = beg + 1; end < size && data[end - 1] != '\n'; end++) {}; - pre = prefix_code(data + beg, end - beg); - - if (pre) - beg += pre; /* skipping prefix */ - else if (!is_empty(data + beg, end - beg)) - /* non-empty non-prefixed line breaks the pre */ - break; - - if (beg < end) { - /* verbatim copy to the working buffer, - escaping entities */ - if (is_empty(data + beg, end - beg)) - hoedown_buffer_putc(work, '\n'); - else hoedown_buffer_put(work, data + beg, end - beg); - } - beg = end; - } - - while (work->size && work->data[work->size - 1] == '\n') - work->size -= 1; - - hoedown_buffer_putc(work, '\n'); - - if (doc->md.blockcode) - doc->md.blockcode(ob, work, NULL, &doc->data); - - popbuf(doc, BUFFER_BLOCK); - return beg; -} - -/* parse_listitem • parsing of a single list item */ -/* assuming initial prefix is already removed */ -static size_t -parse_listitem(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t size, hoedown_list_flags *flags) -{ - hoedown_buffer *work = 0, *inter = 0; - size_t beg = 0, end, pre, sublist = 0, orgpre = 0, i; - int in_empty = 0, has_inside_empty = 0, in_fence = 0; - - /* keeping track of the first indentation prefix */ - while (orgpre < 3 && orgpre < size && data[orgpre] == ' ') - orgpre++; - - beg = prefix_uli(data, size); - if (!beg) - beg = prefix_oli(data, size); - - if (!beg) - return 0; - - /* skipping to the beginning of the following line */ - end = beg; - while (end < size && data[end - 1] != '\n') - end++; - - /* getting working buffers */ - work = newbuf(doc, BUFFER_SPAN); - inter = newbuf(doc, BUFFER_SPAN); - - /* putting the first line into the working buffer */ - hoedown_buffer_put(work, data + beg, end - beg); - beg = end; - - /* process the following lines */ - while (beg < size) { - size_t has_next_uli = 0, has_next_oli = 0; - - end++; - - while (end < size && data[end - 1] != '\n') - end++; - - /* process an empty line */ - if (is_empty(data + beg, end - beg)) { - in_empty = 1; - beg = end; - continue; - } - - /* calculating the indentation */ - i = 0; - while (i < 4 && beg + i < end && data[beg + i] == ' ') - i++; - - pre = i; - - if (doc->ext_flags & HOEDOWN_EXT_FENCED_CODE) { - if (is_codefence(data + beg + i, end - beg - i, NULL, NULL)) - in_fence = !in_fence; - } - - /* Only check for new list items if we are **not** inside - * a fenced code block */ - if (!in_fence) { - has_next_uli = prefix_uli(data + beg + i, end - beg - i); - has_next_oli = prefix_oli(data + beg + i, end - beg - i); - } - - /* checking for a new item */ - if ((has_next_uli && !is_hrule(data + beg + i, end - beg - i)) || has_next_oli) { - if (in_empty) - has_inside_empty = 1; - - /* the following item must have the same (or less) indentation */ - if (pre <= orgpre) { - /* if the following item has different list type, we end this list */ - if (in_empty && ( - ((*flags & HOEDOWN_LIST_ORDERED) && has_next_uli) || - (!(*flags & HOEDOWN_LIST_ORDERED) && has_next_oli))) - *flags |= HOEDOWN_LI_END; - - break; - } - - if (!sublist) - sublist = work->size; - } - /* joining only indented stuff after empty lines; - * note that now we only require 1 space of indentation - * to continue a list */ - else if (in_empty && pre == 0) { - *flags |= HOEDOWN_LI_END; - break; - } - - if (in_empty) { - hoedown_buffer_putc(work, '\n'); - has_inside_empty = 1; - in_empty = 0; - } - - /* adding the line without prefix into the working buffer */ - hoedown_buffer_put(work, data + beg + i, end - beg - i); - beg = end; - } - - /* render of li contents */ - if (has_inside_empty) - *flags |= HOEDOWN_LI_BLOCK; - - if (*flags & HOEDOWN_LI_BLOCK) { - /* intermediate render of block li */ - if (sublist && sublist < work->size) { - parse_block(inter, doc, work->data, sublist); - parse_block(inter, doc, work->data + sublist, work->size - sublist); - } - else - parse_block(inter, doc, work->data, work->size); - } else { - /* intermediate render of inline li */ - if (sublist && sublist < work->size) { - parse_inline(inter, doc, work->data, sublist); - parse_block(inter, doc, work->data + sublist, work->size - sublist); - } - else - parse_inline(inter, doc, work->data, work->size); - } - - /* render of li itself */ - if (doc->md.listitem) - doc->md.listitem(ob, inter, *flags, &doc->data); - - popbuf(doc, BUFFER_SPAN); - popbuf(doc, BUFFER_SPAN); - return beg; -} - - -/* parse_list • parsing ordered or unordered list block */ -static size_t -parse_list(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t size, hoedown_list_flags flags) -{ - hoedown_buffer *work = 0; - size_t i = 0, j; - - work = newbuf(doc, BUFFER_BLOCK); - - while (i < size) { - j = parse_listitem(work, doc, data + i, size - i, &flags); - i += j; - - if (!j || (flags & HOEDOWN_LI_END)) - break; - } - - if (doc->md.list) - doc->md.list(ob, work, flags, &doc->data); - popbuf(doc, BUFFER_BLOCK); - return i; -} - -/* parse_atxheader • parsing of atx-style headers */ -static size_t -parse_atxheader(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t size) -{ - size_t level = 0; - size_t i, end, skip; - - while (level < size && level < 6 && data[level] == '#') - level++; - - for (i = level; i < size && data[i] == ' '; i++); - - for (end = i; end < size && data[end] != '\n'; end++); - skip = end; - - while (end && data[end - 1] == '#') - end--; - - while (end && data[end - 1] == ' ') - end--; - - if (end > i) { - hoedown_buffer *work = newbuf(doc, BUFFER_SPAN); - - parse_inline(work, doc, data + i, end - i); - - if (doc->md.header) - doc->md.header(ob, work, (int)level, &doc->data); - - popbuf(doc, BUFFER_SPAN); - } - - return skip; -} - -/* parse_footnote_def • parse a single footnote definition */ -static void -parse_footnote_def(hoedown_buffer *ob, hoedown_document *doc, unsigned int num, uint8_t *data, size_t size) -{ - hoedown_buffer *work = 0; - work = newbuf(doc, BUFFER_SPAN); - - parse_block(work, doc, data, size); - - if (doc->md.footnote_def) - doc->md.footnote_def(ob, work, num, &doc->data); - popbuf(doc, BUFFER_SPAN); -} - -/* parse_footnote_list • render the contents of the footnotes */ -static void -parse_footnote_list(hoedown_buffer *ob, hoedown_document *doc, struct footnote_list *footnotes) -{ - hoedown_buffer *work = 0; - struct footnote_item *item; - struct footnote_ref *ref; - - if (footnotes->count == 0) - return; - - work = newbuf(doc, BUFFER_BLOCK); - - item = footnotes->head; - while (item) { - ref = item->ref; - parse_footnote_def(work, doc, ref->num, ref->contents->data, ref->contents->size); - item = item->next; - } - - if (doc->md.footnotes) - doc->md.footnotes(ob, work, &doc->data); - popbuf(doc, BUFFER_BLOCK); -} - -/* htmlblock_is_end • check for end of HTML block : ( *)\n */ -/* returns tag length on match, 0 otherwise */ -/* assumes data starts with "<" */ -static size_t -htmlblock_is_end( - const char *tag, - size_t tag_len, - hoedown_document *doc, - uint8_t *data, - size_t size) -{ - size_t i = tag_len + 3, w; - - /* try to match the end tag */ - /* note: we're not considering tags like "" which are still valid */ - if (i > size || - data[1] != '/' || - strncasecmp((char *)data + 2, tag, tag_len) != 0 || - data[tag_len + 2] != '>') - return 0; - - /* rest of the line must be empty */ - if ((w = is_empty(data + i, size - i)) == 0 && i < size) - return 0; - - return i + w; -} - -/* htmlblock_find_end • try to find HTML block ending tag */ -/* returns the length on match, 0 otherwise */ -static size_t -htmlblock_find_end( - const char *tag, - size_t tag_len, - hoedown_document *doc, - uint8_t *data, - size_t size) -{ - size_t i = 0, w; - - while (1) { - while (i < size && data[i] != '<') i++; - if (i >= size) return 0; - - w = htmlblock_is_end(tag, tag_len, doc, data + i, size - i); - if (w) return i + w; - i++; - } -} - -/* htmlblock_find_end_strict • try to find end of HTML block in strict mode */ -/* (it must be an unindented line, and have a blank line afterwads) */ -/* returns the length on match, 0 otherwise */ -static size_t -htmlblock_find_end_strict( - const char *tag, - size_t tag_len, - hoedown_document *doc, - uint8_t *data, - size_t size) -{ - size_t i = 0, mark; - - while (1) { - mark = i; - while (i < size && data[i] != '\n') i++; - if (i < size) i++; - if (i == mark) return 0; - - if (data[mark] == ' ' && mark > 0) continue; - mark += htmlblock_find_end(tag, tag_len, doc, data + mark, i - mark); - if (mark == i && (is_empty(data + i, size - i) || i >= size)) break; - } - - return i; -} - -/* parse_htmlblock • parsing of inline HTML block */ -static size_t -parse_htmlblock(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t size, int do_render) -{ - hoedown_buffer work = { NULL, 0, 0, 0, NULL, NULL, NULL }; - size_t i, j = 0, tag_len, tag_end; - const char *curtag = NULL; - - work.data = data; - - /* identification of the opening tag */ - if (size < 2 || data[0] != '<') - return 0; - - i = 1; - while (i < size && data[i] != '>' && data[i] != ' ') - i++; - - if (i < size) - curtag = hoedown_find_block_tag((char *)data + 1, (int)i - 1); - - /* handling of special cases */ - if (!curtag) { - - /* HTML comment, laxist form */ - if (size > 5 && data[1] == '!' && data[2] == '-' && data[3] == '-') { - i = 5; - - while (i < size && !(data[i - 2] == '-' && data[i - 1] == '-' && data[i] == '>')) - i++; - - i++; - - if (i < size) - j = is_empty(data + i, size - i); - - if (j) { - work.size = i + j; - if (do_render && doc->md.blockhtml) - doc->md.blockhtml(ob, &work, &doc->data); - return work.size; - } - } - - /* HR, which is the only self-closing block tag considered */ - if (size > 4 && (data[1] == 'h' || data[1] == 'H') && (data[2] == 'r' || data[2] == 'R')) { - i = 3; - while (i < size && data[i] != '>') - i++; - - if (i + 1 < size) { - i++; - j = is_empty(data + i, size - i); - if (j) { - work.size = i + j; - if (do_render && doc->md.blockhtml) - doc->md.blockhtml(ob, &work, &doc->data); - return work.size; - } - } - } - - /* no special case recognised */ - return 0; - } - - /* looking for a matching closing tag in strict mode */ - tag_len = strlen(curtag); - tag_end = htmlblock_find_end_strict(curtag, tag_len, doc, data, size); - - /* if not found, trying a second pass looking for indented match */ - /* but not if tag is "ins" or "del" (following original Markdown.pl) */ - if (!tag_end && strcmp(curtag, "ins") != 0 && strcmp(curtag, "del") != 0) - tag_end = htmlblock_find_end(curtag, tag_len, doc, data, size); - - if (!tag_end) - return 0; - - /* the end of the block has been found */ - work.size = tag_end; - if (do_render && doc->md.blockhtml) - doc->md.blockhtml(ob, &work, &doc->data); - - return tag_end; -} - -static void -parse_table_row( - hoedown_buffer *ob, - hoedown_document *doc, - uint8_t *data, - size_t size, - size_t columns, - hoedown_table_flags *col_data, - hoedown_table_flags header_flag) -{ - size_t i = 0, col, len; - hoedown_buffer *row_work = 0; - - if (!doc->md.table_cell || !doc->md.table_row) - return; - - row_work = newbuf(doc, BUFFER_SPAN); - - if (i < size && data[i] == '|') - i++; - - for (col = 0; col < columns && i < size; ++col) { - size_t cell_start, cell_end; - hoedown_buffer *cell_work; - - cell_work = newbuf(doc, BUFFER_SPAN); - - while (i < size && _isspace(data[i])) - i++; - - cell_start = i; - - len = find_emph_char(data + i, size - i, '|'); - i += len ? len : size - i; - - cell_end = i - 1; - - while (cell_end > cell_start && _isspace(data[cell_end])) - cell_end--; - - parse_inline(cell_work, doc, data + cell_start, 1 + cell_end - cell_start); - doc->md.table_cell(row_work, cell_work, col_data[col] | header_flag, &doc->data); - - popbuf(doc, BUFFER_SPAN); - i++; - } - - for (; col < columns; ++col) { - hoedown_buffer empty_cell = { 0, 0, 0, 0, NULL, NULL, NULL }; - doc->md.table_cell(row_work, &empty_cell, col_data[col] | header_flag, &doc->data); - } - - doc->md.table_row(ob, row_work, &doc->data); - - popbuf(doc, BUFFER_SPAN); -} - -static size_t -parse_table_header( - hoedown_buffer *ob, - hoedown_document *doc, - uint8_t *data, - size_t size, - size_t *columns, - hoedown_table_flags **column_data) -{ - int pipes; - size_t i = 0, col, header_end, under_end; - - pipes = 0; - while (i < size && data[i] != '\n') - if (data[i++] == '|') - pipes++; - - if (i == size || pipes == 0) - return 0; - - header_end = i; - - while (header_end > 0 && _isspace(data[header_end - 1])) - header_end--; - - if (data[0] == '|') - pipes--; - - if (header_end && data[header_end - 1] == '|') - pipes--; - - if (pipes < 0) - return 0; - - *columns = pipes + 1; - *column_data = hoedown_calloc(*columns, sizeof(hoedown_table_flags)); - - /* Parse the header underline */ - i++; - if (i < size && data[i] == '|') - i++; - - under_end = i; - while (under_end < size && data[under_end] != '\n') - under_end++; - - for (col = 0; col < *columns && i < under_end; ++col) { - size_t dashes = 0; - - while (i < under_end && data[i] == ' ') - i++; - - if (data[i] == ':') { - i++; (*column_data)[col] |= HOEDOWN_TABLE_ALIGN_LEFT; - dashes++; - } - - while (i < under_end && data[i] == '-') { - i++; dashes++; - } - - if (i < under_end && data[i] == ':') { - i++; (*column_data)[col] |= HOEDOWN_TABLE_ALIGN_RIGHT; - dashes++; - } - - while (i < under_end && data[i] == ' ') - i++; - - if (i < under_end && data[i] != '|' && data[i] != '+') - break; - - if (dashes < 3) - break; - - i++; - } - - if (col < *columns) - return 0; - - parse_table_row( - ob, doc, data, - header_end, - *columns, - *column_data, - HOEDOWN_TABLE_HEADER - ); - - return under_end + 1; -} - -static size_t -parse_table( - hoedown_buffer *ob, - hoedown_document *doc, - uint8_t *data, - size_t size) -{ - size_t i; - - hoedown_buffer *work = 0; - hoedown_buffer *header_work = 0; - hoedown_buffer *body_work = 0; - - size_t columns; - hoedown_table_flags *col_data = NULL; - - work = newbuf(doc, BUFFER_BLOCK); - header_work = newbuf(doc, BUFFER_SPAN); - body_work = newbuf(doc, BUFFER_BLOCK); - - i = parse_table_header(header_work, doc, data, size, &columns, &col_data); - if (i > 0) { - - while (i < size) { - size_t row_start; - int pipes = 0; - - row_start = i; - - while (i < size && data[i] != '\n') - if (data[i++] == '|') - pipes++; - - if (pipes == 0 || i == size) { - i = row_start; - break; - } - - parse_table_row( - body_work, - doc, - data + row_start, - i - row_start, - columns, - col_data, 0 - ); - - i++; - } - - if (doc->md.table_header) - doc->md.table_header(work, header_work, &doc->data); - - if (doc->md.table_body) - doc->md.table_body(work, body_work, &doc->data); - - if (doc->md.table) - doc->md.table(ob, work, &doc->data); - } - - free(col_data); - popbuf(doc, BUFFER_SPAN); - popbuf(doc, BUFFER_BLOCK); - popbuf(doc, BUFFER_BLOCK); - return i; -} - -/* parse_block • parsing of one block, returning next uint8_t to parse */ -static void -parse_block(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t size) -{ - size_t beg, end, i; - uint8_t *txt_data; - beg = 0; - - if (doc->work_bufs[BUFFER_SPAN].size + - doc->work_bufs[BUFFER_BLOCK].size > doc->max_nesting) - return; - - while (beg < size) { - txt_data = data + beg; - end = size - beg; - - if (is_atxheader(doc, txt_data, end)) - beg += parse_atxheader(ob, doc, txt_data, end); - - else if (data[beg] == '<' && doc->md.blockhtml && - (i = parse_htmlblock(ob, doc, txt_data, end, 1)) != 0) - beg += i; - - else if ((i = is_empty(txt_data, end)) != 0) - beg += i; - - else if (is_hrule(txt_data, end)) { - if (doc->md.hrule) - doc->md.hrule(ob, &doc->data); - - while (beg < size && data[beg] != '\n') - beg++; - - beg++; - } - - else if ((doc->ext_flags & HOEDOWN_EXT_FENCED_CODE) != 0 && - (i = parse_fencedcode(ob, doc, txt_data, end)) != 0) - beg += i; - - else if ((doc->ext_flags & HOEDOWN_EXT_TABLES) != 0 && - (i = parse_table(ob, doc, txt_data, end)) != 0) - beg += i; - - else if (prefix_quote(txt_data, end)) - beg += parse_blockquote(ob, doc, txt_data, end); - - else if (!(doc->ext_flags & HOEDOWN_EXT_DISABLE_INDENTED_CODE) && prefix_code(txt_data, end)) - beg += parse_blockcode(ob, doc, txt_data, end); - - else if (prefix_uli(txt_data, end)) - beg += parse_list(ob, doc, txt_data, end, 0); - - else if (prefix_oli(txt_data, end)) - beg += parse_list(ob, doc, txt_data, end, HOEDOWN_LIST_ORDERED); - - else - beg += parse_paragraph(ob, doc, txt_data, end); - } -} - - - -/********************* - * REFERENCE PARSING * - *********************/ - -/* is_footnote • returns whether a line is a footnote definition or not */ -static int -is_footnote(const uint8_t *data, size_t beg, size_t end, size_t *last, struct footnote_list *list) -{ - size_t i = 0; - hoedown_buffer *contents = 0; - size_t ind = 0; - int in_empty = 0; - size_t start = 0; - - size_t id_offset, id_end; - - /* up to 3 optional leading spaces */ - if (beg + 3 >= end) return 0; - if (data[beg] == ' ') { i = 1; - if (data[beg + 1] == ' ') { i = 2; - if (data[beg + 2] == ' ') { i = 3; - if (data[beg + 3] == ' ') return 0; } } } - i += beg; - - /* id part: caret followed by anything between brackets */ - if (data[i] != '[') return 0; - i++; - if (i >= end || data[i] != '^') return 0; - i++; - id_offset = i; - while (i < end && data[i] != '\n' && data[i] != '\r' && data[i] != ']') - i++; - if (i >= end || data[i] != ']') return 0; - id_end = i; - - /* spacer: colon (space | tab)* newline? (space | tab)* */ - i++; - if (i >= end || data[i] != ':') return 0; - i++; - - /* getting content buffer */ - contents = hoedown_buffer_new(64); - - start = i; - - /* process lines similar to a list item */ - while (i < end) { - while (i < end && data[i] != '\n' && data[i] != '\r') i++; - - /* process an empty line */ - if (is_empty(data + start, i - start)) { - in_empty = 1; - if (i < end && (data[i] == '\n' || data[i] == '\r')) { - i++; - if (i < end && data[i] == '\n' && data[i - 1] == '\r') i++; - } - start = i; - continue; - } - - /* calculating the indentation */ - ind = 0; - while (ind < 4 && start + ind < end && data[start + ind] == ' ') - ind++; - - /* joining only indented stuff after empty lines; - * note that now we only require 1 space of indentation - * to continue, just like lists */ - if (ind == 0) { - if (start == id_end + 2 && data[start] == '\t') {} - else break; - } - else if (in_empty) { - hoedown_buffer_putc(contents, '\n'); - } - - in_empty = 0; - - /* adding the line into the content buffer */ - hoedown_buffer_put(contents, data + start + ind, i - start - ind); - /* add carriage return */ - if (i < end) { - hoedown_buffer_putc(contents, '\n'); - if (i < end && (data[i] == '\n' || data[i] == '\r')) { - i++; - if (i < end && data[i] == '\n' && data[i - 1] == '\r') i++; - } - } - start = i; - } - - if (last) - *last = start; - - if (list) { - struct footnote_ref *ref; - ref = create_footnote_ref(list, data + id_offset, id_end - id_offset); - if (!ref) - return 0; - if (!add_footnote_ref(list, ref)) { - free_footnote_ref(ref); - return 0; - } - ref->contents = contents; - } - - return 1; -} - -/* is_ref • returns whether a line is a reference or not */ -static int -is_ref(const uint8_t *data, size_t beg, size_t end, size_t *last, struct link_ref **refs) -{ -/* int n; */ - size_t i = 0; - size_t id_offset, id_end; - size_t link_offset, link_end; - size_t title_offset, title_end; - size_t line_end; - - /* up to 3 optional leading spaces */ - if (beg + 3 >= end) return 0; - if (data[beg] == ' ') { i = 1; - if (data[beg + 1] == ' ') { i = 2; - if (data[beg + 2] == ' ') { i = 3; - if (data[beg + 3] == ' ') return 0; } } } - i += beg; - - /* id part: anything but a newline between brackets */ - if (data[i] != '[') return 0; - i++; - id_offset = i; - while (i < end && data[i] != '\n' && data[i] != '\r' && data[i] != ']') - i++; - if (i >= end || data[i] != ']') return 0; - id_end = i; - - /* spacer: colon (space | tab)* newline? (space | tab)* */ - i++; - if (i >= end || data[i] != ':') return 0; - i++; - while (i < end && data[i] == ' ') i++; - if (i < end && (data[i] == '\n' || data[i] == '\r')) { - i++; - if (i < end && data[i] == '\r' && data[i - 1] == '\n') i++; } - while (i < end && data[i] == ' ') i++; - if (i >= end) return 0; - - /* link: spacing-free sequence, optionally between angle brackets */ - if (data[i] == '<') - i++; - - link_offset = i; - - while (i < end && data[i] != ' ' && data[i] != '\n' && data[i] != '\r') - i++; - - if (data[i - 1] == '>') link_end = i - 1; - else link_end = i; - - /* optional spacer: (space | tab)* (newline | '\'' | '"' | '(' ) */ - while (i < end && data[i] == ' ') i++; - if (i < end && data[i] != '\n' && data[i] != '\r' - && data[i] != '\'' && data[i] != '"' && data[i] != '(') - return 0; - line_end = 0; - /* computing end-of-line */ - if (i >= end || data[i] == '\r' || data[i] == '\n') line_end = i; - if (i + 1 < end && data[i] == '\n' && data[i + 1] == '\r') - line_end = i + 1; - - /* optional (space|tab)* spacer after a newline */ - if (line_end) { - i = line_end + 1; - while (i < end && data[i] == ' ') i++; } - - /* optional title: any non-newline sequence enclosed in '"() - alone on its line */ - title_offset = title_end = 0; - if (i + 1 < end - && (data[i] == '\'' || data[i] == '"' || data[i] == '(')) { - i++; - title_offset = i; - /* looking for EOL */ - while (i < end && data[i] != '\n' && data[i] != '\r') i++; - if (i + 1 < end && data[i] == '\n' && data[i + 1] == '\r') - title_end = i + 1; - else title_end = i; - /* stepping back */ - i -= 1; - while (i > title_offset && data[i] == ' ') - i -= 1; - if (i > title_offset - && (data[i] == '\'' || data[i] == '"' || data[i] == ')')) { - line_end = title_end; - title_end = i; } } - - if (!line_end || link_end == link_offset) - return 0; /* garbage after the link empty link */ - - /* a valid ref has been found, filling-in return structures */ - if (last) - *last = line_end; - - if (refs) { - struct link_ref *ref; - - ref = add_link_ref(refs, data + id_offset, id_end - id_offset); - if (!ref) - return 0; - - ref->link = hoedown_buffer_new(link_end - link_offset); - hoedown_buffer_put(ref->link, data + link_offset, link_end - link_offset); - - if (title_end > title_offset) { - ref->title = hoedown_buffer_new(title_end - title_offset); - hoedown_buffer_put(ref->title, data + title_offset, title_end - title_offset); - } - } - - return 1; -} - -static void expand_tabs(hoedown_buffer *ob, const uint8_t *line, size_t size) -{ - /* This code makes two assumptions: - * - Input is valid UTF-8. (Any byte with top two bits 10 is skipped, - * whether or not it is a valid UTF-8 continuation byte.) - * - Input contains no combining characters. (Combining characters - * should be skipped but are not.) - */ - size_t i = 0, tab = 0; - - while (i < size) { - size_t org = i; - - while (i < size && line[i] != '\t') { - /* ignore UTF-8 continuation bytes */ - if ((line[i] & 0xc0) != 0x80) - tab++; - i++; - } - - if (i > org) - hoedown_buffer_put(ob, line + org, i - org); - - if (i >= size) - break; - - do { - hoedown_buffer_putc(ob, ' '); tab++; - } while (tab % 4); - - i++; - } -} - -/********************** - * EXPORTED FUNCTIONS * - **********************/ - -hoedown_document * -hoedown_document_new( - const hoedown_renderer *renderer, - hoedown_extensions extensions, - size_t max_nesting) -{ - hoedown_document *doc = NULL; - - assert(max_nesting > 0 && renderer); - - doc = hoedown_malloc(sizeof(hoedown_document)); - memcpy(&doc->md, renderer, sizeof(hoedown_renderer)); - - doc->data.opaque = renderer->opaque; - - hoedown_stack_init(&doc->work_bufs[BUFFER_BLOCK], 4); - hoedown_stack_init(&doc->work_bufs[BUFFER_SPAN], 8); - - memset(doc->active_char, 0x0, 256); - - if (extensions & HOEDOWN_EXT_UNDERLINE && doc->md.underline) { - doc->active_char['_'] = MD_CHAR_EMPHASIS; - } - - if (doc->md.emphasis || doc->md.double_emphasis || doc->md.triple_emphasis) { - doc->active_char['*'] = MD_CHAR_EMPHASIS; - doc->active_char['_'] = MD_CHAR_EMPHASIS; - if (extensions & HOEDOWN_EXT_STRIKETHROUGH) - doc->active_char['~'] = MD_CHAR_EMPHASIS; - if (extensions & HOEDOWN_EXT_HIGHLIGHT) - doc->active_char['='] = MD_CHAR_EMPHASIS; - } - - if (doc->md.codespan) - doc->active_char['`'] = MD_CHAR_CODESPAN; - - if (doc->md.linebreak) - doc->active_char['\n'] = MD_CHAR_LINEBREAK; - - if (doc->md.image || doc->md.link || doc->md.footnotes || doc->md.footnote_ref) - doc->active_char['['] = MD_CHAR_LINK; - - doc->active_char['<'] = MD_CHAR_LANGLE; - doc->active_char['\\'] = MD_CHAR_ESCAPE; - doc->active_char['&'] = MD_CHAR_ENTITY; - - if (extensions & HOEDOWN_EXT_AUTOLINK) { - doc->active_char[':'] = MD_CHAR_AUTOLINK_URL; - doc->active_char['@'] = MD_CHAR_AUTOLINK_EMAIL; - doc->active_char['w'] = MD_CHAR_AUTOLINK_WWW; - } - - if (extensions & HOEDOWN_EXT_SUPERSCRIPT) - doc->active_char['^'] = MD_CHAR_SUPERSCRIPT; - - if (extensions & HOEDOWN_EXT_QUOTE) - doc->active_char['"'] = MD_CHAR_QUOTE; - - if (extensions & HOEDOWN_EXT_MATH) - doc->active_char['$'] = MD_CHAR_MATH; - - /* Extension data */ - doc->ext_flags = extensions; - doc->max_nesting = max_nesting; - doc->in_link_body = 0; - - return doc; -} - -void -hoedown_document_render(hoedown_document *doc, hoedown_buffer *ob, const uint8_t *data, size_t size) -{ - static const uint8_t UTF8_BOM[] = {0xEF, 0xBB, 0xBF}; - - hoedown_buffer *text; - size_t beg, end; - - int footnotes_enabled; - - text = hoedown_buffer_new(64); - - /* Preallocate enough space for our buffer to avoid expanding while copying */ - hoedown_buffer_grow(text, size); - - /* reset the references table */ - memset(&doc->refs, 0x0, REF_TABLE_SIZE * sizeof(void *)); - - footnotes_enabled = doc->ext_flags & HOEDOWN_EXT_FOOTNOTES; - - /* reset the footnotes lists */ - if (footnotes_enabled) { - memset(&doc->footnotes_found, 0x0, sizeof(doc->footnotes_found)); - memset(&doc->footnotes_used, 0x0, sizeof(doc->footnotes_used)); - } - - /* first pass: looking for references, copying everything else */ - beg = 0; - - /* Skip a possible UTF-8 BOM, even though the Unicode standard - * discourages having these in UTF-8 documents */ - if (size >= 3 && memcmp(data, UTF8_BOM, 3) == 0) - beg += 3; - - while (beg < size) /* iterating over lines */ - if (footnotes_enabled && is_footnote(data, beg, size, &end, &doc->footnotes_found)) - beg = end; - else if (is_ref(data, beg, size, &end, doc->refs)) - beg = end; - else { /* skipping to the next line */ - end = beg; - while (end < size && data[end] != '\n' && data[end] != '\r') - end++; - - /* adding the line body if present */ - if (end > beg) - expand_tabs(text, data + beg, end - beg); - - while (end < size && (data[end] == '\n' || data[end] == '\r')) { - /* add one \n per newline */ - if (data[end] == '\n' || (end + 1 < size && data[end + 1] != '\n')) - hoedown_buffer_putc(text, '\n'); - end++; - } - - beg = end; - } - - /* pre-grow the output buffer to minimize allocations */ - hoedown_buffer_grow(ob, text->size + (text->size >> 1)); - - /* second pass: actual rendering */ - if (doc->md.doc_header) - doc->md.doc_header(ob, 0, &doc->data); - - if (text->size) { - /* adding a final newline if not already present */ - if (text->data[text->size - 1] != '\n' && text->data[text->size - 1] != '\r') - hoedown_buffer_putc(text, '\n'); - - parse_block(ob, doc, text->data, text->size); - } - - /* footnotes */ - if (footnotes_enabled) - parse_footnote_list(ob, doc, &doc->footnotes_used); - - if (doc->md.doc_footer) - doc->md.doc_footer(ob, 0, &doc->data); - - /* clean-up */ - hoedown_buffer_free(text); - free_link_refs(doc->refs); - if (footnotes_enabled) { - free_footnote_list(&doc->footnotes_found, 1); - free_footnote_list(&doc->footnotes_used, 0); - } - - assert(doc->work_bufs[BUFFER_SPAN].size == 0); - assert(doc->work_bufs[BUFFER_BLOCK].size == 0); -} - -void -hoedown_document_render_inline(hoedown_document *doc, hoedown_buffer *ob, const uint8_t *data, size_t size) -{ - size_t i = 0, mark; - hoedown_buffer *text = hoedown_buffer_new(64); - - /* reset the references table */ - memset(&doc->refs, 0x0, REF_TABLE_SIZE * sizeof(void *)); - - /* first pass: expand tabs and process newlines */ - hoedown_buffer_grow(text, size); - while (1) { - mark = i; - while (i < size && data[i] != '\n' && data[i] != '\r') - i++; - - expand_tabs(text, data + mark, i - mark); - - if (i >= size) - break; - - while (i < size && (data[i] == '\n' || data[i] == '\r')) { - /* add one \n per newline */ - if (data[i] == '\n' || (i + 1 < size && data[i + 1] != '\n')) - hoedown_buffer_putc(text, '\n'); - i++; - } - } - - /* second pass: actual rendering */ - hoedown_buffer_grow(ob, text->size + (text->size >> 1)); - - if (doc->md.doc_header) - doc->md.doc_header(ob, 1, &doc->data); - - parse_inline(ob, doc, text->data, text->size); - - if (doc->md.doc_footer) - doc->md.doc_footer(ob, 1, &doc->data); - - /* clean-up */ - hoedown_buffer_free(text); - - assert(doc->work_bufs[BUFFER_SPAN].size == 0); - assert(doc->work_bufs[BUFFER_BLOCK].size == 0); -} - -void -hoedown_document_free(hoedown_document *doc) -{ - size_t i; - - for (i = 0; i < (size_t)doc->work_bufs[BUFFER_SPAN].asize; ++i) - hoedown_buffer_free(doc->work_bufs[BUFFER_SPAN].item[i]); - - for (i = 0; i < (size_t)doc->work_bufs[BUFFER_BLOCK].asize; ++i) - hoedown_buffer_free(doc->work_bufs[BUFFER_BLOCK].item[i]); - - hoedown_stack_uninit(&doc->work_bufs[BUFFER_SPAN]); - hoedown_stack_uninit(&doc->work_bufs[BUFFER_BLOCK]); - - free(doc); -} diff --git a/libraries/hoedown/src/escape.c b/libraries/hoedown/src/escape.c deleted file mode 100644 index ce25dd54..00000000 --- a/libraries/hoedown/src/escape.c +++ /dev/null @@ -1,188 +0,0 @@ -#include "hoedown/escape.h" - -#include -#include -#include - - -#define likely(x) __builtin_expect((x),1) -#define unlikely(x) __builtin_expect((x),0) - - -/* - * The following characters will not be escaped: - * - * -_.+!*'(),%#@?=;:/,+&$ alphanum - * - * Note that this character set is the addition of: - * - * - The characters which are safe to be in an URL - * - The characters which are *not* safe to be in - * an URL because they are RESERVED characters. - * - * We assume (lazily) that any RESERVED char that - * appears inside an URL is actually meant to - * have its native function (i.e. as an URL - * component/separator) and hence needs no escaping. - * - * There are two exceptions: the chacters & (amp) - * and ' (single quote) do not appear in the table. - * They are meant to appear in the URL as components, - * yet they require special HTML-entity escaping - * to generate valid HTML markup. - * - * All other characters will be escaped to %XX. - * - */ -static const uint8_t HREF_SAFE[UINT8_MAX+1] = { - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 1, 0, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, - 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -}; - -void -hoedown_escape_href(hoedown_buffer *ob, const uint8_t *data, size_t size) -{ - static const char hex_chars[] = "0123456789ABCDEF"; - size_t i = 0, mark; - char hex_str[3]; - - hex_str[0] = '%'; - - while (i < size) { - mark = i; - while (i < size && HREF_SAFE[data[i]]) i++; - - /* Optimization for cases where there's nothing to escape */ - if (mark == 0 && i >= size) { - hoedown_buffer_put(ob, data, size); - return; - } - - if (likely(i > mark)) { - hoedown_buffer_put(ob, data + mark, i - mark); - } - - /* escaping */ - if (i >= size) - break; - - switch (data[i]) { - /* amp appears all the time in URLs, but needs - * HTML-entity escaping to be inside an href */ - case '&': - HOEDOWN_BUFPUTSL(ob, "&"); - break; - - /* the single quote is a valid URL character - * according to the standard; it needs HTML - * entity escaping too */ - case '\'': - HOEDOWN_BUFPUTSL(ob, "'"); - break; - - /* the space can be escaped to %20 or a plus - * sign. we're going with the generic escape - * for now. the plus thing is more commonly seen - * when building GET strings */ -#if 0 - case ' ': - hoedown_buffer_putc(ob, '+'); - break; -#endif - - /* every other character goes with a %XX escaping */ - default: - hex_str[1] = hex_chars[(data[i] >> 4) & 0xF]; - hex_str[2] = hex_chars[data[i] & 0xF]; - hoedown_buffer_put(ob, (uint8_t *)hex_str, 3); - } - - i++; - } -} - - -/** - * According to the OWASP rules: - * - * & --> & - * < --> < - * > --> > - * " --> " - * ' --> ' ' is not recommended - * / --> / forward slash is included as it helps end an HTML entity - * - */ -static const uint8_t HTML_ESCAPE_TABLE[UINT8_MAX+1] = { - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 1, 0, 0, 0, 2, 3, 0, 0, 0, 0, 0, 0, 0, 4, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 6, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -}; - -static const char *HTML_ESCAPES[] = { - "", - """, - "&", - "'", - "/", - "<", - ">" -}; - -void -hoedown_escape_html(hoedown_buffer *ob, const uint8_t *data, size_t size, int secure) -{ - size_t i = 0, mark; - - while (1) { - mark = i; - while (i < size && HTML_ESCAPE_TABLE[data[i]] == 0) i++; - - /* Optimization for cases where there's nothing to escape */ - if (mark == 0 && i >= size) { - hoedown_buffer_put(ob, data, size); - return; - } - - if (likely(i > mark)) - hoedown_buffer_put(ob, data + mark, i - mark); - - if (i >= size) break; - - /* The forward slash is only escaped in secure mode */ - if (!secure && data[i] == '/') { - hoedown_buffer_putc(ob, '/'); - } else { - hoedown_buffer_puts(ob, HTML_ESCAPES[HTML_ESCAPE_TABLE[data[i]]]); - } - - i++; - } -} diff --git a/libraries/hoedown/src/html.c b/libraries/hoedown/src/html.c deleted file mode 100644 index 8bf3358e..00000000 --- a/libraries/hoedown/src/html.c +++ /dev/null @@ -1,754 +0,0 @@ -#include "hoedown/html.h" - -#include -#include -#include -#include - -#include "hoedown/escape.h" - -#define USE_XHTML(opt) (opt->flags & HOEDOWN_HTML_USE_XHTML) - -hoedown_html_tag -hoedown_html_is_tag(const uint8_t *data, size_t size, const char *tagname) -{ - size_t i; - int closed = 0; - - if (size < 3 || data[0] != '<') - return HOEDOWN_HTML_TAG_NONE; - - i = 1; - - if (data[i] == '/') { - closed = 1; - i++; - } - - for (; i < size; ++i, ++tagname) { - if (*tagname == 0) - break; - - if (data[i] != *tagname) - return HOEDOWN_HTML_TAG_NONE; - } - - if (i == size) - return HOEDOWN_HTML_TAG_NONE; - - if (isspace(data[i]) || data[i] == '>') - return closed ? HOEDOWN_HTML_TAG_CLOSE : HOEDOWN_HTML_TAG_OPEN; - - return HOEDOWN_HTML_TAG_NONE; -} - -static void escape_html(hoedown_buffer *ob, const uint8_t *source, size_t length) -{ - hoedown_escape_html(ob, source, length, 0); -} - -static void escape_href(hoedown_buffer *ob, const uint8_t *source, size_t length) -{ - hoedown_escape_href(ob, source, length); -} - -/******************** - * GENERIC RENDERER * - ********************/ -static int -rndr_autolink(hoedown_buffer *ob, const hoedown_buffer *link, hoedown_autolink_type type, const hoedown_renderer_data *data) -{ - hoedown_html_renderer_state *state = data->opaque; - - if (!link || !link->size) - return 0; - - HOEDOWN_BUFPUTSL(ob, "data, link->size); - - if (state->link_attributes) { - hoedown_buffer_putc(ob, '\"'); - state->link_attributes(ob, link, data); - hoedown_buffer_putc(ob, '>'); - } else { - HOEDOWN_BUFPUTSL(ob, "\">"); - } - - /* - * Pretty printing: if we get an email address as - * an actual URI, e.g. `mailto:foo@bar.com`, we don't - * want to print the `mailto:` prefix - */ - if (hoedown_buffer_prefix(link, "mailto:") == 0) { - escape_html(ob, link->data + 7, link->size - 7); - } else { - escape_html(ob, link->data, link->size); - } - - HOEDOWN_BUFPUTSL(ob, ""); - - return 1; -} - -static void -rndr_blockcode(hoedown_buffer *ob, const hoedown_buffer *text, const hoedown_buffer *lang, const hoedown_renderer_data *data) -{ - if (ob->size) hoedown_buffer_putc(ob, '\n'); - - if (lang) { - HOEDOWN_BUFPUTSL(ob, "
    data, lang->size);
    -        HOEDOWN_BUFPUTSL(ob, "\">");
    -    } else {
    -        HOEDOWN_BUFPUTSL(ob, "
    ");
    -    }
    -
    -    if (text)
    -        escape_html(ob, text->data, text->size);
    -
    -    HOEDOWN_BUFPUTSL(ob, "
    \n"); -} - -static void -rndr_blockquote(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data) -{ - if (ob->size) hoedown_buffer_putc(ob, '\n'); - HOEDOWN_BUFPUTSL(ob, "
    \n"); - if (content) hoedown_buffer_put(ob, content->data, content->size); - HOEDOWN_BUFPUTSL(ob, "
    \n"); -} - -static int -rndr_codespan(hoedown_buffer *ob, const hoedown_buffer *text, const hoedown_renderer_data *data) -{ - HOEDOWN_BUFPUTSL(ob, ""); - if (text) escape_html(ob, text->data, text->size); - HOEDOWN_BUFPUTSL(ob, ""); - return 1; -} - -static int -rndr_strikethrough(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data) -{ - if (!content || !content->size) - return 0; - - HOEDOWN_BUFPUTSL(ob, ""); - hoedown_buffer_put(ob, content->data, content->size); - HOEDOWN_BUFPUTSL(ob, ""); - return 1; -} - -static int -rndr_double_emphasis(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data) -{ - if (!content || !content->size) - return 0; - - HOEDOWN_BUFPUTSL(ob, ""); - hoedown_buffer_put(ob, content->data, content->size); - HOEDOWN_BUFPUTSL(ob, ""); - - return 1; -} - -static int -rndr_emphasis(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data) -{ - if (!content || !content->size) return 0; - HOEDOWN_BUFPUTSL(ob, ""); - if (content) hoedown_buffer_put(ob, content->data, content->size); - HOEDOWN_BUFPUTSL(ob, ""); - return 1; -} - -static int -rndr_underline(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data) -{ - if (!content || !content->size) - return 0; - - HOEDOWN_BUFPUTSL(ob, ""); - hoedown_buffer_put(ob, content->data, content->size); - HOEDOWN_BUFPUTSL(ob, ""); - - return 1; -} - -static int -rndr_highlight(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data) -{ - if (!content || !content->size) - return 0; - - HOEDOWN_BUFPUTSL(ob, ""); - hoedown_buffer_put(ob, content->data, content->size); - HOEDOWN_BUFPUTSL(ob, ""); - - return 1; -} - -static int -rndr_quote(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data) -{ - if (!content || !content->size) - return 0; - - HOEDOWN_BUFPUTSL(ob, ""); - hoedown_buffer_put(ob, content->data, content->size); - HOEDOWN_BUFPUTSL(ob, ""); - - return 1; -} - -static int -rndr_linebreak(hoedown_buffer *ob, const hoedown_renderer_data *data) -{ - hoedown_html_renderer_state *state = data->opaque; - hoedown_buffer_puts(ob, USE_XHTML(state) ? "
    \n" : "
    \n"); - return 1; -} - -static void -rndr_header(hoedown_buffer *ob, const hoedown_buffer *content, int level, const hoedown_renderer_data *data) -{ - hoedown_html_renderer_state *state = data->opaque; - - if (ob->size) - hoedown_buffer_putc(ob, '\n'); - - if (level <= state->toc_data.nesting_level) - hoedown_buffer_printf(ob, "", level, state->toc_data.header_count++); - else - hoedown_buffer_printf(ob, "", level); - - if (content) hoedown_buffer_put(ob, content->data, content->size); - hoedown_buffer_printf(ob, "\n", level); -} - -static int -rndr_link(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_buffer *link, const hoedown_buffer *title, const hoedown_renderer_data *data) -{ - hoedown_html_renderer_state *state = data->opaque; - - HOEDOWN_BUFPUTSL(ob, "size) - escape_href(ob, link->data, link->size); - - if (title && title->size) { - HOEDOWN_BUFPUTSL(ob, "\" title=\""); - escape_html(ob, title->data, title->size); - } - - if (state->link_attributes) { - hoedown_buffer_putc(ob, '\"'); - state->link_attributes(ob, link, data); - hoedown_buffer_putc(ob, '>'); - } else { - HOEDOWN_BUFPUTSL(ob, "\">"); - } - - if (content && content->size) hoedown_buffer_put(ob, content->data, content->size); - HOEDOWN_BUFPUTSL(ob, ""); - return 1; -} - -static void -rndr_list(hoedown_buffer *ob, const hoedown_buffer *content, hoedown_list_flags flags, const hoedown_renderer_data *data) -{ - if (ob->size) hoedown_buffer_putc(ob, '\n'); - hoedown_buffer_put(ob, (const uint8_t *)(flags & HOEDOWN_LIST_ORDERED ? "
      \n" : "
        \n"), 5); - if (content) hoedown_buffer_put(ob, content->data, content->size); - hoedown_buffer_put(ob, (const uint8_t *)(flags & HOEDOWN_LIST_ORDERED ? "
    \n" : "\n"), 6); -} - -static void -rndr_listitem(hoedown_buffer *ob, const hoedown_buffer *content, hoedown_list_flags flags, const hoedown_renderer_data *data) -{ - HOEDOWN_BUFPUTSL(ob, "
  • "); - if (content) { - size_t size = content->size; - while (size && content->data[size - 1] == '\n') - size--; - - hoedown_buffer_put(ob, content->data, size); - } - HOEDOWN_BUFPUTSL(ob, "
  • \n"); -} - -static void -rndr_paragraph(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data) -{ - hoedown_html_renderer_state *state = data->opaque; - size_t i = 0; - - if (ob->size) hoedown_buffer_putc(ob, '\n'); - - if (!content || !content->size) - return; - - while (i < content->size && isspace(content->data[i])) i++; - - if (i == content->size) - return; - - HOEDOWN_BUFPUTSL(ob, "

    "); - if (state->flags & HOEDOWN_HTML_HARD_WRAP) { - size_t org; - while (i < content->size) { - org = i; - while (i < content->size && content->data[i] != '\n') - i++; - - if (i > org) - hoedown_buffer_put(ob, content->data + org, i - org); - - /* - * do not insert a line break if this newline - * is the last character on the paragraph - */ - if (i >= content->size - 1) - break; - - rndr_linebreak(ob, data); - i++; - } - } else { - hoedown_buffer_put(ob, content->data + i, content->size - i); - } - HOEDOWN_BUFPUTSL(ob, "

    \n"); -} - -static void -rndr_raw_block(hoedown_buffer *ob, const hoedown_buffer *text, const hoedown_renderer_data *data) -{ - size_t org, sz; - - if (!text) - return; - - /* FIXME: Do we *really* need to trim the HTML? How does that make a difference? */ - sz = text->size; - while (sz > 0 && text->data[sz - 1] == '\n') - sz--; - - org = 0; - while (org < sz && text->data[org] == '\n') - org++; - - if (org >= sz) - return; - - if (ob->size) - hoedown_buffer_putc(ob, '\n'); - - hoedown_buffer_put(ob, text->data + org, sz - org); - hoedown_buffer_putc(ob, '\n'); -} - -static int -rndr_triple_emphasis(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data) -{ - if (!content || !content->size) return 0; - HOEDOWN_BUFPUTSL(ob, ""); - hoedown_buffer_put(ob, content->data, content->size); - HOEDOWN_BUFPUTSL(ob, ""); - return 1; -} - -static void -rndr_hrule(hoedown_buffer *ob, const hoedown_renderer_data *data) -{ - hoedown_html_renderer_state *state = data->opaque; - if (ob->size) hoedown_buffer_putc(ob, '\n'); - hoedown_buffer_puts(ob, USE_XHTML(state) ? "
    \n" : "
    \n"); -} - -static int -rndr_image(hoedown_buffer *ob, const hoedown_buffer *link, const hoedown_buffer *title, const hoedown_buffer *alt, const hoedown_renderer_data *data) -{ - hoedown_html_renderer_state *state = data->opaque; - if (!link || !link->size) return 0; - - HOEDOWN_BUFPUTSL(ob, "data, link->size); - HOEDOWN_BUFPUTSL(ob, "\" alt=\""); - - if (alt && alt->size) - escape_html(ob, alt->data, alt->size); - - if (title && title->size) { - HOEDOWN_BUFPUTSL(ob, "\" title=\""); - escape_html(ob, title->data, title->size); } - - hoedown_buffer_puts(ob, USE_XHTML(state) ? "\"/>" : "\">"); - return 1; -} - -static int -rndr_raw_html(hoedown_buffer *ob, const hoedown_buffer *text, const hoedown_renderer_data *data) -{ - hoedown_html_renderer_state *state = data->opaque; - - /* ESCAPE overrides SKIP_HTML. It doesn't look to see if - * there are any valid tags, just escapes all of them. */ - if((state->flags & HOEDOWN_HTML_ESCAPE) != 0) { - escape_html(ob, text->data, text->size); - return 1; - } - - if ((state->flags & HOEDOWN_HTML_SKIP_HTML) != 0) - return 1; - - hoedown_buffer_put(ob, text->data, text->size); - return 1; -} - -static void -rndr_table(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data) -{ - if (ob->size) hoedown_buffer_putc(ob, '\n'); - HOEDOWN_BUFPUTSL(ob, "\n"); - hoedown_buffer_put(ob, content->data, content->size); - HOEDOWN_BUFPUTSL(ob, "
    \n"); -} - -static void -rndr_table_header(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data) -{ - if (ob->size) hoedown_buffer_putc(ob, '\n'); - HOEDOWN_BUFPUTSL(ob, "\n"); - hoedown_buffer_put(ob, content->data, content->size); - HOEDOWN_BUFPUTSL(ob, "\n"); -} - -static void -rndr_table_body(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data) -{ - if (ob->size) hoedown_buffer_putc(ob, '\n'); - HOEDOWN_BUFPUTSL(ob, "\n"); - hoedown_buffer_put(ob, content->data, content->size); - HOEDOWN_BUFPUTSL(ob, "\n"); -} - -static void -rndr_tablerow(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data) -{ - HOEDOWN_BUFPUTSL(ob, "\n"); - if (content) hoedown_buffer_put(ob, content->data, content->size); - HOEDOWN_BUFPUTSL(ob, "\n"); -} - -static void -rndr_tablecell(hoedown_buffer *ob, const hoedown_buffer *content, hoedown_table_flags flags, const hoedown_renderer_data *data) -{ - if (flags & HOEDOWN_TABLE_HEADER) { - HOEDOWN_BUFPUTSL(ob, ""); - break; - - case HOEDOWN_TABLE_ALIGN_LEFT: - HOEDOWN_BUFPUTSL(ob, " style=\"text-align: left\">"); - break; - - case HOEDOWN_TABLE_ALIGN_RIGHT: - HOEDOWN_BUFPUTSL(ob, " style=\"text-align: right\">"); - break; - - default: - HOEDOWN_BUFPUTSL(ob, ">"); - } - - if (content) - hoedown_buffer_put(ob, content->data, content->size); - - if (flags & HOEDOWN_TABLE_HEADER) { - HOEDOWN_BUFPUTSL(ob, "\n"); - } else { - HOEDOWN_BUFPUTSL(ob, "\n"); - } -} - -static int -rndr_superscript(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data) -{ - if (!content || !content->size) return 0; - HOEDOWN_BUFPUTSL(ob, ""); - hoedown_buffer_put(ob, content->data, content->size); - HOEDOWN_BUFPUTSL(ob, ""); - return 1; -} - -static void -rndr_normal_text(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data) -{ - if (content) - escape_html(ob, content->data, content->size); -} - -static void -rndr_footnotes(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data) -{ - hoedown_html_renderer_state *state = data->opaque; - - if (ob->size) hoedown_buffer_putc(ob, '\n'); - HOEDOWN_BUFPUTSL(ob, "
    \n"); - hoedown_buffer_puts(ob, USE_XHTML(state) ? "
    \n" : "
    \n"); - HOEDOWN_BUFPUTSL(ob, "
      \n"); - - if (content) hoedown_buffer_put(ob, content->data, content->size); - - HOEDOWN_BUFPUTSL(ob, "\n
    \n
    \n"); -} - -static void -rndr_footnote_def(hoedown_buffer *ob, const hoedown_buffer *content, unsigned int num, const hoedown_renderer_data *data) -{ - size_t i = 0; - int pfound = 0; - - /* insert anchor at the end of first paragraph block */ - if (content) { - while ((i+3) < content->size) { - if (content->data[i++] != '<') continue; - if (content->data[i++] != '/') continue; - if (content->data[i++] != 'p' && content->data[i] != 'P') continue; - if (content->data[i] != '>') continue; - i -= 3; - pfound = 1; - break; - } - } - - hoedown_buffer_printf(ob, "\n
  • \n", num); - if (pfound) { - hoedown_buffer_put(ob, content->data, i); - hoedown_buffer_printf(ob, " ", num); - hoedown_buffer_put(ob, content->data + i, content->size - i); - } else if (content) { - hoedown_buffer_put(ob, content->data, content->size); - } - HOEDOWN_BUFPUTSL(ob, "
  • \n"); -} - -static int -rndr_footnote_ref(hoedown_buffer *ob, unsigned int num, const hoedown_renderer_data *data) -{ - hoedown_buffer_printf(ob, "%d", num, num, num); - return 1; -} - -static int -rndr_math(hoedown_buffer *ob, const hoedown_buffer *text, int displaymode, const hoedown_renderer_data *data) -{ - hoedown_buffer_put(ob, (const uint8_t *)(displaymode ? "\\[" : "\\("), 2); - escape_html(ob, text->data, text->size); - hoedown_buffer_put(ob, (const uint8_t *)(displaymode ? "\\]" : "\\)"), 2); - return 1; -} - -static void -toc_header(hoedown_buffer *ob, const hoedown_buffer *content, int level, const hoedown_renderer_data *data) -{ - hoedown_html_renderer_state *state = data->opaque; - - if (level <= state->toc_data.nesting_level) { - /* set the level offset if this is the first header - * we're parsing for the document */ - if (state->toc_data.current_level == 0) - state->toc_data.level_offset = level - 1; - - level -= state->toc_data.level_offset; - - if (level > state->toc_data.current_level) { - while (level > state->toc_data.current_level) { - HOEDOWN_BUFPUTSL(ob, "
      \n
    • \n"); - state->toc_data.current_level++; - } - } else if (level < state->toc_data.current_level) { - HOEDOWN_BUFPUTSL(ob, "
    • \n"); - while (level < state->toc_data.current_level) { - HOEDOWN_BUFPUTSL(ob, "
    \n
  • \n"); - state->toc_data.current_level--; - } - HOEDOWN_BUFPUTSL(ob,"
  • \n"); - } else { - HOEDOWN_BUFPUTSL(ob,"
  • \n
  • \n"); - } - - hoedown_buffer_printf(ob, "", state->toc_data.header_count++); - if (content) hoedown_buffer_put(ob, content->data, content->size); - HOEDOWN_BUFPUTSL(ob, "\n"); - } -} - -static int -toc_link(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_buffer *link, const hoedown_buffer *title, const hoedown_renderer_data *data) -{ - if (content && content->size) hoedown_buffer_put(ob, content->data, content->size); - return 1; -} - -static void -toc_finalize(hoedown_buffer *ob, int inline_render, const hoedown_renderer_data *data) -{ - hoedown_html_renderer_state *state; - - if (inline_render) - return; - - state = data->opaque; - - while (state->toc_data.current_level > 0) { - HOEDOWN_BUFPUTSL(ob, "
  • \n\n"); - state->toc_data.current_level--; - } - - state->toc_data.header_count = 0; -} - -hoedown_renderer * -hoedown_html_toc_renderer_new(int nesting_level) -{ - static const hoedown_renderer cb_default = { - NULL, - - NULL, - NULL, - toc_header, - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - - NULL, - rndr_codespan, - rndr_double_emphasis, - rndr_emphasis, - rndr_underline, - rndr_highlight, - rndr_quote, - NULL, - NULL, - toc_link, - rndr_triple_emphasis, - rndr_strikethrough, - rndr_superscript, - NULL, - NULL, - NULL, - - NULL, - rndr_normal_text, - - NULL, - toc_finalize - }; - - hoedown_html_renderer_state *state; - hoedown_renderer *renderer; - - /* Prepare the state pointer */ - state = hoedown_malloc(sizeof(hoedown_html_renderer_state)); - memset(state, 0x0, sizeof(hoedown_html_renderer_state)); - - state->toc_data.nesting_level = nesting_level; - - /* Prepare the renderer */ - renderer = hoedown_malloc(sizeof(hoedown_renderer)); - memcpy(renderer, &cb_default, sizeof(hoedown_renderer)); - - renderer->opaque = state; - return renderer; -} - -hoedown_renderer * -hoedown_html_renderer_new(hoedown_html_flags render_flags, int nesting_level) -{ - static const hoedown_renderer cb_default = { - NULL, - - rndr_blockcode, - rndr_blockquote, - rndr_header, - rndr_hrule, - rndr_list, - rndr_listitem, - rndr_paragraph, - rndr_table, - rndr_table_header, - rndr_table_body, - rndr_tablerow, - rndr_tablecell, - rndr_footnotes, - rndr_footnote_def, - rndr_raw_block, - - rndr_autolink, - rndr_codespan, - rndr_double_emphasis, - rndr_emphasis, - rndr_underline, - rndr_highlight, - rndr_quote, - rndr_image, - rndr_linebreak, - rndr_link, - rndr_triple_emphasis, - rndr_strikethrough, - rndr_superscript, - rndr_footnote_ref, - rndr_math, - rndr_raw_html, - - NULL, - rndr_normal_text, - - NULL, - NULL - }; - - hoedown_html_renderer_state *state; - hoedown_renderer *renderer; - - /* Prepare the state pointer */ - state = hoedown_malloc(sizeof(hoedown_html_renderer_state)); - memset(state, 0x0, sizeof(hoedown_html_renderer_state)); - - state->flags = render_flags; - state->toc_data.nesting_level = nesting_level; - - /* Prepare the renderer */ - renderer = hoedown_malloc(sizeof(hoedown_renderer)); - memcpy(renderer, &cb_default, sizeof(hoedown_renderer)); - - if (render_flags & HOEDOWN_HTML_SKIP_HTML || render_flags & HOEDOWN_HTML_ESCAPE) - renderer->blockhtml = NULL; - - renderer->opaque = state; - return renderer; -} - -void -hoedown_html_renderer_free(hoedown_renderer *renderer) -{ - free(renderer->opaque); - free(renderer); -} diff --git a/libraries/hoedown/src/html_blocks.c b/libraries/hoedown/src/html_blocks.c deleted file mode 100644 index f5e9dce9..00000000 --- a/libraries/hoedown/src/html_blocks.c +++ /dev/null @@ -1,240 +0,0 @@ -/* ANSI-C code produced by gperf version 3.0.3 */ -/* Command-line: gperf -L ANSI-C -N hoedown_find_block_tag -c -C -E -S 1 --ignore-case -m100 html_block_names.gperf */ -/* Computed positions: -k'1-2' */ - -#if !((' ' == 32) && ('!' == 33) && ('"' == 34) && ('#' == 35) \ - && ('%' == 37) && ('&' == 38) && ('\'' == 39) && ('(' == 40) \ - && (')' == 41) && ('*' == 42) && ('+' == 43) && (',' == 44) \ - && ('-' == 45) && ('.' == 46) && ('/' == 47) && ('0' == 48) \ - && ('1' == 49) && ('2' == 50) && ('3' == 51) && ('4' == 52) \ - && ('5' == 53) && ('6' == 54) && ('7' == 55) && ('8' == 56) \ - && ('9' == 57) && (':' == 58) && (';' == 59) && ('<' == 60) \ - && ('=' == 61) && ('>' == 62) && ('?' == 63) && ('A' == 65) \ - && ('B' == 66) && ('C' == 67) && ('D' == 68) && ('E' == 69) \ - && ('F' == 70) && ('G' == 71) && ('H' == 72) && ('I' == 73) \ - && ('J' == 74) && ('K' == 75) && ('L' == 76) && ('M' == 77) \ - && ('N' == 78) && ('O' == 79) && ('P' == 80) && ('Q' == 81) \ - && ('R' == 82) && ('S' == 83) && ('T' == 84) && ('U' == 85) \ - && ('V' == 86) && ('W' == 87) && ('X' == 88) && ('Y' == 89) \ - && ('Z' == 90) && ('[' == 91) && ('\\' == 92) && (']' == 93) \ - && ('^' == 94) && ('_' == 95) && ('a' == 97) && ('b' == 98) \ - && ('c' == 99) && ('d' == 100) && ('e' == 101) && ('f' == 102) \ - && ('g' == 103) && ('h' == 104) && ('i' == 105) && ('j' == 106) \ - && ('k' == 107) && ('l' == 108) && ('m' == 109) && ('n' == 110) \ - && ('o' == 111) && ('p' == 112) && ('q' == 113) && ('r' == 114) \ - && ('s' == 115) && ('t' == 116) && ('u' == 117) && ('v' == 118) \ - && ('w' == 119) && ('x' == 120) && ('y' == 121) && ('z' == 122) \ - && ('{' == 123) && ('|' == 124) && ('}' == 125) && ('~' == 126)) -/* The character set is not based on ISO-646. */ -#error "gperf generated tables don't work with this execution character set. Please report a bug to ." -#endif - -/* maximum key range = 24, duplicates = 0 */ - -#ifndef GPERF_DOWNCASE -#define GPERF_DOWNCASE 1 -static unsigned char gperf_downcase[256] = - { - 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, - 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, - 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, - 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, - 60, 61, 62, 63, 64, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, - 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, - 122, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, - 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, - 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, - 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, - 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, - 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, - 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, - 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, - 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224, - 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, - 240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, - 255 - }; -#endif - -#ifndef GPERF_CASE_STRNCMP -#define GPERF_CASE_STRNCMP 1 -static int -gperf_case_strncmp (register const char *s1, register const char *s2, register unsigned int n) -{ - for (; n > 0;) - { - unsigned char c1 = gperf_downcase[(unsigned char)*s1++]; - unsigned char c2 = gperf_downcase[(unsigned char)*s2++]; - if (c1 != 0 && c1 == c2) - { - n--; - continue; - } - return (int)c1 - (int)c2; - } - return 0; -} -#endif - -#ifdef __GNUC__ -__inline -#else -#ifdef __cplusplus -inline -#endif -#endif -static unsigned int -hash (register const char *str, register unsigned int len) -{ - static const unsigned char asso_values[] = - { - 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, - 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, - 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, - 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, - 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, - 22, 21, 19, 18, 16, 0, 25, 25, 25, 25, - 25, 25, 25, 25, 25, 25, 1, 25, 0, 25, - 1, 0, 0, 13, 0, 25, 25, 11, 2, 1, - 0, 25, 25, 5, 0, 2, 25, 25, 25, 25, - 25, 25, 25, 25, 25, 25, 25, 25, 1, 25, - 0, 25, 1, 0, 0, 13, 0, 25, 25, 11, - 2, 1, 0, 25, 25, 5, 0, 2, 25, 25, - 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, - 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, - 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, - 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, - 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, - 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, - 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, - 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, - 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, - 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, - 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, - 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, - 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, - 25, 25, 25, 25, 25, 25, 25 - }; - register int hval = (int)len; - - switch (hval) - { - default: - hval += asso_values[(unsigned char)str[1]+1]; - /*FALLTHROUGH*/ - case 1: - hval += asso_values[(unsigned char)str[0]]; - break; - } - return hval; -} - -#ifdef __GNUC__ -__inline -#ifdef __GNUC_STDC_INLINE__ -__attribute__ ((__gnu_inline__)) -#endif -#endif -const char * -hoedown_find_block_tag (register const char *str, register unsigned int len) -{ - enum - { - TOTAL_KEYWORDS = 24, - MIN_WORD_LENGTH = 1, - MAX_WORD_LENGTH = 10, - MIN_HASH_VALUE = 1, - MAX_HASH_VALUE = 24 - }; - - if (len <= MAX_WORD_LENGTH && len >= MIN_WORD_LENGTH) - { - register int key = hash (str, len); - - if (key <= MAX_HASH_VALUE && key >= MIN_HASH_VALUE) - { - register const char *resword; - - switch (key - 1) - { - case 0: - resword = "p"; - goto compare; - case 1: - resword = "h6"; - goto compare; - case 2: - resword = "div"; - goto compare; - case 3: - resword = "del"; - goto compare; - case 4: - resword = "form"; - goto compare; - case 5: - resword = "table"; - goto compare; - case 6: - resword = "figure"; - goto compare; - case 7: - resword = "pre"; - goto compare; - case 8: - resword = "fieldset"; - goto compare; - case 9: - resword = "noscript"; - goto compare; - case 10: - resword = "script"; - goto compare; - case 11: - resword = "style"; - goto compare; - case 12: - resword = "dl"; - goto compare; - case 13: - resword = "ol"; - goto compare; - case 14: - resword = "ul"; - goto compare; - case 15: - resword = "math"; - goto compare; - case 16: - resword = "ins"; - goto compare; - case 17: - resword = "h5"; - goto compare; - case 18: - resword = "iframe"; - goto compare; - case 19: - resword = "h4"; - goto compare; - case 20: - resword = "h3"; - goto compare; - case 21: - resword = "blockquote"; - goto compare; - case 22: - resword = "h2"; - goto compare; - case 23: - resword = "h1"; - goto compare; - } - return 0; - compare: - if ((((unsigned char)*str ^ (unsigned char)*resword) & ~32) == 0 && !gperf_case_strncmp (str, resword, len) && resword[len] == '\0') - return resword; - } - } - return 0; -} diff --git a/libraries/hoedown/src/html_smartypants.c b/libraries/hoedown/src/html_smartypants.c deleted file mode 100644 index d89624f3..00000000 --- a/libraries/hoedown/src/html_smartypants.c +++ /dev/null @@ -1,435 +0,0 @@ -#include "hoedown/html.h" - -#include -#include -#include -#include - -#ifdef _MSC_VER -#define snprintf _snprintf -#endif - -struct smartypants_data { - int in_squote; - int in_dquote; -}; - -static size_t smartypants_cb__ltag(hoedown_buffer *ob, struct smartypants_data *smrt, uint8_t previous_char, const uint8_t *text, size_t size); -static size_t smartypants_cb__dquote(hoedown_buffer *ob, struct smartypants_data *smrt, uint8_t previous_char, const uint8_t *text, size_t size); -static size_t smartypants_cb__amp(hoedown_buffer *ob, struct smartypants_data *smrt, uint8_t previous_char, const uint8_t *text, size_t size); -static size_t smartypants_cb__period(hoedown_buffer *ob, struct smartypants_data *smrt, uint8_t previous_char, const uint8_t *text, size_t size); -static size_t smartypants_cb__number(hoedown_buffer *ob, struct smartypants_data *smrt, uint8_t previous_char, const uint8_t *text, size_t size); -static size_t smartypants_cb__dash(hoedown_buffer *ob, struct smartypants_data *smrt, uint8_t previous_char, const uint8_t *text, size_t size); -static size_t smartypants_cb__parens(hoedown_buffer *ob, struct smartypants_data *smrt, uint8_t previous_char, const uint8_t *text, size_t size); -static size_t smartypants_cb__squote(hoedown_buffer *ob, struct smartypants_data *smrt, uint8_t previous_char, const uint8_t *text, size_t size); -static size_t smartypants_cb__backtick(hoedown_buffer *ob, struct smartypants_data *smrt, uint8_t previous_char, const uint8_t *text, size_t size); -static size_t smartypants_cb__escape(hoedown_buffer *ob, struct smartypants_data *smrt, uint8_t previous_char, const uint8_t *text, size_t size); - -static size_t (*smartypants_cb_ptrs[]) - (hoedown_buffer *, struct smartypants_data *, uint8_t, const uint8_t *, size_t) = -{ - NULL, /* 0 */ - smartypants_cb__dash, /* 1 */ - smartypants_cb__parens, /* 2 */ - smartypants_cb__squote, /* 3 */ - smartypants_cb__dquote, /* 4 */ - smartypants_cb__amp, /* 5 */ - smartypants_cb__period, /* 6 */ - smartypants_cb__number, /* 7 */ - smartypants_cb__ltag, /* 8 */ - smartypants_cb__backtick, /* 9 */ - smartypants_cb__escape, /* 10 */ -}; - -static const uint8_t smartypants_cb_chars[UINT8_MAX+1] = { - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 4, 0, 0, 0, 5, 3, 2, 0, 0, 0, 0, 1, 6, 0, - 0, 7, 0, 7, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 0, 0, 0, - 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -}; - -static int -word_boundary(uint8_t c) -{ - return c == 0 || isspace(c) || ispunct(c); -} - -/* - If 'text' begins with any kind of single quote (e.g. "'" or "'" etc.), - returns the length of the sequence of characters that makes up the single- - quote. Otherwise, returns zero. -*/ -static size_t -squote_len(const uint8_t *text, size_t size) -{ - static char* single_quote_list[] = { "'", "'", "'", "'", NULL }; - char** p; - - for (p = single_quote_list; *p; ++p) { - size_t len = strlen(*p); - if (size >= len && memcmp(text, *p, len) == 0) { - return len; - } - } - - return 0; -} - -/* Converts " or ' at very beginning or end of a word to left or right quote */ -static int -smartypants_quotes(hoedown_buffer *ob, uint8_t previous_char, uint8_t next_char, uint8_t quote, int *is_open) -{ - char ent[8]; - - if (*is_open && !word_boundary(next_char)) - return 0; - - if (!(*is_open) && !word_boundary(previous_char)) - return 0; - - snprintf(ent, sizeof(ent), "&%c%cquo;", (*is_open) ? 'r' : 'l', quote); - *is_open = !(*is_open); - hoedown_buffer_puts(ob, ent); - return 1; -} - -/* - Converts ' to left or right single quote; but the initial ' might be in - different forms, e.g. ' or ' or '. - 'squote_text' points to the original single quote, and 'squote_size' is its length. - 'text' points at the last character of the single-quote, e.g. ' or ; -*/ -static size_t -smartypants_squote(hoedown_buffer *ob, struct smartypants_data *smrt, uint8_t previous_char, const uint8_t *text, size_t size, - const uint8_t *squote_text, size_t squote_size) -{ - if (size >= 2) { - uint8_t t1 = tolower(text[1]); - size_t next_squote_len = squote_len(text+1, size-1); - - /* convert '' to “ or ” */ - if (next_squote_len > 0) { - uint8_t next_char = (size > 1+next_squote_len) ? text[1+next_squote_len] : 0; - if (smartypants_quotes(ob, previous_char, next_char, 'd', &smrt->in_dquote)) - return next_squote_len; - } - - /* Tom's, isn't, I'm, I'd */ - if ((t1 == 's' || t1 == 't' || t1 == 'm' || t1 == 'd') && - (size == 3 || word_boundary(text[2]))) { - HOEDOWN_BUFPUTSL(ob, "’"); - return 0; - } - - /* you're, you'll, you've */ - if (size >= 3) { - uint8_t t2 = tolower(text[2]); - - if (((t1 == 'r' && t2 == 'e') || - (t1 == 'l' && t2 == 'l') || - (t1 == 'v' && t2 == 'e')) && - (size == 4 || word_boundary(text[3]))) { - HOEDOWN_BUFPUTSL(ob, "’"); - return 0; - } - } - } - - if (smartypants_quotes(ob, previous_char, size > 0 ? text[1] : 0, 's', &smrt->in_squote)) - return 0; - - hoedown_buffer_put(ob, squote_text, squote_size); - return 0; -} - -/* Converts ' to left or right single quote. */ -static size_t -smartypants_cb__squote(hoedown_buffer *ob, struct smartypants_data *smrt, uint8_t previous_char, const uint8_t *text, size_t size) -{ - return smartypants_squote(ob, smrt, previous_char, text, size, text, 1); -} - -/* Converts (c), (r), (tm) */ -static size_t -smartypants_cb__parens(hoedown_buffer *ob, struct smartypants_data *smrt, uint8_t previous_char, const uint8_t *text, size_t size) -{ - if (size >= 3) { - uint8_t t1 = tolower(text[1]); - uint8_t t2 = tolower(text[2]); - - if (t1 == 'c' && t2 == ')') { - HOEDOWN_BUFPUTSL(ob, "©"); - return 2; - } - - if (t1 == 'r' && t2 == ')') { - HOEDOWN_BUFPUTSL(ob, "®"); - return 2; - } - - if (size >= 4 && t1 == 't' && t2 == 'm' && text[3] == ')') { - HOEDOWN_BUFPUTSL(ob, "™"); - return 3; - } - } - - hoedown_buffer_putc(ob, text[0]); - return 0; -} - -/* Converts "--" to em-dash, etc. */ -static size_t -smartypants_cb__dash(hoedown_buffer *ob, struct smartypants_data *smrt, uint8_t previous_char, const uint8_t *text, size_t size) -{ - if (size >= 3 && text[1] == '-' && text[2] == '-') { - HOEDOWN_BUFPUTSL(ob, "—"); - return 2; - } - - if (size >= 2 && text[1] == '-') { - HOEDOWN_BUFPUTSL(ob, "–"); - return 1; - } - - hoedown_buffer_putc(ob, text[0]); - return 0; -} - -/* Converts " etc. */ -static size_t -smartypants_cb__amp(hoedown_buffer *ob, struct smartypants_data *smrt, uint8_t previous_char, const uint8_t *text, size_t size) -{ - size_t len; - if (size >= 6 && memcmp(text, """, 6) == 0) { - if (smartypants_quotes(ob, previous_char, size >= 7 ? text[6] : 0, 'd', &smrt->in_dquote)) - return 5; - } - - len = squote_len(text, size); - if (len > 0) { - return (len-1) + smartypants_squote(ob, smrt, previous_char, text+(len-1), size-(len-1), text, len); - } - - if (size >= 4 && memcmp(text, "�", 4) == 0) - return 3; - - hoedown_buffer_putc(ob, '&'); - return 0; -} - -/* Converts "..." to ellipsis */ -static size_t -smartypants_cb__period(hoedown_buffer *ob, struct smartypants_data *smrt, uint8_t previous_char, const uint8_t *text, size_t size) -{ - if (size >= 3 && text[1] == '.' && text[2] == '.') { - HOEDOWN_BUFPUTSL(ob, "…"); - return 2; - } - - if (size >= 5 && text[1] == ' ' && text[2] == '.' && text[3] == ' ' && text[4] == '.') { - HOEDOWN_BUFPUTSL(ob, "…"); - return 4; - } - - hoedown_buffer_putc(ob, text[0]); - return 0; -} - -/* Converts `` to opening double quote */ -static size_t -smartypants_cb__backtick(hoedown_buffer *ob, struct smartypants_data *smrt, uint8_t previous_char, const uint8_t *text, size_t size) -{ - if (size >= 2 && text[1] == '`') { - if (smartypants_quotes(ob, previous_char, size >= 3 ? text[2] : 0, 'd', &smrt->in_dquote)) - return 1; - } - - hoedown_buffer_putc(ob, text[0]); - return 0; -} - -/* Converts 1/2, 1/4, 3/4 */ -static size_t -smartypants_cb__number(hoedown_buffer *ob, struct smartypants_data *smrt, uint8_t previous_char, const uint8_t *text, size_t size) -{ - if (word_boundary(previous_char) && size >= 3) { - if (text[0] == '1' && text[1] == '/' && text[2] == '2') { - if (size == 3 || word_boundary(text[3])) { - HOEDOWN_BUFPUTSL(ob, "½"); - return 2; - } - } - - if (text[0] == '1' && text[1] == '/' && text[2] == '4') { - if (size == 3 || word_boundary(text[3]) || - (size >= 5 && tolower(text[3]) == 't' && tolower(text[4]) == 'h')) { - HOEDOWN_BUFPUTSL(ob, "¼"); - return 2; - } - } - - if (text[0] == '3' && text[1] == '/' && text[2] == '4') { - if (size == 3 || word_boundary(text[3]) || - (size >= 6 && tolower(text[3]) == 't' && tolower(text[4]) == 'h' && tolower(text[5]) == 's')) { - HOEDOWN_BUFPUTSL(ob, "¾"); - return 2; - } - } - } - - hoedown_buffer_putc(ob, text[0]); - return 0; -} - -/* Converts " to left or right double quote */ -static size_t -smartypants_cb__dquote(hoedown_buffer *ob, struct smartypants_data *smrt, uint8_t previous_char, const uint8_t *text, size_t size) -{ - if (!smartypants_quotes(ob, previous_char, size > 0 ? text[1] : 0, 'd', &smrt->in_dquote)) - HOEDOWN_BUFPUTSL(ob, """); - - return 0; -} - -static size_t -smartypants_cb__ltag(hoedown_buffer *ob, struct smartypants_data *smrt, uint8_t previous_char, const uint8_t *text, size_t size) -{ - static const char *skip_tags[] = { - "pre", "code", "var", "samp", "kbd", "math", "script", "style" - }; - static const size_t skip_tags_count = 8; - - size_t tag, i = 0; - - /* This is a comment. Copy everything verbatim until --> or EOF is seen. */ - if (i + 4 < size && memcmp(text, "", 3) != 0) - i++; - i += 3; - hoedown_buffer_put(ob, text, i + 1); - return i; - } - - while (i < size && text[i] != '>') - i++; - - for (tag = 0; tag < skip_tags_count; ++tag) { - if (hoedown_html_is_tag(text, size, skip_tags[tag]) == HOEDOWN_HTML_TAG_OPEN) - break; - } - - if (tag < skip_tags_count) { - for (;;) { - while (i < size && text[i] != '<') - i++; - - if (i == size) - break; - - if (hoedown_html_is_tag(text + i, size - i, skip_tags[tag]) == HOEDOWN_HTML_TAG_CLOSE) - break; - - i++; - } - - while (i < size && text[i] != '>') - i++; - } - - hoedown_buffer_put(ob, text, i + 1); - return i; -} - -static size_t -smartypants_cb__escape(hoedown_buffer *ob, struct smartypants_data *smrt, uint8_t previous_char, const uint8_t *text, size_t size) -{ - if (size < 2) - return 0; - - switch (text[1]) { - case '\\': - case '"': - case '\'': - case '.': - case '-': - case '`': - hoedown_buffer_putc(ob, text[1]); - return 1; - - default: - hoedown_buffer_putc(ob, '\\'); - return 0; - } -} - -#if 0 -static struct { - uint8_t c0; - const uint8_t *pattern; - const uint8_t *entity; - int skip; -} smartypants_subs[] = { - { '\'', "'s>", "’", 0 }, - { '\'', "'t>", "’", 0 }, - { '\'', "'re>", "’", 0 }, - { '\'', "'ll>", "’", 0 }, - { '\'', "'ve>", "’", 0 }, - { '\'', "'m>", "’", 0 }, - { '\'', "'d>", "’", 0 }, - { '-', "--", "—", 1 }, - { '-', "<->", "–", 0 }, - { '.', "...", "…", 2 }, - { '.', ". . .", "…", 4 }, - { '(', "(c)", "©", 2 }, - { '(', "(r)", "®", 2 }, - { '(', "(tm)", "™", 3 }, - { '3', "<3/4>", "¾", 2 }, - { '3', "<3/4ths>", "¾", 2 }, - { '1', "<1/2>", "½", 2 }, - { '1', "<1/4>", "¼", 2 }, - { '1', "<1/4th>", "¼", 2 }, - { '&', "�", 0, 3 }, -}; -#endif - -void -hoedown_html_smartypants(hoedown_buffer *ob, const uint8_t *text, size_t size) -{ - size_t i; - struct smartypants_data smrt = {0, 0}; - - if (!text) - return; - - hoedown_buffer_grow(ob, size); - - for (i = 0; i < size; ++i) { - size_t org; - uint8_t action = 0; - - org = i; - while (i < size && (action = smartypants_cb_chars[text[i]]) == 0) - i++; - - if (i > org) - hoedown_buffer_put(ob, text + org, i - org); - - if (i < size) { - i += smartypants_cb_ptrs[(int)action] - (ob, &smrt, i ? text[i - 1] : 0, text + i, size - i); - } - } -} diff --git a/libraries/hoedown/src/stack.c b/libraries/hoedown/src/stack.c deleted file mode 100644 index 0523c11b..00000000 --- a/libraries/hoedown/src/stack.c +++ /dev/null @@ -1,79 +0,0 @@ -#include "hoedown/stack.h" - -#include "hoedown/buffer.h" - -#include -#include -#include - -void -hoedown_stack_init(hoedown_stack *st, size_t initial_size) -{ - assert(st); - - st->item = NULL; - st->size = st->asize = 0; - - if (!initial_size) - initial_size = 8; - - hoedown_stack_grow(st, initial_size); -} - -void -hoedown_stack_uninit(hoedown_stack *st) -{ - assert(st); - - free(st->item); -} - -void -hoedown_stack_grow(hoedown_stack *st, size_t neosz) -{ - assert(st); - - if (st->asize >= neosz) - return; - - st->item = hoedown_realloc(st->item, neosz * sizeof(void *)); - memset(st->item + st->asize, 0x0, (neosz - st->asize) * sizeof(void *)); - - st->asize = neosz; - - if (st->size > neosz) - st->size = neosz; -} - -void -hoedown_stack_push(hoedown_stack *st, void *item) -{ - assert(st); - - if (st->size >= st->asize) - hoedown_stack_grow(st, st->size * 2); - - st->item[st->size++] = item; -} - -void * -hoedown_stack_pop(hoedown_stack *st) -{ - assert(st); - - if (!st->size) - return NULL; - - return st->item[--st->size]; -} - -void * -hoedown_stack_top(const hoedown_stack *st) -{ - assert(st); - - if (!st->size) - return NULL; - - return st->item[st->size - 1]; -} diff --git a/libraries/hoedown/src/version.c b/libraries/hoedown/src/version.c deleted file mode 100644 index 10d36cb9..00000000 --- a/libraries/hoedown/src/version.c +++ /dev/null @@ -1,9 +0,0 @@ -#include "hoedown/version.h" - -void -hoedown_version(int *major, int *minor, int *revision) -{ - *major = HOEDOWN_VERSION_MAJOR; - *minor = HOEDOWN_VERSION_MINOR; - *revision = HOEDOWN_VERSION_REVISION; -} From 22a2b7ac463e7ea339d4d57be3b770fbf09518bf Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Sat, 7 Jan 2023 14:57:13 +0100 Subject: [PATCH 078/152] refactor: support system and bundled cmark Signed-off-by: Sefa Eyeoglu --- .gitmodules | 3 +++ CMakeLists.txt | 13 +++++++++++++ launcher/CMakeLists.txt | 2 +- libraries/cmark | 1 + 4 files changed, 18 insertions(+), 1 deletion(-) create mode 160000 libraries/cmark diff --git a/.gitmodules b/.gitmodules index 95274f15..87703fee 100644 --- a/.gitmodules +++ b/.gitmodules @@ -16,3 +16,6 @@ [submodule "libraries/extra-cmake-modules"] path = libraries/extra-cmake-modules url = https://github.com/KDE/extra-cmake-modules +[submodule "libraries/cmark"] + path = libraries/cmark + url = https://github.com/commonmark/cmark.git diff --git a/CMakeLists.txt b/CMakeLists.txt index f235a2ac..2194317b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -266,6 +266,9 @@ if(NOT Launcher_FORCE_BUNDLED_LIBS) # Find ghc_filesystem find_package(ghc_filesystem QUIET) + + # Find cmark + find_package(cmark QUIET) endif() include(ECMQtDeclareLoggingCategory) @@ -407,6 +410,16 @@ if(NOT tomlplusplus_FOUND) else() message(STATUS "Using system tomlplusplus") endif() +if(NOT cmark_FOUND) + message(STATUS "Using bundled cmark") + set(CMARK_STATIC ON CACHE BOOL "Build static libcmark library" FORCE) + set(CMARK_SHARED OFF CACHE BOOL "Build shared libcmark library" FORCE) + set(CMARK_TESTS OFF CACHE BOOL "Build cmark tests and enable testing" FORCE) + add_subdirectory(libraries/cmark EXCLUDE_FROM_ALL) # Markdown parser + add_library(cmark::cmark ALIAS cmark_static) +else() + message(STATUS "Using system cmark") +endif() add_subdirectory(libraries/katabasis) # An OAuth2 library that tried to do too much add_subdirectory(libraries/gamemode) add_subdirectory(libraries/murmur2) # Hash for usage with the CurseForge API diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index 7dc744aa..60acc6fc 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -1043,7 +1043,7 @@ target_link_libraries(Launcher_logic ) target_link_libraries(Launcher_logic QuaZip::QuaZip - cmark + cmark::cmark LocalPeer Launcher_rainbow ) diff --git a/libraries/cmark b/libraries/cmark new file mode 160000 index 00000000..a8da5a2f --- /dev/null +++ b/libraries/cmark @@ -0,0 +1 @@ +Subproject commit a8da5a2f252b96eca60ae8bada1a9ba059a38401 From 3ee0ec7cd03a2bd0d2b1d64a8341abd4fad9d88d Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Sat, 7 Jan 2023 15:07:53 +0100 Subject: [PATCH 079/152] fix(nix): add cmark dependency Signed-off-by: Sefa Eyeoglu --- nix/default.nix | 2 ++ 1 file changed, 2 insertions(+) diff --git a/nix/default.nix b/nix/default.nix index 82ba9c7d..f6ab1332 100644 --- a/nix/default.nix +++ b/nix/default.nix @@ -18,6 +18,7 @@ , extra-cmake-modules , tomlplusplus , ghc_filesystem +, cmark , msaClientID ? "" , jdks ? [ jdk17 jdk8 ] @@ -41,6 +42,7 @@ stdenv.mkDerivation rec { quazip ghc_filesystem tomlplusplus + cmark ] ++ lib.optional (lib.versionAtLeast qtbase.version "6") qtwayland; cmakeFlags = lib.optionals (msaClientID != "") [ "-DLauncher_MSA_CLIENT_ID=${msaClientID}" ] From 4e2a9588962fd24f6a5fe37e1c44555966ca7aa4 Mon Sep 17 00:00:00 2001 From: Joshua Goins Date: Sat, 7 Jan 2023 14:18:37 -0500 Subject: [PATCH 080/152] fix(flatpak): enable builddir Signed-off-by: Joshua Goins --- flatpak/org.prismlauncher.PrismLauncher.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/flatpak/org.prismlauncher.PrismLauncher.yml b/flatpak/org.prismlauncher.PrismLauncher.yml index fca306d7..071772c6 100644 --- a/flatpak/org.prismlauncher.PrismLauncher.yml +++ b/flatpak/org.prismlauncher.PrismLauncher.yml @@ -39,6 +39,7 @@ modules: sources: - type: dir path: ../ + builddir: true - name: openjdk buildsystem: simple build-commands: From 807da6a0358c99cea907b51fb389654c969e27da Mon Sep 17 00:00:00 2001 From: Joshua Goins Date: Sat, 7 Jan 2023 15:38:16 -0500 Subject: [PATCH 081/152] fix: Remove extra line breaks for modrinth descriptions Signed-off-by: Joshua Goins --- launcher/modplatform/modrinth/ModrinthPackIndex.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/launcher/modplatform/modrinth/ModrinthPackIndex.cpp b/launcher/modplatform/modrinth/ModrinthPackIndex.cpp index ae45e096..aec45a73 100644 --- a/launcher/modplatform/modrinth/ModrinthPackIndex.cpp +++ b/launcher/modplatform/modrinth/ModrinthPackIndex.cpp @@ -87,7 +87,7 @@ void Modrinth::loadExtraPackData(ModPlatform::IndexedPack& pack, QJsonObject& ob pack.extraData.donate.append(donate); } - pack.extraData.body = Json::ensureString(obj, "body"); + pack.extraData.body = Json::ensureString(obj, "body").remove("
    "); pack.extraDataLoaded = true; } From ff7878217d6a5bab7cd688bb2051ef212c8b6117 Mon Sep 17 00:00:00 2001 From: DioEgizio <83089242+DioEgizio@users.noreply.github.com> Date: Wed, 11 Jan 2023 10:16:28 +0100 Subject: [PATCH 082/152] fix: add cmark:p to mingw build this way we can just dynamically link it on that build instead of building it ourselves and statically linking it Signed-off-by: DioEgizio <83089242+DioEgizio@users.noreply.github.com> --- .github/workflows/build.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b6e179e1..050adb87 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -135,6 +135,7 @@ jobs: quazip-qt6:p ccache:p qt6-5compat:p + cmark:p - name: Force newer ccache if: runner.os == 'Windows' && matrix.msystem == '' && inputs.build_type == 'Debug' From 80eea05deb44ec0f156476f51af88daebd3e5d25 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 12 Jan 2023 22:06:50 +0000 Subject: [PATCH 083/152] chore(deps): update hendrikmuhs/ccache-action action to v1.2.7 --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d074863d..77e934e1 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -144,7 +144,7 @@ jobs: - name: Setup ccache if: (runner.os != 'Windows' || matrix.msystem == '') && inputs.build_type == 'Debug' - uses: hendrikmuhs/ccache-action@v1.2.6 + uses: hendrikmuhs/ccache-action@v1.2.7 with: key: ${{ matrix.os }}-qt${{ matrix.qt_ver }}-${{ matrix.architecture }} From 160dd09fc2788fea17c8e9e332c2877586640971 Mon Sep 17 00:00:00 2001 From: Aaron <10217842+byteduck@users.noreply.github.com> Date: Thu, 12 Jan 2023 20:03:31 -0800 Subject: [PATCH 084/152] Fix instance account selector face for offline accounts --- .../pages/instance/InstanceSettingsPage.cpp | 26 +++++++++---------- .../ui/pages/instance/InstanceSettingsPage.h | 1 + 2 files changed, 13 insertions(+), 14 deletions(-) diff --git a/launcher/ui/pages/instance/InstanceSettingsPage.cpp b/launcher/ui/pages/instance/InstanceSettingsPage.cpp index 24b261ba..4b4c73dc 100644 --- a/launcher/ui/pages/instance/InstanceSettingsPage.cpp +++ b/launcher/ui/pages/instance/InstanceSettingsPage.cpp @@ -466,7 +466,7 @@ void InstanceSettingsPage::updateAccountsMenu() if (defaultAccount) { ui->instanceAccountSelector->setText(defaultAccount->profileName()); - ui->instanceAccountSelector->setIcon(defaultAccount->getFace()); + ui->instanceAccountSelector->setIcon(getFaceForAccount(defaultAccount)); } else { ui->instanceAccountSelector->setText(tr("No default account")); ui->instanceAccountSelector->setIcon(APPLICATION->getThemedIcon("noaccount")); @@ -480,19 +480,21 @@ void InstanceSettingsPage::updateAccountsMenu() if (accountIndex == i) { action->setChecked(true); } - - auto face = account->getFace(); - if (!face.isNull()) { - action->setIcon(face); - } else { - action->setIcon(APPLICATION->getThemedIcon("noaccount")); - } - + action->setIcon(getFaceForAccount(account)); accountMenu->addAction(action); connect(action, SIGNAL(triggered(bool)), this, SLOT(changeInstanceAccount())); } } +QIcon InstanceSettingsPage::getFaceForAccount(MinecraftAccountPtr account) +{ + if (auto face = account->getFace(); !face.isNull()) { + return face; + } + + return APPLICATION->getThemedIcon("noaccount"); +} + void InstanceSettingsPage::changeInstanceAccount() { QAction* sAction = (QAction*)sender(); @@ -506,11 +508,7 @@ void InstanceSettingsPage::changeInstanceAccount() m_settings->set("InstanceAccountId", account->profileId()); ui->instanceAccountSelector->setText(account->profileName()); - if (auto face = account->getFace(); !face.isNull()) { - ui->instanceAccountSelector->setIcon(face); - } else { - ui->instanceAccountSelector->setIcon(APPLICATION->getThemedIcon("noaccount")); - } + ui->instanceAccountSelector->setIcon(getFaceForAccount(account)); } void InstanceSettingsPage::on_maxMemSpinBox_valueChanged(int i) diff --git a/launcher/ui/pages/instance/InstanceSettingsPage.h b/launcher/ui/pages/instance/InstanceSettingsPage.h index b80db99a..cb6fbae0 100644 --- a/launcher/ui/pages/instance/InstanceSettingsPage.h +++ b/launcher/ui/pages/instance/InstanceSettingsPage.h @@ -94,6 +94,7 @@ private slots: void globalSettingsButtonClicked(bool checked); void updateAccountsMenu(); + QIcon getFaceForAccount(MinecraftAccountPtr account); void changeInstanceAccount(); private: From 4e80d1fc79fcd2181dcf5975553f39a6895f4635 Mon Sep 17 00:00:00 2001 From: Aaron <10217842+byteduck@users.noreply.github.com> Date: Thu, 12 Jan 2023 20:16:48 -0800 Subject: [PATCH 085/152] DCO Remediation Commit for Aaron <10217842+byteduck@users.noreply.github.com> I, Aaron <10217842+byteduck@users.noreply.github.com>, hereby add my Signed-off-by to this commit: 160dd09fc2788fea17c8e9e332c2877586640971 Signed-off-by: Aaron <10217842+byteduck@users.noreply.github.com> From 6a1807995390b2a2cbe074ee1f47d3791e0e3f10 Mon Sep 17 00:00:00 2001 From: flow Date: Fri, 25 Nov 2022 09:23:46 -0300 Subject: [PATCH 086/152] refactor: generalize mod models and APIs to resources Firstly, this abstract away behavior in the mod download models that can also be applied to other types of resources into a superclass, allowing other resource types to be implemented without so much code duplication. For that, this also generalizes the APIs used (currently, ModrinthAPI and FlameAPI) to be able to make requests to other types of resources. It also does a general cleanup of both of those. In particular, this makes use of std::optional instead of invalid values for errors and, well, optional values :p This is a squash of some commits that were becoming too interlaced together to be cleanly separated. Signed-off-by: flow --- launcher/CMakeLists.txt | 37 +- launcher/ModDownloadTask.cpp | 72 ---- launcher/ResourceDownloadTask.cpp | 80 ++++ ...dDownloadTask.h => ResourceDownloadTask.h} | 16 +- launcher/minecraft/PackProfile.cpp | 28 +- launcher/minecraft/PackProfile.h | 4 +- launcher/modplatform/CheckUpdateTask.h | 14 +- launcher/modplatform/EnsureMetadataTask.cpp | 14 +- launcher/modplatform/EnsureMetadataTask.h | 6 +- launcher/modplatform/ModAPI.h | 118 ------ launcher/modplatform/ModIndex.cpp | 24 +- launcher/modplatform/ModIndex.h | 19 +- launcher/modplatform/ResourceAPI.h | 149 ++++++++ launcher/modplatform/flame/FlameAPI.cpp | 16 +- launcher/modplatform/flame/FlameAPI.h | 99 ++--- .../modplatform/flame/FlameCheckUpdate.cpp | 11 +- launcher/modplatform/flame/FlameCheckUpdate.h | 2 +- .../flame/FlameInstanceCreationTask.cpp | 4 +- .../flame/FlameInstanceCreationTask.h | 2 +- launcher/modplatform/flame/FlameModIndex.cpp | 4 +- launcher/modplatform/helpers/HashUtils.cpp | 16 +- launcher/modplatform/helpers/HashUtils.h | 10 +- .../modplatform/helpers/NetworkModAPI.cpp | 97 ----- launcher/modplatform/helpers/NetworkModAPI.h | 17 - .../helpers/NetworkResourceAPI.cpp | 124 ++++++ .../modplatform/helpers/NetworkResourceAPI.h | 18 + launcher/modplatform/modrinth/ModrinthAPI.cpp | 36 +- launcher/modplatform/modrinth/ModrinthAPI.h | 106 ++++-- .../modrinth/ModrinthCheckUpdate.cpp | 25 +- .../modrinth/ModrinthCheckUpdate.h | 2 +- .../modrinth/ModrinthPackIndex.cpp | 4 +- launcher/modplatform/packwiz/Packwiz.cpp | 8 +- launcher/modplatform/packwiz/Packwiz.h | 2 +- launcher/net/NetAction.h | 4 - launcher/net/NetJob.cpp | 5 +- launcher/ui/dialogs/BlockedModsDialog.cpp | 2 +- launcher/ui/dialogs/ChooseProviderDialog.cpp | 6 +- launcher/ui/dialogs/ChooseProviderDialog.h | 6 +- launcher/ui/dialogs/ModDownloadDialog.cpp | 171 +-------- launcher/ui/dialogs/ModDownloadDialog.h | 43 +-- launcher/ui/dialogs/ModUpdateDialog.cpp | 44 ++- launcher/ui/dialogs/ModUpdateDialog.h | 8 +- .../ui/dialogs/ResourceDownloadDialog.cpp | 152 ++++++++ launcher/ui/dialogs/ResourceDownloadDialog.h | 55 +++ launcher/ui/dialogs/ReviewMessageBox.cpp | 4 +- launcher/ui/dialogs/ReviewMessageBox.h | 8 +- launcher/ui/pages/instance/ModFolderPage.cpp | 6 +- launcher/ui/pages/instance/ResourcePackPage.h | 1 + launcher/ui/pages/modplatform/ModModel.cpp | 290 +++----------- launcher/ui/pages/modplatform/ModModel.h | 64 +--- launcher/ui/pages/modplatform/ModPage.cpp | 359 ++---------------- launcher/ui/pages/modplatform/ModPage.h | 78 +--- .../ui/pages/modplatform/ResourceModel.cpp | 258 +++++++++++++ launcher/ui/pages/modplatform/ResourceModel.h | 101 +++++ .../ui/pages/modplatform/ResourcePage.cpp | 347 +++++++++++++++++ launcher/ui/pages/modplatform/ResourcePage.h | 95 +++++ .../{ModPage.ui => ResourcePage.ui} | 12 +- ...meModModel.cpp => FlameResourceModels.cpp} | 4 +- ...{FlameModModel.h => FlameResourceModels.h} | 4 +- ...lameModPage.cpp => FlameResourcePages.cpp} | 38 +- .../{FlameModPage.h => FlameResourcePages.h} | 13 +- ...odModel.cpp => ModrinthResourceModels.cpp} | 9 +- ...nthModModel.h => ModrinthResourceModels.h} | 9 +- ...hModPage.cpp => ModrinthResourcePages.cpp} | 57 +-- ...rinthModPage.h => ModrinthResourcePages.h} | 32 +- launcher/ui/widgets/ProgressWidget.cpp | 6 +- launcher/ui/widgets/ProgressWidget.h | 6 +- tests/Packwiz_test.cpp | 4 +- 68 files changed, 1965 insertions(+), 1520 deletions(-) delete mode 100644 launcher/ModDownloadTask.cpp create mode 100644 launcher/ResourceDownloadTask.cpp rename launcher/{ModDownloadTask.h => ResourceDownloadTask.h} (70%) delete mode 100644 launcher/modplatform/ModAPI.h create mode 100644 launcher/modplatform/ResourceAPI.h delete mode 100644 launcher/modplatform/helpers/NetworkModAPI.cpp delete mode 100644 launcher/modplatform/helpers/NetworkModAPI.h create mode 100644 launcher/modplatform/helpers/NetworkResourceAPI.cpp create mode 100644 launcher/modplatform/helpers/NetworkResourceAPI.h create mode 100644 launcher/ui/dialogs/ResourceDownloadDialog.cpp create mode 100644 launcher/ui/dialogs/ResourceDownloadDialog.h create mode 100644 launcher/ui/pages/modplatform/ResourceModel.cpp create mode 100644 launcher/ui/pages/modplatform/ResourceModel.h create mode 100644 launcher/ui/pages/modplatform/ResourcePage.cpp create mode 100644 launcher/ui/pages/modplatform/ResourcePage.h rename launcher/ui/pages/modplatform/{ModPage.ui => ResourcePage.ui} (90%) rename launcher/ui/pages/modplatform/flame/{FlameModModel.cpp => FlameResourceModels.cpp} (92%) rename launcher/ui/pages/modplatform/flame/{FlameModModel.h => FlameResourceModels.h} (92%) rename launcher/ui/pages/modplatform/flame/{FlameModPage.cpp => FlameResourcePages.cpp} (71%) rename launcher/ui/pages/modplatform/flame/{FlameModPage.h => FlameResourcePages.h} (91%) rename launcher/ui/pages/modplatform/modrinth/{ModrinthModModel.cpp => ModrinthResourceModels.cpp} (88%) rename launcher/ui/pages/modplatform/modrinth/{ModrinthModModel.h => ModrinthResourceModels.h} (88%) rename launcher/ui/pages/modplatform/modrinth/{ModrinthModPage.cpp => ModrinthResourcePages.cpp} (61%) rename launcher/ui/pages/modplatform/modrinth/{ModrinthModPage.h => ModrinthResourcePages.h} (64%) diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index eec6c787..a1a68f5b 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -38,9 +38,9 @@ set(CORE_SOURCES InstanceImportTask.h InstanceImportTask.cpp - # Mod downloading task - ModDownloadTask.h - ModDownloadTask.cpp + # Resource downloading task + ResourceDownloadTask.h + ResourceDownloadTask.cpp # Use tracking separate from memory management Usable.h @@ -473,7 +473,7 @@ set(API_SOURCES modplatform/ModIndex.h modplatform/ModIndex.cpp - modplatform/ModAPI.h + modplatform/ResourceAPI.h modplatform/EnsureMetadataTask.h modplatform/EnsureMetadataTask.cpp @@ -484,8 +484,8 @@ set(API_SOURCES modplatform/flame/FlameAPI.cpp modplatform/modrinth/ModrinthAPI.h modplatform/modrinth/ModrinthAPI.cpp - modplatform/helpers/NetworkModAPI.h - modplatform/helpers/NetworkModAPI.cpp + modplatform/helpers/NetworkResourceAPI.h + modplatform/helpers/NetworkResourceAPI.cpp modplatform/helpers/HashUtils.h modplatform/helpers/HashUtils.cpp modplatform/helpers/OverrideUtils.h @@ -771,6 +771,11 @@ SET(LAUNCHER_SOURCES ui/pages/modplatform/VanillaPage.cpp ui/pages/modplatform/VanillaPage.h + ui/pages/modplatform/ResourcePage.cpp + ui/pages/modplatform/ResourcePage.h + ui/pages/modplatform/ResourceModel.cpp + ui/pages/modplatform/ResourceModel.h + ui/pages/modplatform/ModPage.cpp ui/pages/modplatform/ModPage.h ui/pages/modplatform/ModModel.cpp @@ -803,10 +808,10 @@ SET(LAUNCHER_SOURCES ui/pages/modplatform/flame/FlameModel.h ui/pages/modplatform/flame/FlamePage.cpp ui/pages/modplatform/flame/FlamePage.h - ui/pages/modplatform/flame/FlameModModel.cpp - ui/pages/modplatform/flame/FlameModModel.h - ui/pages/modplatform/flame/FlameModPage.cpp - ui/pages/modplatform/flame/FlameModPage.h + ui/pages/modplatform/flame/FlameResourceModels.cpp + ui/pages/modplatform/flame/FlameResourceModels.h + ui/pages/modplatform/flame/FlameResourcePages.cpp + ui/pages/modplatform/flame/FlameResourcePages.h ui/pages/modplatform/modrinth/ModrinthPage.cpp ui/pages/modplatform/modrinth/ModrinthPage.h @@ -821,10 +826,10 @@ SET(LAUNCHER_SOURCES ui/pages/modplatform/ImportPage.cpp ui/pages/modplatform/ImportPage.h - ui/pages/modplatform/modrinth/ModrinthModModel.cpp - ui/pages/modplatform/modrinth/ModrinthModModel.h - ui/pages/modplatform/modrinth/ModrinthModPage.cpp - ui/pages/modplatform/modrinth/ModrinthModPage.h + ui/pages/modplatform/modrinth/ModrinthResourceModels.cpp + ui/pages/modplatform/modrinth/ModrinthResourceModels.h + ui/pages/modplatform/modrinth/ModrinthResourcePages.cpp + ui/pages/modplatform/modrinth/ModrinthResourcePages.h # GUI - dialogs ui/dialogs/AboutDialog.cpp @@ -869,6 +874,8 @@ SET(LAUNCHER_SOURCES ui/dialogs/VersionSelectDialog.h ui/dialogs/SkinUploadDialog.cpp ui/dialogs/SkinUploadDialog.h + ui/dialogs/ResourceDownloadDialog.cpp + ui/dialogs/ResourceDownloadDialog.h ui/dialogs/ModDownloadDialog.cpp ui/dialogs/ModDownloadDialog.h ui/dialogs/ScrollMessageBox.cpp @@ -965,7 +972,7 @@ qt_wrap_ui(LAUNCHER_UI ui/pages/modplatform/atlauncher/AtlOptionalModDialog.ui ui/pages/modplatform/atlauncher/AtlPage.ui ui/pages/modplatform/VanillaPage.ui - ui/pages/modplatform/ModPage.ui + ui/pages/modplatform/ResourcePage.ui ui/pages/modplatform/flame/FlamePage.ui ui/pages/modplatform/legacy_ftb/Page.ui ui/pages/modplatform/ImportPage.ui diff --git a/launcher/ModDownloadTask.cpp b/launcher/ModDownloadTask.cpp deleted file mode 100644 index 2b0343f4..00000000 --- a/launcher/ModDownloadTask.cpp +++ /dev/null @@ -1,72 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-only -/* -* PolyMC - Minecraft Launcher -* Copyright (c) 2022 flowln -* 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 . -*/ - -#include "ModDownloadTask.h" - -#include "Application.h" -#include "minecraft/mod/ModFolderModel.h" - -ModDownloadTask::ModDownloadTask(ModPlatform::IndexedPack mod, ModPlatform::IndexedVersion version, const std::shared_ptr mods, bool is_indexed) - : m_mod(mod), m_mod_version(version), mods(mods) -{ - if (is_indexed) { - m_update_task.reset(new LocalModUpdateTask(mods->indexDir(), m_mod, m_mod_version)); - connect(m_update_task.get(), &LocalModUpdateTask::hasOldMod, this, &ModDownloadTask::hasOldMod); - - addTask(m_update_task); - } - - m_filesNetJob.reset(new NetJob(tr("Mod download"), APPLICATION->network())); - m_filesNetJob->setStatus(tr("Downloading mod:\n%1").arg(m_mod_version.downloadUrl)); - - m_filesNetJob->addNetAction(Net::Download::makeFile(m_mod_version.downloadUrl, mods->dir().absoluteFilePath(getFilename()))); - connect(m_filesNetJob.get(), &NetJob::succeeded, this, &ModDownloadTask::downloadSucceeded); - connect(m_filesNetJob.get(), &NetJob::progress, this, &ModDownloadTask::downloadProgressChanged); - connect(m_filesNetJob.get(), &NetJob::failed, this, &ModDownloadTask::downloadFailed); - - addTask(m_filesNetJob); -} - -void ModDownloadTask::downloadSucceeded() -{ - m_filesNetJob.reset(); - auto name = std::get<0>(to_delete); - auto filename = std::get<1>(to_delete); - if (!name.isEmpty() && filename != m_mod_version.fileName) { - mods->uninstallMod(filename, true); - } -} - -void ModDownloadTask::downloadFailed(QString reason) -{ - emitFailed(reason); - m_filesNetJob.reset(); -} - -void ModDownloadTask::downloadProgressChanged(qint64 current, qint64 total) -{ - emit progress(current, total); -} - -// This indirection is done so that we don't delete a mod before being sure it was -// downloaded successfully! -void ModDownloadTask::hasOldMod(QString name, QString filename) -{ - to_delete = {name, filename}; -} diff --git a/launcher/ResourceDownloadTask.cpp b/launcher/ResourceDownloadTask.cpp new file mode 100644 index 00000000..687eaf51 --- /dev/null +++ b/launcher/ResourceDownloadTask.cpp @@ -0,0 +1,80 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* +* PolyMC - Minecraft Launcher +* Copyright (c) 2022 flowln +* 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 . +*/ + +#include "ResourceDownloadTask.h" + +#include "Application.h" + +#include "minecraft/mod/ModFolderModel.h" +#include "minecraft/mod/ResourceFolderModel.h" + +ResourceDownloadTask::ResourceDownloadTask(ModPlatform::IndexedPack pack, + ModPlatform::IndexedVersion version, + const std::shared_ptr packs, + bool is_indexed) + : m_pack(std::move(pack)), m_pack_version(std::move(version)), m_pack_model(packs) +{ + if (auto model = dynamic_cast(m_pack_model.get()); model && is_indexed) { + m_update_task.reset(new LocalModUpdateTask(model->indexDir(), m_pack, m_pack_version)); + connect(m_update_task.get(), &LocalModUpdateTask::hasOldMod, this, &ResourceDownloadTask::hasOldResource); + + addTask(m_update_task); + } + + m_filesNetJob.reset(new NetJob(tr("Resource download"), APPLICATION->network())); + m_filesNetJob->setStatus(tr("Downloading resource:\n%1").arg(m_pack_version.downloadUrl)); + + m_filesNetJob->addNetAction(Net::Download::makeFile(m_pack_version.downloadUrl, m_pack_model->dir().absoluteFilePath(getFilename()))); + connect(m_filesNetJob.get(), &NetJob::succeeded, this, &ResourceDownloadTask::downloadSucceeded); + connect(m_filesNetJob.get(), &NetJob::progress, this, &ResourceDownloadTask::downloadProgressChanged); + connect(m_filesNetJob.get(), &NetJob::failed, this, &ResourceDownloadTask::downloadFailed); + + addTask(m_filesNetJob); +} + +void ResourceDownloadTask::downloadSucceeded() +{ + m_filesNetJob.reset(); + auto name = std::get<0>(to_delete); + auto filename = std::get<1>(to_delete); + if (!name.isEmpty() && filename != m_pack_version.fileName) { + if (auto model = dynamic_cast(m_pack_model.get()); model) + model->uninstallMod(filename, true); + else + m_pack_model->uninstallResource(filename); + } +} + +void ResourceDownloadTask::downloadFailed(QString reason) +{ + emitFailed(reason); + m_filesNetJob.reset(); +} + +void ResourceDownloadTask::downloadProgressChanged(qint64 current, qint64 total) +{ + emit progress(current, total); +} + +// This indirection is done so that we don't delete a mod before being sure it was +// downloaded successfully! +void ResourceDownloadTask::hasOldResource(QString name, QString filename) +{ + to_delete = { name, filename }; +} diff --git a/launcher/ModDownloadTask.h b/launcher/ResourceDownloadTask.h similarity index 70% rename from launcher/ModDownloadTask.h rename to launcher/ResourceDownloadTask.h index 95020470..350c2edd 100644 --- a/launcher/ModDownloadTask.h +++ b/launcher/ResourceDownloadTask.h @@ -25,18 +25,18 @@ #include "modplatform/ModIndex.h" #include "minecraft/mod/tasks/LocalModUpdateTask.h" -class ModFolderModel; +class ResourceFolderModel; -class ModDownloadTask : public SequentialTask { +class ResourceDownloadTask : public SequentialTask { Q_OBJECT public: - explicit ModDownloadTask(ModPlatform::IndexedPack mod, ModPlatform::IndexedVersion version, const std::shared_ptr mods, bool is_indexed = true); - const QString& getFilename() const { return m_mod_version.fileName; } + explicit ResourceDownloadTask(ModPlatform::IndexedPack pack, ModPlatform::IndexedVersion version, const std::shared_ptr packs, bool is_indexed = true); + const QString& getFilename() const { return m_pack_version.fileName; } private: - ModPlatform::IndexedPack m_mod; - ModPlatform::IndexedVersion m_mod_version; - const std::shared_ptr mods; + ModPlatform::IndexedPack m_pack; + ModPlatform::IndexedVersion m_pack_version; + const std::shared_ptr m_pack_model; NetJob::Ptr m_filesNetJob; LocalModUpdateTask::Ptr m_update_task; @@ -50,7 +50,7 @@ private: std::tuple to_delete {"", ""}; private slots: - void hasOldMod(QString name, QString filename); + void hasOldResource(QString name, QString filename); }; diff --git a/launcher/minecraft/PackProfile.cpp b/launcher/minecraft/PackProfile.cpp index 43fa3f8d..42021b3c 100644 --- a/launcher/minecraft/PackProfile.cpp +++ b/launcher/minecraft/PackProfile.cpp @@ -55,12 +55,13 @@ #include "PackProfile_p.h" #include "ComponentUpdateTask.h" -#include "modplatform/ModAPI.h" +#include "Application.h" +#include "modplatform/ResourceAPI.h" -static const QMap modloaderMapping{ - {"net.minecraftforge", ModAPI::Forge}, - {"net.fabricmc.fabric-loader", ModAPI::Fabric}, - {"org.quiltmc.quilt-loader", ModAPI::Quilt} +static const QMap modloaderMapping{ + {"net.minecraftforge", ResourceAPI::Forge}, + {"net.fabricmc.fabric-loader", ResourceAPI::Fabric}, + {"org.quiltmc.quilt-loader", ResourceAPI::Quilt} }; PackProfile::PackProfile(MinecraftInstance * instance) @@ -1066,19 +1067,22 @@ void PackProfile::disableInteraction(bool disable) } } -ModAPI::ModLoaderTypes PackProfile::getModLoaders() +std::optional PackProfile::getModLoaders() { - ModAPI::ModLoaderTypes result = ModAPI::Unspecified; + ResourceAPI::ModLoaderTypes result; + bool has_any_loader = false; - QMapIterator i(modloaderMapping); + QMapIterator i(modloaderMapping); - while (i.hasNext()) - { + while (i.hasNext()) { i.next(); - Component* c = getComponent(i.key()); - if (c != nullptr && c->isEnabled()) { + if (auto c = getComponent(i.key()); c != nullptr && c->isEnabled()) { result |= i.value(); + has_any_loader = true; } } + + if (!has_any_loader) + return {}; return result; } diff --git a/launcher/minecraft/PackProfile.h b/launcher/minecraft/PackProfile.h index 2330cca1..67b418f4 100644 --- a/launcher/minecraft/PackProfile.h +++ b/launcher/minecraft/PackProfile.h @@ -49,7 +49,7 @@ #include "BaseVersion.h" #include "MojangDownloadInfo.h" #include "net/Mode.h" -#include "modplatform/ModAPI.h" +#include "modplatform/ResourceAPI.h" class MinecraftInstance; struct PackProfileData; @@ -145,7 +145,7 @@ public: // todo(merged): is this the best approach void appendComponent(ComponentPtr component); - ModAPI::ModLoaderTypes getModLoaders(); + std::optional getModLoaders(); private: void scheduleSave(); diff --git a/launcher/modplatform/CheckUpdateTask.h b/launcher/modplatform/CheckUpdateTask.h index 91922034..932a62d9 100644 --- a/launcher/modplatform/CheckUpdateTask.h +++ b/launcher/modplatform/CheckUpdateTask.h @@ -1,18 +1,18 @@ #pragma once #include "minecraft/mod/Mod.h" -#include "modplatform/ModAPI.h" +#include "modplatform/ResourceAPI.h" #include "modplatform/ModIndex.h" #include "tasks/Task.h" -class ModDownloadTask; +class ResourceDownloadTask; class ModFolderModel; class CheckUpdateTask : public Task { Q_OBJECT public: - CheckUpdateTask(QList& mods, std::list& mcVersions, ModAPI::ModLoaderTypes loaders, std::shared_ptr mods_folder) + CheckUpdateTask(QList& mods, 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) {}; struct UpdatableMod { @@ -21,11 +21,11 @@ class CheckUpdateTask : public Task { QString old_version; QString new_version; QString changelog; - ModPlatform::Provider provider; - ModDownloadTask* download; + ModPlatform::ResourceProvider provider; + ResourceDownloadTask* download; public: - UpdatableMod(QString name, QString old_h, QString old_v, QString new_v, QString changelog, ModPlatform::Provider p, ModDownloadTask* t) + UpdatableMod(QString name, QString old_h, QString old_v, QString new_v, QString changelog, ModPlatform::ResourceProvider p, ResourceDownloadTask* t) : name(name), old_hash(old_h), old_version(old_v), new_version(new_v), changelog(changelog), provider(p), download(t) {} }; @@ -44,7 +44,7 @@ class CheckUpdateTask : public Task { protected: QList& m_mods; std::list& m_game_versions; - ModAPI::ModLoaderTypes m_loaders; + std::optional m_loaders; std::shared_ptr m_mods_folder; std::vector m_updatable; diff --git a/launcher/modplatform/EnsureMetadataTask.cpp b/launcher/modplatform/EnsureMetadataTask.cpp index 234330a7..9bf81338 100644 --- a/launcher/modplatform/EnsureMetadataTask.cpp +++ b/launcher/modplatform/EnsureMetadataTask.cpp @@ -20,7 +20,7 @@ static ModPlatform::ProviderCapabilities ProviderCaps; static ModrinthAPI modrinth_api; static FlameAPI flame_api; -EnsureMetadataTask::EnsureMetadataTask(Mod* mod, QDir dir, ModPlatform::Provider prov) +EnsureMetadataTask::EnsureMetadataTask(Mod* mod, QDir dir, ModPlatform::ResourceProvider prov) : Task(nullptr), m_index_dir(dir), m_provider(prov), m_hashing_task(nullptr), m_current_task(nullptr) { auto hash_task = createNewHash(mod); @@ -31,7 +31,7 @@ EnsureMetadataTask::EnsureMetadataTask(Mod* mod, QDir dir, ModPlatform::Provider hash_task->start(); } -EnsureMetadataTask::EnsureMetadataTask(QList& mods, QDir dir, ModPlatform::Provider prov) +EnsureMetadataTask::EnsureMetadataTask(QList& mods, QDir dir, ModPlatform::ResourceProvider prov) : Task(nullptr), m_index_dir(dir), m_provider(prov), m_current_task(nullptr) { m_hashing_task = new ConcurrentTask(this, "MakeHashesTask", 10); @@ -110,10 +110,10 @@ void EnsureMetadataTask::executeTask() NetJob::Ptr version_task; switch (m_provider) { - case (ModPlatform::Provider::MODRINTH): + case (ModPlatform::ResourceProvider::MODRINTH): version_task = modrinthVersionsTask(); break; - case (ModPlatform::Provider::FLAME): + case (ModPlatform::ResourceProvider::FLAME): version_task = flameVersionsTask(); break; } @@ -130,10 +130,10 @@ void EnsureMetadataTask::executeTask() NetJob::Ptr project_task; switch (m_provider) { - case (ModPlatform::Provider::MODRINTH): + case (ModPlatform::ResourceProvider::MODRINTH): project_task = modrinthProjectsTask(); break; - case (ModPlatform::Provider::FLAME): + case (ModPlatform::ResourceProvider::FLAME): project_task = flameProjectsTask(); break; } @@ -212,7 +212,7 @@ void EnsureMetadataTask::emitFail(Mod* m, QString key, RemoveFromList remove) NetJob::Ptr EnsureMetadataTask::modrinthVersionsTask() { - auto hash_type = ProviderCaps.hashType(ModPlatform::Provider::MODRINTH).first(); + auto hash_type = ProviderCaps.hashType(ModPlatform::ResourceProvider::MODRINTH).first(); auto* response = new QByteArray(); auto ver_task = modrinth_api.currentVersions(m_mods.keys(), hash_type, response); diff --git a/launcher/modplatform/EnsureMetadataTask.h b/launcher/modplatform/EnsureMetadataTask.h index a8b0851e..a79e5861 100644 --- a/launcher/modplatform/EnsureMetadataTask.h +++ b/launcher/modplatform/EnsureMetadataTask.h @@ -14,8 +14,8 @@ class EnsureMetadataTask : public Task { Q_OBJECT public: - EnsureMetadataTask(Mod*, QDir, ModPlatform::Provider = ModPlatform::Provider::MODRINTH); - EnsureMetadataTask(QList&, QDir, ModPlatform::Provider = ModPlatform::Provider::MODRINTH); + EnsureMetadataTask(Mod*, QDir, ModPlatform::ResourceProvider = ModPlatform::ResourceProvider::MODRINTH); + EnsureMetadataTask(QList&, QDir, ModPlatform::ResourceProvider = ModPlatform::ResourceProvider::MODRINTH); ~EnsureMetadataTask() = default; @@ -57,7 +57,7 @@ class EnsureMetadataTask : public Task { private: QHash m_mods; QDir m_index_dir; - ModPlatform::Provider m_provider; + ModPlatform::ResourceProvider m_provider; QHash m_temp_versions; ConcurrentTask* m_hashing_task; diff --git a/launcher/modplatform/ModAPI.h b/launcher/modplatform/ModAPI.h deleted file mode 100644 index 703de143..00000000 --- a/launcher/modplatform/ModAPI.h +++ /dev/null @@ -1,118 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-only -/* - * PolyMC - 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. - */ - -#pragma once - -#include -#include -#include - -#include "../Version.h" -#include "net/NetJob.h" - -namespace ModPlatform { -class ListModel; -struct IndexedPack; -} - -class ModAPI { - protected: - using CallerType = ModPlatform::ListModel; - - public: - virtual ~ModAPI() = default; - - enum ModLoaderType { - Unspecified = 0, - Forge = 1 << 0, - Cauldron = 1 << 1, - LiteLoader = 1 << 2, - Fabric = 1 << 3, - Quilt = 1 << 4 - }; - Q_DECLARE_FLAGS(ModLoaderTypes, ModLoaderType) - - struct SearchArgs { - int offset; - QString search; - QString sorting; - ModLoaderTypes loaders; - std::list versions; - }; - - virtual void searchMods(CallerType* caller, SearchArgs&& args) const = 0; - virtual void getModInfo(ModPlatform::IndexedPack& pack, std::function callback) = 0; - - virtual auto getProject(QString addonId, QByteArray* response) const -> NetJob* = 0; - virtual auto getProjects(QStringList addonIds, QByteArray* response) const -> NetJob* = 0; - - - struct VersionSearchArgs { - QString addonId; - std::list mcVersions; - ModLoaderTypes loaders; - }; - - virtual void getVersions(VersionSearchArgs&& args, std::function callback) const = 0; - - static auto getModLoaderString(ModLoaderType type) -> const QString { - switch (type) { - case Unspecified: - break; - case Forge: - return "forge"; - case Cauldron: - return "cauldron"; - case LiteLoader: - return "liteloader"; - case Fabric: - return "fabric"; - case Quilt: - return "quilt"; - } - return ""; - } - - protected: - inline auto getGameVersionsString(std::list mcVersions) const -> QString - { - QString s; - for(auto& ver : mcVersions){ - s += QString("\"%1\",").arg(ver.toString()); - } - s.remove(s.length() - 1, 1); //remove last comma - return s; - } -}; diff --git a/launcher/modplatform/ModIndex.cpp b/launcher/modplatform/ModIndex.cpp index 34fd9f30..6a507caf 100644 --- a/launcher/modplatform/ModIndex.cpp +++ b/launcher/modplatform/ModIndex.cpp @@ -24,47 +24,47 @@ namespace ModPlatform { -auto ProviderCapabilities::name(Provider p) -> const char* +auto ProviderCapabilities::name(ResourceProvider p) -> const char* { switch (p) { - case Provider::MODRINTH: + case ResourceProvider::MODRINTH: return "modrinth"; - case Provider::FLAME: + case ResourceProvider::FLAME: return "curseforge"; } return {}; } -auto ProviderCapabilities::readableName(Provider p) -> QString +auto ProviderCapabilities::readableName(ResourceProvider p) -> QString { switch (p) { - case Provider::MODRINTH: + case ResourceProvider::MODRINTH: return "Modrinth"; - case Provider::FLAME: + case ResourceProvider::FLAME: return "CurseForge"; } return {}; } -auto ProviderCapabilities::hashType(Provider p) -> QStringList +auto ProviderCapabilities::hashType(ResourceProvider p) -> QStringList { switch (p) { - case Provider::MODRINTH: + case ResourceProvider::MODRINTH: return { "sha512", "sha1" }; - case Provider::FLAME: + case ResourceProvider::FLAME: // Try newer formats first, fall back to old format return { "sha1", "md5", "murmur2" }; } return {}; } -auto ProviderCapabilities::hash(Provider p, QIODevice* device, QString type) -> QString +auto ProviderCapabilities::hash(ResourceProvider p, QIODevice* device, QString type) -> QString { QCryptographicHash::Algorithm algo = QCryptographicHash::Sha1; switch (p) { - case Provider::MODRINTH: { + case ResourceProvider::MODRINTH: { algo = (type == "sha1") ? QCryptographicHash::Sha1 : QCryptographicHash::Sha512; break; } - case Provider::FLAME: + case ResourceProvider::FLAME: algo = (type == "sha1") ? QCryptographicHash::Sha1 : QCryptographicHash::Md5; break; } diff --git a/launcher/modplatform/ModIndex.h b/launcher/modplatform/ModIndex.h index 518fed7c..f65a6a4b 100644 --- a/launcher/modplatform/ModIndex.h +++ b/launcher/modplatform/ModIndex.h @@ -28,17 +28,16 @@ class QIODevice; namespace ModPlatform { -enum class Provider { - MODRINTH, - FLAME -}; +enum class ResourceProvider { MODRINTH, FLAME }; + +enum class ResourceType { MOD, RESOURCE_PACK }; class ProviderCapabilities { public: - auto name(Provider) -> const char*; - auto readableName(Provider) -> QString; - auto hashType(Provider) -> QStringList; - auto hash(Provider, QIODevice*, QString type = "") -> QString; + auto name(ResourceProvider) -> const char*; + auto readableName(ResourceProvider) -> QString; + auto hashType(ResourceProvider) -> QStringList; + auto hash(ResourceProvider, QIODevice*, QString type = "") -> QString; }; struct ModpackAuthor { @@ -81,7 +80,7 @@ struct ExtraPackData { struct IndexedPack { QVariant addonId; - Provider provider; + ResourceProvider provider; QString name; QString slug; QString description; @@ -101,4 +100,4 @@ struct IndexedPack { } // namespace ModPlatform Q_DECLARE_METATYPE(ModPlatform::IndexedPack) -Q_DECLARE_METATYPE(ModPlatform::Provider) +Q_DECLARE_METATYPE(ModPlatform::ResourceProvider) diff --git a/launcher/modplatform/ResourceAPI.h b/launcher/modplatform/ResourceAPI.h new file mode 100644 index 00000000..d18a2caa --- /dev/null +++ b/launcher/modplatform/ResourceAPI.h @@ -0,0 +1,149 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - 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. + */ + +#pragma once + +#include +#include + +#include + +#include "../Version.h" + +#include "modplatform/ModIndex.h" +#include "net/NetJob.h" + +/* Simple class with a common interface for interacting with APIs */ +class ResourceAPI { + public: + virtual ~ResourceAPI() = default; + + enum ModLoaderType { Forge = 1 << 0, Cauldron = 1 << 1, LiteLoader = 1 << 2, Fabric = 1 << 3, Quilt = 1 << 4 }; + Q_DECLARE_FLAGS(ModLoaderTypes, ModLoaderType) + + struct SearchArgs { + ModPlatform::ResourceType type{}; + int offset = 0; + + std::optional search; + std::optional sorting; + std::optional loaders; + std::optional > versions; + }; + struct SearchCallbacks { + std::function on_succeed; + std::function on_fail; + std::function on_abort; + }; + + struct VersionSearchArgs { + QString addonId; + + std::optional > mcVersions; + std::optional loaders; + }; + struct VersionSearchCallbacks { + std::function on_succeed; + }; + + struct ProjectInfoArgs { + ModPlatform::IndexedPack& pack; + + void operator=(ProjectInfoArgs other) { pack = other.pack; } + }; + struct ProjectInfoCallbacks { + std::function on_succeed; + }; + + public slots: + [[nodiscard]] virtual NetJob::Ptr searchProjects(SearchArgs&&, SearchCallbacks&&) const + { + qWarning() << "TODO"; + return nullptr; + } + [[nodiscard]] virtual NetJob::Ptr getProject(QString addonId, QByteArray* response) const + { + qWarning() << "TODO"; + return nullptr; + } + [[nodiscard]] virtual NetJob::Ptr getProjects(QStringList addonIds, QByteArray* response) const + { + qWarning() << "TODO"; + return nullptr; + } + + [[nodiscard]] virtual NetJob::Ptr getProjectInfo(ProjectInfoArgs&&, ProjectInfoCallbacks&&) const + { + qWarning() << "TODO"; + return nullptr; + } + [[nodiscard]] virtual NetJob::Ptr getProjectVersions(VersionSearchArgs&&, VersionSearchCallbacks&&) const + { + qWarning() << "TODO"; + return nullptr; + } + + static auto getModLoaderString(ModLoaderType type) -> const QString + { + switch (type) { + case Forge: + return "forge"; + case Cauldron: + return "cauldron"; + case LiteLoader: + return "liteloader"; + case Fabric: + return "fabric"; + case Quilt: + return "quilt"; + default: + break; + } + return ""; + } + + protected: + [[nodiscard]] inline QString debugName() const { return "External resource API"; } + + [[nodiscard]] inline auto getGameVersionsString(std::list mcVersions) const -> QString + { + QString s; + for (auto& ver : mcVersions) { + s += QString("\"%1\",").arg(ver.toString()); + } + s.remove(s.length() - 1, 1); // remove last comma + return s; + } +}; diff --git a/launcher/modplatform/flame/FlameAPI.cpp b/launcher/modplatform/flame/FlameAPI.cpp index 4d71da21..ae401399 100644 --- a/launcher/modplatform/flame/FlameAPI.cpp +++ b/launcher/modplatform/flame/FlameAPI.cpp @@ -106,13 +106,19 @@ auto FlameAPI::getModDescription(int modId) -> QString auto FlameAPI::getLatestVersion(VersionSearchArgs&& args) -> ModPlatform::IndexedVersion { + auto versions_url_optional = getVersionsURL(args); + if (!versions_url_optional.has_value()) + return {}; + + auto versions_url = versions_url_optional.value(); + QEventLoop loop; auto netJob = new NetJob(QString("Flame::GetLatestVersion(%1)").arg(args.addonId), APPLICATION->network()); auto response = new QByteArray(); ModPlatform::IndexedVersion ver; - netJob->addNetAction(Net::Download::makeByteArray(getVersionsURL(args), response)); + netJob->addNetAction(Net::Download::makeByteArray(versions_url, response)); QObject::connect(netJob, &NetJob::succeeded, [response, args, &ver] { QJsonParseError parse_error{}; @@ -161,7 +167,7 @@ auto FlameAPI::getLatestVersion(VersionSearchArgs&& args) -> ModPlatform::Indexe return ver; } -auto FlameAPI::getProjects(QStringList addonIds, QByteArray* response) const -> NetJob* +NetJob::Ptr FlameAPI::getProjects(QStringList addonIds, QByteArray* response) const { auto* netJob = new NetJob(QString("Flame::GetProjects"), APPLICATION->network()); @@ -178,13 +184,13 @@ auto FlameAPI::getProjects(QStringList addonIds, QByteArray* response) const -> netJob->addNetAction(Net::Upload::makeByteArray(QString("https://api.curseforge.com/v1/mods"), response, body_raw)); - QObject::connect(netJob, &NetJob::finished, [response, netJob] { delete response; netJob->deleteLater(); }); + QObject::connect(netJob, &NetJob::finished, [response] { delete response; }); QObject::connect(netJob, &NetJob::failed, [body_raw] { qDebug() << body_raw; }); return netJob; } -auto FlameAPI::getFiles(const QStringList& fileIds, QByteArray* response) const -> NetJob* +NetJob::Ptr FlameAPI::getFiles(const QStringList& fileIds, QByteArray* response) const { auto* netJob = new NetJob(QString("Flame::GetFiles"), APPLICATION->network()); @@ -201,7 +207,7 @@ auto FlameAPI::getFiles(const QStringList& fileIds, QByteArray* response) const netJob->addNetAction(Net::Upload::makeByteArray(QString("https://api.curseforge.com/v1/mods/files"), response, body_raw)); - QObject::connect(netJob, &NetJob::finished, [response, netJob] { delete response; netJob->deleteLater(); }); + QObject::connect(netJob, &NetJob::finished, [response] { delete response; }); QObject::connect(netJob, &NetJob::failed, [body_raw] { qDebug() << body_raw; }); return netJob; diff --git a/launcher/modplatform/flame/FlameAPI.h b/launcher/modplatform/flame/FlameAPI.h index 4c6ca64c..114a2716 100644 --- a/launcher/modplatform/flame/FlameAPI.h +++ b/launcher/modplatform/flame/FlameAPI.h @@ -1,21 +1,21 @@ #pragma once #include "modplatform/ModIndex.h" -#include "modplatform/helpers/NetworkModAPI.h" +#include "modplatform/helpers/NetworkResourceAPI.h" -class FlameAPI : public NetworkModAPI { +class FlameAPI : public NetworkResourceAPI { public: - auto matchFingerprints(const QList& fingerprints, QByteArray* response) -> NetJob::Ptr; auto getModFileChangelog(int modId, int fileId) -> QString; auto getModDescription(int modId) -> QString; auto getLatestVersion(VersionSearchArgs&& args) -> ModPlatform::IndexedVersion; - auto getProjects(QStringList addonIds, QByteArray* response) const -> NetJob* override; - auto getFiles(const QStringList& fileIds, QByteArray* response) const -> NetJob*; + NetJob::Ptr getProjects(QStringList addonIds, QByteArray* response) const override; + NetJob::Ptr matchFingerprints(const QList& fingerprints, QByteArray* response); + NetJob::Ptr getFiles(const QStringList& fileIds, QByteArray* response) const; private: - inline auto getSortFieldInt(QString sortString) const -> int + static int getSortFieldInt(QString const& sortString) { return sortString == "Featured" ? 1 : sortString == "Popularity" ? 2 @@ -28,48 +28,16 @@ class FlameAPI : public NetworkModAPI { : 1; } - private: - inline auto getModSearchURL(SearchArgs& args) const -> QString override + static int getClassId(ModPlatform::ResourceType type) { - auto gameVersionStr = args.versions.size() != 0 ? QString("gameVersion=%1").arg(args.versions.front().toString()) : QString(); + switch (type) { + default: + case ModPlatform::ResourceType::MOD: + return 6; + } + } - return QString( - "https://api.curseforge.com/v1/mods/search?" - "gameId=432&" - "classId=6&" - - "index=%1&" - "pageSize=25&" - "searchFilter=%2&" - "sortField=%3&" - "sortOrder=desc&" - "modLoaderType=%4&" - "%5") - .arg(args.offset) - .arg(args.search) - .arg(getSortFieldInt(args.sorting)) - .arg(getMappedModLoader(args.loaders)) - .arg(gameVersionStr); - }; - - inline auto getModInfoURL(QString& id) const -> QString override - { - return QString("https://api.curseforge.com/v1/mods/%1").arg(id); - }; - - inline auto getVersionsURL(VersionSearchArgs& args) const -> QString override - { - QString gameVersionQuery = args.mcVersions.size() == 1 ? QString("gameVersion=%1&").arg(args.mcVersions.front().toString()) : ""; - QString modLoaderQuery = QString("modLoaderType=%1&").arg(getMappedModLoader(args.loaders)); - - return QString("https://api.curseforge.com/v1/mods/%1/files?pageSize=10000&%2%3") - .arg(args.addonId) - .arg(gameVersionQuery) - .arg(modLoaderQuery); - }; - - public: - static auto getMappedModLoader(const ModLoaderTypes loaders) -> int + static int getMappedModLoader(ModLoaderTypes loaders) { // https://docs.curseforge.com/?http#tocS_ModLoaderType if (loaders & Forge) @@ -81,4 +49,43 @@ class FlameAPI : public NetworkModAPI { return 4; // Quilt would probably be 5 return 0; } + + private: + [[nodiscard]] std::optional getSearchURL(SearchArgs const& args) const override + { + auto gameVersionStr = args.versions.has_value() ? QString("gameVersion=%1").arg(args.versions.value().front().toString()) : QString(); + + QStringList get_arguments; + get_arguments.append(QString("classId=%1").arg(getClassId(args.type))); + get_arguments.append(QString("index=%1").arg(args.offset)); + get_arguments.append("pageSize=25"); + if (args.search.has_value()) + get_arguments.append(QString("searchFilter=%1").arg(args.search.value())); + if (args.sorting.has_value()) + get_arguments.append(QString("sortField=%1").arg(getSortFieldInt(args.sorting.value()))); + get_arguments.append("sortOrder=desc"); + if (args.loaders.has_value()) + get_arguments.append(QString("modLoaderType=%1").arg(getMappedModLoader(args.loaders.value()))); + get_arguments.append(gameVersionStr); + + return "https://api.curseforge.com/v1/mods/search?gameId=432&" + get_arguments.join('&'); + }; + + [[nodiscard]] std::optional getInfoURL(QString const& id) const override + { + return QString("https://api.curseforge.com/v1/mods/%1").arg(id); + }; + + [[nodiscard]] std::optional getVersionsURL(VersionSearchArgs const& args) const override + { + QString url{QString("https://api.curseforge.com/v1/mods/%1/files?pageSize=10000&").arg(args.addonId)}; + + QStringList get_parameters; + if (args.mcVersions.has_value()) + get_parameters.append(QString("gameVersion=%1").arg(args.mcVersions.value().front().toString())); + if (args.loaders.has_value()) + get_parameters.append(QString("modLoaderType=%1").arg(getMappedModLoader(args.loaders.value()))); + + return url + get_parameters.join('&'); + }; }; diff --git a/launcher/modplatform/flame/FlameCheckUpdate.cpp b/launcher/modplatform/flame/FlameCheckUpdate.cpp index 8dd3a846..285fa49f 100644 --- a/launcher/modplatform/flame/FlameCheckUpdate.cpp +++ b/launcher/modplatform/flame/FlameCheckUpdate.cpp @@ -7,7 +7,10 @@ #include "FileSystem.h" #include "Json.h" -#include "ModDownloadTask.h" +#include "ResourceDownloadTask.h" + +#include "minecraft/mod/ModFolderModel.h" +#include "minecraft/mod/ResourceFolderModel.h" static FlameAPI api; @@ -160,7 +163,7 @@ void FlameCheckUpdate::executeTask() for (auto& author : mod->authors()) pack.authors.append({ author }); pack.description = mod->description(); - pack.provider = ModPlatform::Provider::FLAME; + pack.provider = ModPlatform::ResourceProvider::FLAME; auto old_version = mod->version(); if (old_version.isEmpty() && mod->status() != ModStatus::NotInstalled) { @@ -168,10 +171,10 @@ void FlameCheckUpdate::executeTask() old_version = current_ver.version; } - auto download_task = new ModDownloadTask(pack, latest_ver, m_mods_folder); + auto download_task = new ResourceDownloadTask(pack, latest_ver, m_mods_folder); m_updatable.emplace_back(pack.name, mod->metadata()->hash, old_version, latest_ver.version, api.getModFileChangelog(latest_ver.addonId.toInt(), latest_ver.fileId.toInt()), - ModPlatform::Provider::FLAME, download_task); + ModPlatform::ResourceProvider::FLAME, download_task); } } diff --git a/launcher/modplatform/flame/FlameCheckUpdate.h b/launcher/modplatform/flame/FlameCheckUpdate.h index 163c706c..4a98d684 100644 --- a/launcher/modplatform/flame/FlameCheckUpdate.h +++ b/launcher/modplatform/flame/FlameCheckUpdate.h @@ -8,7 +8,7 @@ class FlameCheckUpdate : public CheckUpdateTask { Q_OBJECT public: - FlameCheckUpdate(QList& mods, std::list& mcVersions, ModAPI::ModLoaderTypes loaders, std::shared_ptr mods_folder) + FlameCheckUpdate(QList& mods, std::list& mcVersions, std::optional loaders, std::shared_ptr mods_folder) : CheckUpdateTask(mods, mcVersions, loaders, mods_folder) {} diff --git a/launcher/modplatform/flame/FlameInstanceCreationTask.cpp b/launcher/modplatform/flame/FlameInstanceCreationTask.cpp index dc69769a..fb6f78e8 100644 --- a/launcher/modplatform/flame/FlameInstanceCreationTask.cpp +++ b/launcher/modplatform/flame/FlameInstanceCreationTask.cpp @@ -183,7 +183,7 @@ bool FlameCreationTask::updateInstance() QEventLoop loop; - connect(job, &NetJob::succeeded, this, [this, raw_response, fileIds, old_inst_dir, &old_files, old_minecraft_dir] { + connect(job.get(), &NetJob::succeeded, this, [this, raw_response, fileIds, old_inst_dir, &old_files, old_minecraft_dir] { // Parse the API response QJsonParseError parse_error{}; auto doc = QJsonDocument::fromJson(*raw_response, &parse_error); @@ -225,7 +225,7 @@ bool FlameCreationTask::updateInstance() m_files_to_remove.append(old_minecraft_dir.absoluteFilePath(relative_path)); } }); - connect(job, &NetJob::finished, &loop, &QEventLoop::quit); + connect(job.get(), &NetJob::finished, &loop, &QEventLoop::quit); m_process_update_file_info_job = job; job->start(); diff --git a/launcher/modplatform/flame/FlameInstanceCreationTask.h b/launcher/modplatform/flame/FlameInstanceCreationTask.h index 498e1d6e..36b62e3e 100644 --- a/launcher/modplatform/flame/FlameInstanceCreationTask.h +++ b/launcher/modplatform/flame/FlameInstanceCreationTask.h @@ -86,7 +86,7 @@ class FlameCreationTask final : public InstanceCreationTask { Flame::Manifest m_pack; // Handle to allow aborting - NetJob* m_process_update_file_info_job = nullptr; + NetJob::Ptr m_process_update_file_info_job = nullptr; NetJob::Ptr m_files_job = nullptr; QString m_managed_id, m_managed_version_id; diff --git a/launcher/modplatform/flame/FlameModIndex.cpp b/launcher/modplatform/flame/FlameModIndex.cpp index 32aa4bdb..617b98ce 100644 --- a/launcher/modplatform/flame/FlameModIndex.cpp +++ b/launcher/modplatform/flame/FlameModIndex.cpp @@ -11,7 +11,7 @@ static ModPlatform::ProviderCapabilities ProviderCaps; void FlameMod::loadIndexedPack(ModPlatform::IndexedPack& pack, QJsonObject& obj) { pack.addonId = Json::requireInteger(obj, "id"); - pack.provider = ModPlatform::Provider::FLAME; + pack.provider = ModPlatform::ResourceProvider::FLAME; pack.name = Json::requireString(obj, "name"); pack.slug = Json::requireString(obj, "slug"); pack.websiteUrl = Json::ensureString(Json::ensureObject(obj, "links"), "websiteUrl", ""); @@ -127,7 +127,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::Provider::FLAME); + auto hash_types = ProviderCaps.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/helpers/HashUtils.cpp b/launcher/modplatform/helpers/HashUtils.cpp index f1e4759e..af484be0 100644 --- a/launcher/modplatform/helpers/HashUtils.cpp +++ b/launcher/modplatform/helpers/HashUtils.cpp @@ -12,12 +12,12 @@ namespace Hashing { static ModPlatform::ProviderCapabilities ProviderCaps; -Hasher::Ptr createHasher(QString file_path, ModPlatform::Provider provider) +Hasher::Ptr createHasher(QString file_path, ModPlatform::ResourceProvider provider) { switch (provider) { - case ModPlatform::Provider::MODRINTH: + case ModPlatform::ResourceProvider::MODRINTH: return createModrinthHasher(file_path); - case ModPlatform::Provider::FLAME: + case ModPlatform::ResourceProvider::FLAME: return createFlameHasher(file_path); default: qCritical() << "[Hashing]" @@ -36,12 +36,12 @@ Hasher::Ptr createFlameHasher(QString file_path) return new FlameHasher(file_path); } -Hasher::Ptr createBlockedModHasher(QString file_path, ModPlatform::Provider provider) +Hasher::Ptr createBlockedModHasher(QString file_path, ModPlatform::ResourceProvider provider) { return new BlockedModHasher(file_path, provider); } -Hasher::Ptr createBlockedModHasher(QString file_path, ModPlatform::Provider provider, QString type) +Hasher::Ptr createBlockedModHasher(QString file_path, ModPlatform::ResourceProvider provider, QString type) { auto hasher = new BlockedModHasher(file_path, provider); hasher->useHashType(type); @@ -62,8 +62,8 @@ void ModrinthHasher::executeTask() return; } - auto hash_type = ProviderCaps.hashType(ModPlatform::Provider::MODRINTH).first(); - m_hash = ProviderCaps.hash(ModPlatform::Provider::MODRINTH, &file, hash_type); + auto hash_type = ProviderCaps.hashType(ModPlatform::ResourceProvider::MODRINTH).first(); + m_hash = ProviderCaps.hash(ModPlatform::ResourceProvider::MODRINTH, &file, hash_type); file.close(); @@ -92,7 +92,7 @@ void FlameHasher::executeTask() } -BlockedModHasher::BlockedModHasher(QString file_path, ModPlatform::Provider provider) +BlockedModHasher::BlockedModHasher(QString file_path, ModPlatform::ResourceProvider provider) : Hasher(file_path), provider(provider) { setObjectName(QString("BlockedModHasher: %1").arg(file_path)); hash_type = ProviderCaps.hashType(provider).first(); diff --git a/launcher/modplatform/helpers/HashUtils.h b/launcher/modplatform/helpers/HashUtils.h index fa3244f6..91146a52 100644 --- a/launcher/modplatform/helpers/HashUtils.h +++ b/launcher/modplatform/helpers/HashUtils.h @@ -42,21 +42,21 @@ class ModrinthHasher : public Hasher { class BlockedModHasher : public Hasher { public: - BlockedModHasher(QString file_path, ModPlatform::Provider provider); + BlockedModHasher(QString file_path, ModPlatform::ResourceProvider provider); void executeTask() override; QStringList getHashTypes(); bool useHashType(QString type); private: - ModPlatform::Provider provider; + ModPlatform::ResourceProvider provider; QString hash_type; }; -Hasher::Ptr createHasher(QString file_path, ModPlatform::Provider provider); +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::Provider provider); -Hasher::Ptr createBlockedModHasher(QString file_path, ModPlatform::Provider provider, QString type); +Hasher::Ptr createBlockedModHasher(QString file_path, ModPlatform::ResourceProvider provider); +Hasher::Ptr createBlockedModHasher(QString file_path, ModPlatform::ResourceProvider provider, QString type); } // namespace Hashing diff --git a/launcher/modplatform/helpers/NetworkModAPI.cpp b/launcher/modplatform/helpers/NetworkModAPI.cpp deleted file mode 100644 index 7633030e..00000000 --- a/launcher/modplatform/helpers/NetworkModAPI.cpp +++ /dev/null @@ -1,97 +0,0 @@ -#include "NetworkModAPI.h" - -#include "ui/pages/modplatform/ModModel.h" - -#include "Application.h" -#include "net/NetJob.h" - -void NetworkModAPI::searchMods(CallerType* caller, SearchArgs&& args) const -{ - auto netJob = new NetJob(QString("%1::Search").arg(caller->debugName()), APPLICATION->network()); - auto searchUrl = getModSearchURL(args); - - auto response = new QByteArray(); - netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchUrl), response)); - - QObject::connect(netJob, &NetJob::started, caller, [caller, netJob] { caller->setActiveJob(netJob); }); - QObject::connect(netJob, &NetJob::failed, caller, &CallerType::searchRequestFailed); - QObject::connect(netJob, &NetJob::aborted, caller, &CallerType::searchRequestAborted); - QObject::connect(netJob, &NetJob::succeeded, caller, [caller, response] { - QJsonParseError parse_error{}; - QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); - if (parse_error.error != QJsonParseError::NoError) { - qWarning() << "Error while parsing JSON response from " << caller->debugName() << " at " << parse_error.offset - << " reason: " << parse_error.errorString(); - qWarning() << *response; - return; - } - - caller->searchRequestFinished(doc); - }); - - netJob->start(); -} - -void NetworkModAPI::getModInfo(ModPlatform::IndexedPack& pack, std::function callback) -{ - auto response = new QByteArray(); - auto job = getProject(pack.addonId.toString(), response); - - QObject::connect(job, &NetJob::succeeded, [callback, &pack, response] { - QJsonParseError parse_error{}; - QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); - if (parse_error.error != QJsonParseError::NoError) { - qWarning() << "Error while parsing JSON response for mod info at " << parse_error.offset - << " reason: " << parse_error.errorString(); - qWarning() << *response; - return; - } - - callback(doc, pack); - }); - - job->start(); -} - -void NetworkModAPI::getVersions(VersionSearchArgs&& args, std::function callback) const -{ - auto netJob = new NetJob(QString("ModVersions(%2)").arg(args.addonId), APPLICATION->network()); - auto response = new QByteArray(); - - netJob->addNetAction(Net::Download::makeByteArray(getVersionsURL(args), response)); - - QObject::connect(netJob, &NetJob::succeeded, [response, callback, args] { - QJsonParseError parse_error{}; - QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); - if (parse_error.error != QJsonParseError::NoError) { - qWarning() << "Error while parsing JSON response for getting versions at " << parse_error.offset - << " reason: " << parse_error.errorString(); - qWarning() << *response; - return; - } - - callback(doc, args.addonId); - }); - - QObject::connect(netJob, &NetJob::finished, [response, netJob] { - netJob->deleteLater(); - delete response; - }); - - netJob->start(); -} - -auto NetworkModAPI::getProject(QString addonId, QByteArray* response) const -> NetJob* -{ - auto netJob = new NetJob(QString("%1::GetProject").arg(addonId), APPLICATION->network()); - auto searchUrl = getModInfoURL(addonId); - - netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchUrl), response)); - - QObject::connect(netJob, &NetJob::finished, [response, netJob] { - netJob->deleteLater(); - delete response; - }); - - return netJob; -} diff --git a/launcher/modplatform/helpers/NetworkModAPI.h b/launcher/modplatform/helpers/NetworkModAPI.h deleted file mode 100644 index b8af22c7..00000000 --- a/launcher/modplatform/helpers/NetworkModAPI.h +++ /dev/null @@ -1,17 +0,0 @@ -#pragma once - -#include "modplatform/ModAPI.h" - -class NetworkModAPI : public ModAPI { - public: - void searchMods(CallerType* caller, SearchArgs&& args) const override; - void getModInfo(ModPlatform::IndexedPack& pack, std::function callback) override; - void getVersions(VersionSearchArgs&& args, std::function callback) const override; - - auto getProject(QString addonId, QByteArray* response) const -> NetJob* override; - - protected: - virtual auto getModSearchURL(SearchArgs& args) const -> QString = 0; - virtual auto getModInfoURL(QString& id) const -> QString = 0; - virtual auto getVersionsURL(VersionSearchArgs& args) const -> QString = 0; -}; diff --git a/launcher/modplatform/helpers/NetworkResourceAPI.cpp b/launcher/modplatform/helpers/NetworkResourceAPI.cpp new file mode 100644 index 00000000..eb17008c --- /dev/null +++ b/launcher/modplatform/helpers/NetworkResourceAPI.cpp @@ -0,0 +1,124 @@ +#include "NetworkResourceAPI.h" + +#include "Application.h" +#include "net/NetJob.h" + +#include "modplatform/ModIndex.h" + +NetJob::Ptr NetworkResourceAPI::searchProjects(SearchArgs&& args, SearchCallbacks&& callbacks) const +{ + auto search_url_optional = getSearchURL(args); + if (!search_url_optional.has_value()) { + callbacks.on_fail("Failed to create search URL", -1); + return nullptr; + } + + auto search_url = search_url_optional.value(); + + auto response = new QByteArray(); + auto netJob = new NetJob(QString("%1::Search").arg(debugName()), APPLICATION->network()); + + netJob->addNetAction(Net::Download::makeByteArray(QUrl(search_url), response)); + + QObject::connect(netJob, &NetJob::succeeded, [=]{ + QJsonParseError parse_error{}; + QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); + if (parse_error.error != QJsonParseError::NoError) { + qWarning() << "Error while parsing JSON response from " << debugName() << " at " << parse_error.offset + << " reason: " << parse_error.errorString(); + qWarning() << *response; + + callbacks.on_fail(parse_error.errorString(), -1); + + return; + } + + callbacks.on_succeed(doc); + }); + + QObject::connect(netJob, &NetJob::failed, [=](QString reason){ + int network_error_code = -1; + if (auto* failed_action = netJob->getFailedActions().at(0); failed_action && failed_action->m_reply) + network_error_code = failed_action->m_reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + + callbacks.on_fail(reason, network_error_code); + }); + QObject::connect(netJob, &NetJob::aborted, [=]{ + callbacks.on_abort(); + }); + + return netJob; +} + +NetJob::Ptr NetworkResourceAPI::getProjectInfo(ProjectInfoArgs&& args, ProjectInfoCallbacks&& callbacks) const +{ + auto response = new QByteArray(); + auto job = getProject(args.pack.addonId.toString(), response); + + QObject::connect(job.get(), &NetJob::succeeded, [response, callbacks, args] { + QJsonParseError parse_error{}; + QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); + if (parse_error.error != QJsonParseError::NoError) { + qWarning() << "Error while parsing JSON response for mod info at " << parse_error.offset + << " reason: " << parse_error.errorString(); + qWarning() << *response; + return; + } + + callbacks.on_succeed(doc, args.pack); + }); + + return job; +} + +NetJob::Ptr NetworkResourceAPI::getProjectVersions(VersionSearchArgs&& args, VersionSearchCallbacks&& callbacks) const +{ + auto versions_url_optional = getVersionsURL(args); + if (!versions_url_optional.has_value()) + return nullptr; + + auto versions_url = versions_url_optional.value(); + + auto netJob = new NetJob(QString("%1::Versions").arg(args.addonId), APPLICATION->network()); + auto response = new QByteArray(); + + netJob->addNetAction(Net::Download::makeByteArray(versions_url, response)); + + QObject::connect(netJob, &NetJob::succeeded, [=] { + QJsonParseError parse_error{}; + QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); + if (parse_error.error != QJsonParseError::NoError) { + qWarning() << "Error while parsing JSON response for getting versions at " << parse_error.offset + << " reason: " << parse_error.errorString(); + qWarning() << *response; + return; + } + + callbacks.on_succeed(doc, args.addonId); + }); + + QObject::connect(netJob, &NetJob::finished, [response] { + delete response; + }); + + return netJob; +} + +NetJob::Ptr NetworkResourceAPI::getProject(QString addonId, QByteArray* response) const +{ + auto project_url_optional = getInfoURL(addonId); + if (!project_url_optional.has_value()) + return nullptr; + + auto project_url = project_url_optional.value(); + + auto netJob = new NetJob(QString("%1::GetProject").arg(addonId), APPLICATION->network()); + + netJob->addNetAction(Net::Download::makeByteArray(QUrl(project_url), response)); + + QObject::connect(netJob, &NetJob::finished, [response] { + delete response; + }); + + return netJob; +} diff --git a/launcher/modplatform/helpers/NetworkResourceAPI.h b/launcher/modplatform/helpers/NetworkResourceAPI.h new file mode 100644 index 00000000..834f274a --- /dev/null +++ b/launcher/modplatform/helpers/NetworkResourceAPI.h @@ -0,0 +1,18 @@ +#pragma once + +#include "modplatform/ResourceAPI.h" + +class NetworkResourceAPI : public ResourceAPI { + public: + NetJob::Ptr searchProjects(SearchArgs&&, SearchCallbacks&&) const override; + + NetJob::Ptr getProject(QString addonId, QByteArray* response) const override; + + NetJob::Ptr getProjectInfo(ProjectInfoArgs&&, ProjectInfoCallbacks&&) const override; + NetJob::Ptr getProjectVersions(VersionSearchArgs&&, VersionSearchCallbacks&&) const override; + + protected: + [[nodiscard]] virtual auto getSearchURL(SearchArgs const& args) const -> std::optional = 0; + [[nodiscard]] virtual auto getInfoURL(QString const& id) const -> std::optional = 0; + [[nodiscard]] virtual auto getVersionsURL(VersionSearchArgs const& args) const -> std::optional = 0; +}; diff --git a/launcher/modplatform/modrinth/ModrinthAPI.cpp b/launcher/modplatform/modrinth/ModrinthAPI.cpp index 747cf4c3..8e64be09 100644 --- a/launcher/modplatform/modrinth/ModrinthAPI.cpp +++ b/launcher/modplatform/modrinth/ModrinthAPI.cpp @@ -37,21 +37,24 @@ auto ModrinthAPI::currentVersions(const QStringList& hashes, QString hash_format auto ModrinthAPI::latestVersion(QString hash, QString hash_format, - std::list mcVersions, - ModLoaderTypes loaders, + std::optional> mcVersions, + std::optional loaders, QByteArray* response) -> NetJob::Ptr { auto* netJob = new NetJob(QString("Modrinth::GetLatestVersion"), APPLICATION->network()); QJsonObject body_obj; - Json::writeStringList(body_obj, "loaders", getModLoaderStrings(loaders)); + if (loaders.has_value()) + Json::writeStringList(body_obj, "loaders", getModLoaderStrings(loaders.value())); - QStringList game_versions; - for (auto& ver : mcVersions) { - game_versions.append(ver.toString()); + if (mcVersions.has_value()) { + QStringList game_versions; + for (auto& ver : mcVersions.value()) { + game_versions.append(ver.toString()); + } + Json::writeStringList(body_obj, "game_versions", game_versions); } - Json::writeStringList(body_obj, "game_versions", game_versions); QJsonDocument body(body_obj); auto body_raw = body.toJson(); @@ -66,8 +69,8 @@ auto ModrinthAPI::latestVersion(QString hash, auto ModrinthAPI::latestVersions(const QStringList& hashes, QString hash_format, - std::list mcVersions, - ModLoaderTypes loaders, + std::optional> mcVersions, + std::optional loaders, QByteArray* response) -> NetJob::Ptr { auto* netJob = new NetJob(QString("Modrinth::GetLatestVersions"), APPLICATION->network()); @@ -77,13 +80,16 @@ auto ModrinthAPI::latestVersions(const QStringList& hashes, Json::writeStringList(body_obj, "hashes", hashes); Json::writeString(body_obj, "algorithm", hash_format); - Json::writeStringList(body_obj, "loaders", getModLoaderStrings(loaders)); + if (loaders.has_value()) + Json::writeStringList(body_obj, "loaders", getModLoaderStrings(loaders.value())); - QStringList game_versions; - for (auto& ver : mcVersions) { - game_versions.append(ver.toString()); + if (mcVersions.has_value()) { + QStringList game_versions; + for (auto& ver : mcVersions.value()) { + game_versions.append(ver.toString()); + } + Json::writeStringList(body_obj, "game_versions", game_versions); } - Json::writeStringList(body_obj, "game_versions", game_versions); QJsonDocument body(body_obj); auto body_raw = body.toJson(); @@ -95,7 +101,7 @@ auto ModrinthAPI::latestVersions(const QStringList& hashes, return netJob; } -auto ModrinthAPI::getProjects(QStringList addonIds, QByteArray* response) const -> NetJob* +NetJob::Ptr ModrinthAPI::getProjects(QStringList addonIds, QByteArray* response) const { auto netJob = new NetJob(QString("Modrinth::GetProjects"), APPLICATION->network()); auto searchUrl = getMultipleModInfoURL(addonIds); diff --git a/launcher/modplatform/modrinth/ModrinthAPI.h b/launcher/modplatform/modrinth/ModrinthAPI.h index e1a18681..bd84fb54 100644 --- a/launcher/modplatform/modrinth/ModrinthAPI.h +++ b/launcher/modplatform/modrinth/ModrinthAPI.h @@ -19,13 +19,12 @@ #pragma once #include "BuildConfig.h" -#include "modplatform/ModAPI.h" #include "modplatform/ModIndex.h" -#include "modplatform/helpers/NetworkModAPI.h" +#include "modplatform/helpers/NetworkResourceAPI.h" #include -class ModrinthAPI : public NetworkModAPI { +class ModrinthAPI : public NetworkResourceAPI { public: auto currentVersion(QString hash, QString hash_format, @@ -37,17 +36,17 @@ class ModrinthAPI : public NetworkModAPI { auto latestVersion(QString hash, QString hash_format, - std::list mcVersions, - ModLoaderTypes loaders, + std::optional> mcVersions, + std::optional loaders, QByteArray* response) -> NetJob::Ptr; auto latestVersions(const QStringList& hashes, QString hash_format, - std::list mcVersions, - ModLoaderTypes loaders, + std::optional> mcVersions, + std::optional loaders, QByteArray* response) -> NetJob::Ptr; - auto getProjects(QStringList addonIds, QByteArray* response) const -> NetJob* override; + NetJob::Ptr getProjects(QStringList addonIds, QByteArray* response) const override; public: inline auto getAuthorURL(const QString& name) const -> QString { return "https://modrinth.com/user/" + name; }; @@ -55,15 +54,13 @@ class ModrinthAPI : public NetworkModAPI { static auto getModLoaderStrings(const ModLoaderTypes types) -> const QStringList { QStringList l; - for (auto loader : {Forge, Fabric, Quilt}) - { - if ((types & loader) || types == Unspecified) - { - l << ModAPI::getModLoaderString(loader); + for (auto loader : {Forge, Fabric, Quilt}) { + if (types & loader) { + l << getModLoaderString(loader); } } if ((types & Quilt) && (~types & Fabric)) // Add Fabric if Quilt is in use, if Fabric isn't already there - l << ModAPI::getModLoaderString(Fabric); + l << getModLoaderString(Fabric); return l; } @@ -78,28 +75,54 @@ class ModrinthAPI : public NetworkModAPI { } private: - inline auto getModSearchURL(SearchArgs& args) const -> QString override + [[nodiscard]] static QString resourceTypeParameter(ModPlatform::ResourceType type) { - if (!validateModLoaders(args.loaders)) { - qWarning() << "Modrinth only have Forge and Fabric-compatible mods!"; - return ""; + switch (type) { + case ModPlatform::ResourceType::MOD: + return "mod"; + default: + qWarning() << "Invalid resource type for Modrinth API!"; + break; } - return QString(BuildConfig.MODRINTH_PROD_URL + - "/search?" - "offset=%1&" - "limit=25&" - "query=%2&" - "index=%3&" - "facets=[[%4],%5[\"project_type:mod\"]]") - .arg(args.offset) - .arg(args.search) - .arg(args.sorting) - .arg(getModLoaderFilters(args.loaders)) - .arg(getGameVersionsArray(args.versions)); + return ""; + } + [[nodiscard]] QString createFacets(SearchArgs const& args) const + { + QStringList facets_list; + + if (args.loaders.has_value()) + 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()))); + facets_list.append(QString("[\"project_type:%1\"]").arg(resourceTypeParameter(args.type))); + + return QString("[%1]").arg(facets_list.join(',')); + } + + public: + [[nodiscard]] inline auto getSearchURL(SearchArgs const& args) const -> std::optional override + { + if (args.loaders.has_value()) { + if (!validateModLoaders(args.loaders.value())) { + qWarning() << "Modrinth only have Forge and Fabric-compatible mods!"; + return {}; + } + } + + QStringList get_arguments; + get_arguments.append(QString("offset=%1").arg(args.offset)); + get_arguments.append(QString("limit=25")); + if (args.search.has_value()) + get_arguments.append(QString("query=%1").arg(args.search.value())); + if (args.sorting.has_value()) + get_arguments.append(QString("index=%1").arg(args.sorting.value())); + get_arguments.append(QString("facets=%1").arg(createFacets(args))); + + return BuildConfig.MODRINTH_PROD_URL + "/search?" + get_arguments.join('&'); }; - inline auto getModInfoURL(QString& id) const -> QString override + inline auto getInfoURL(QString const& id) const -> std::optional override { return BuildConfig.MODRINTH_PROD_URL + "/project/" + id; }; @@ -109,15 +132,16 @@ class ModrinthAPI : public NetworkModAPI { return BuildConfig.MODRINTH_PROD_URL + QString("/projects?ids=[\"%1\"]").arg(ids.join("\",\"")); }; - inline auto getVersionsURL(VersionSearchArgs& args) const -> QString override + inline auto getVersionsURL(VersionSearchArgs const& args) const -> std::optional override { - return QString(BuildConfig.MODRINTH_PROD_URL + - "/project/%1/version?" - "game_versions=[%2]&" - "loaders=[\"%3\"]") - .arg(args.addonId, - getGameVersionsString(args.mcVersions), - getModLoaderStrings(args.loaders).join("\",\"")); + QStringList get_arguments; + if (args.mcVersions.has_value()) + get_arguments.append(QString("game_versions=[%1]").arg(getGameVersionsString(args.mcVersions.value()))); + if (args.loaders.has_value()) + get_arguments.append(QString("loaders=[\"%1\"]").arg(getModLoaderStrings(args.loaders.value()).join("\",\""))); + + return QString("%1/project/%2/version%3%4") + .arg(BuildConfig.MODRINTH_PROD_URL, args.addonId, get_arguments.isEmpty() ? "" : "?", get_arguments.join('&')); }; auto getGameVersionsArray(std::list mcVersions) const -> QString @@ -127,12 +151,12 @@ class ModrinthAPI : public NetworkModAPI { s += QString("\"versions:%1\",").arg(ver.toString()); } s.remove(s.length() - 1, 1); //remove last comma - return s.isEmpty() ? QString() : QString("[%1],").arg(s); + return s.isEmpty() ? QString() : s; } inline auto validateModLoaders(ModLoaderTypes loaders) const -> bool { - return (loaders == Unspecified) || (loaders & (Forge | Fabric | Quilt)); + return loaders & (Forge | Fabric | Quilt); } }; diff --git a/launcher/modplatform/modrinth/ModrinthCheckUpdate.cpp b/launcher/modplatform/modrinth/ModrinthCheckUpdate.cpp index e2d27547..7826b33d 100644 --- a/launcher/modplatform/modrinth/ModrinthCheckUpdate.cpp +++ b/launcher/modplatform/modrinth/ModrinthCheckUpdate.cpp @@ -4,12 +4,15 @@ #include "Json.h" -#include "ModDownloadTask.h" +#include "ResourceDownloadTask.h" #include "modplatform/helpers/HashUtils.h" #include "tasks/ConcurrentTask.h" +#include "minecraft/mod/ModFolderModel.h" +#include "minecraft/mod/ResourceFolderModel.h" + static ModrinthAPI api; static ModPlatform::ProviderCapabilities ProviderCaps; @@ -34,7 +37,7 @@ void ModrinthCheckUpdate::executeTask() // Create all hashes QStringList hashes; - auto best_hash_type = ProviderCaps.hashType(ModPlatform::Provider::MODRINTH).first(); + auto best_hash_type = ProviderCaps.hashType(ModPlatform::ResourceProvider::MODRINTH).first(); ConcurrentTask hashing_task(this, "MakeModrinthHashesTask", 10); for (auto* mod : m_mods) { @@ -108,11 +111,13 @@ void ModrinthCheckUpdate::executeTask() // Sometimes a version may have multiple files, one with "forge" and one with "fabric", // so we may want to filter it QString loader_filter; - static auto flags = { ModAPI::ModLoaderType::Forge, ModAPI::ModLoaderType::Fabric, ModAPI::ModLoaderType::Quilt }; - for (auto flag : flags) { - if (m_loaders.testFlag(flag)) { - loader_filter = api.getModLoaderString(flag); - break; + if (m_loaders.has_value()) { + static auto flags = { ResourceAPI::ModLoaderType::Forge, ResourceAPI::ModLoaderType::Fabric, ResourceAPI::ModLoaderType::Quilt }; + for (auto flag : flags) { + if (m_loaders.value().testFlag(flag)) { + loader_filter = api.getModLoaderString(flag); + break; + } } } @@ -152,12 +157,12 @@ void ModrinthCheckUpdate::executeTask() for (auto& author : mod->authors()) pack.authors.append({ author }); pack.description = mod->description(); - pack.provider = ModPlatform::Provider::MODRINTH; + pack.provider = ModPlatform::ResourceProvider::MODRINTH; - auto download_task = new ModDownloadTask(pack, project_ver, m_mods_folder); + auto download_task = new ResourceDownloadTask(pack, project_ver, m_mods_folder); m_updatable.emplace_back(pack.name, hash, mod->version(), project_ver.version_number, project_ver.changelog, - ModPlatform::Provider::MODRINTH, download_task); + ModPlatform::ResourceProvider::MODRINTH, download_task); } } } catch (Json::JsonException& e) { diff --git a/launcher/modplatform/modrinth/ModrinthCheckUpdate.h b/launcher/modplatform/modrinth/ModrinthCheckUpdate.h index abf8ada1..177ce516 100644 --- a/launcher/modplatform/modrinth/ModrinthCheckUpdate.h +++ b/launcher/modplatform/modrinth/ModrinthCheckUpdate.h @@ -8,7 +8,7 @@ class ModrinthCheckUpdate : public CheckUpdateTask { Q_OBJECT public: - ModrinthCheckUpdate(QList& mods, std::list& mcVersions, ModAPI::ModLoaderTypes loaders, std::shared_ptr mods_folder) + ModrinthCheckUpdate(QList& mods, std::list& mcVersions, std::optional loaders, std::shared_ptr mods_folder) : CheckUpdateTask(mods, mcVersions, loaders, mods_folder) {} diff --git a/launcher/modplatform/modrinth/ModrinthPackIndex.cpp b/launcher/modplatform/modrinth/ModrinthPackIndex.cpp index aec45a73..a0161089 100644 --- a/launcher/modplatform/modrinth/ModrinthPackIndex.cpp +++ b/launcher/modplatform/modrinth/ModrinthPackIndex.cpp @@ -33,7 +33,7 @@ void Modrinth::loadIndexedPack(ModPlatform::IndexedPack& pack, QJsonObject& obj) if (pack.addonId.toString().isEmpty()) pack.addonId = Json::requireString(obj, "id"); - pack.provider = ModPlatform::Provider::MODRINTH; + pack.provider = ModPlatform::ResourceProvider::MODRINTH; pack.name = Json::requireString(obj, "title"); pack.slug = Json::ensureString(obj, "slug", ""); @@ -179,7 +179,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::Provider::MODRINTH); + auto hash_types = ProviderCaps.hashType(ModPlatform::ResourceProvider::MODRINTH); for (auto& hash_type : hash_types) { if (hash_list.contains(hash_type)) { file.hash = Json::requireString(hash_list, hash_type); diff --git a/launcher/modplatform/packwiz/Packwiz.cpp b/launcher/modplatform/packwiz/Packwiz.cpp index 0ed29311..510c7309 100644 --- a/launcher/modplatform/packwiz/Packwiz.cpp +++ b/launcher/modplatform/packwiz/Packwiz.cpp @@ -97,7 +97,7 @@ auto V1::createModFormat(QDir& index_dir, ModPlatform::IndexedPack& mod_pack, Mo mod.name = mod_pack.name; mod.filename = mod_version.fileName; - if (mod_pack.provider == ModPlatform::Provider::FLAME) { + if (mod_pack.provider == ModPlatform::ResourceProvider::FLAME) { mod.mode = "metadata:curseforge"; } else { mod.mode = "url"; @@ -176,11 +176,11 @@ void V1::updateModIndex(QDir& index_dir, Mod& mod) in_stream << QString("\n[update]\n"); in_stream << QString("[update.%1]\n").arg(ProviderCaps.name(mod.provider)); switch (mod.provider) { - case (ModPlatform::Provider::FLAME): + case (ModPlatform::ResourceProvider::FLAME): in_stream << QString("file-id = %1\n").arg(mod.file_id.toString()); in_stream << QString("project-id = %1\n").arg(mod.project_id.toString()); break; - case (ModPlatform::Provider::MODRINTH): + case (ModPlatform::ResourceProvider::MODRINTH): addToStream("mod-id", mod.mod_id().toString()); addToStream("version", mod.version().toString()); break; @@ -273,7 +273,7 @@ auto V1::getIndexForMod(QDir& index_dir, QString slug) -> Mod } { // [update] info - using Provider = ModPlatform::Provider; + using Provider = ModPlatform::ResourceProvider; auto update_table = table["update"]; if (!update_table || !update_table.is_table()) { diff --git a/launcher/modplatform/packwiz/Packwiz.h b/launcher/modplatform/packwiz/Packwiz.h index 9754e5c4..4b096eec 100644 --- a/launcher/modplatform/packwiz/Packwiz.h +++ b/launcher/modplatform/packwiz/Packwiz.h @@ -49,7 +49,7 @@ class V1 { QString hash {}; // [update] - ModPlatform::Provider provider {}; + ModPlatform::ResourceProvider provider {}; QVariant file_id {}; QVariant project_id {}; diff --git a/launcher/net/NetAction.h b/launcher/net/NetAction.h index d9c4fadc..38fe058b 100644 --- a/launcher/net/NetAction.h +++ b/launcher/net/NetAction.h @@ -52,7 +52,6 @@ class NetAction : public Task { virtual ~NetAction() = default; QUrl url() { return m_url; } - auto index() -> int { return m_index_within_job; } void setNetwork(shared_qobject_ptr network) { m_network = network; } @@ -75,9 +74,6 @@ class NetAction : public Task { public: shared_qobject_ptr m_network; - /// index within the parent job, FIXME: nuke - int m_index_within_job = 0; - /// the network reply unique_qobject_ptr m_reply; diff --git a/launcher/net/NetJob.cpp b/launcher/net/NetJob.cpp index 9b5d4f1b..4bcd40b5 100644 --- a/launcher/net/NetJob.cpp +++ b/launcher/net/NetJob.cpp @@ -38,11 +38,10 @@ auto NetJob::addNetAction(NetAction::Ptr action) -> bool { - action->m_index_within_job = m_queue.size(); - m_queue.append(action); - action->setNetwork(m_network); + addTask(action); + return true; } diff --git a/launcher/ui/dialogs/BlockedModsDialog.cpp b/launcher/ui/dialogs/BlockedModsDialog.cpp index 8b49bd1a..5977fd10 100644 --- a/launcher/ui/dialogs/BlockedModsDialog.cpp +++ b/launcher/ui/dialogs/BlockedModsDialog.cpp @@ -230,7 +230,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::Provider::FLAME, "sha1"); + auto hash_task = Hashing::createBlockedModHasher(path, ModPlatform::ResourceProvider::FLAME, "sha1"); 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 89935d9a..83748e1e 100644 --- a/launcher/ui/dialogs/ChooseProviderDialog.cpp +++ b/launcher/ui/dialogs/ChooseProviderDialog.cpp @@ -67,9 +67,9 @@ void ChooseProviderDialog::confirmAll() accept(); } -auto ChooseProviderDialog::getSelectedProvider() const -> ModPlatform::Provider +auto ChooseProviderDialog::getSelectedProvider() const -> ModPlatform::ResourceProvider { - return ModPlatform::Provider(m_providers.checkedId()); + return ModPlatform::ResourceProvider(m_providers.checkedId()); } void ChooseProviderDialog::addProviders() @@ -77,7 +77,7 @@ void ChooseProviderDialog::addProviders() int btn_index = 0; QRadioButton* btn; - for (auto& provider : { ModPlatform::Provider::MODRINTH, ModPlatform::Provider::FLAME }) { + for (auto& provider : { ModPlatform::ResourceProvider::MODRINTH, ModPlatform::ResourceProvider::FLAME }) { btn = new QRadioButton(ProviderCaps.readableName(provider), this); m_providers.addButton(btn, btn_index++); ui->providersLayout->addWidget(btn); diff --git a/launcher/ui/dialogs/ChooseProviderDialog.h b/launcher/ui/dialogs/ChooseProviderDialog.h index 4a3b9f29..be9735b5 100644 --- a/launcher/ui/dialogs/ChooseProviderDialog.h +++ b/launcher/ui/dialogs/ChooseProviderDialog.h @@ -8,7 +8,7 @@ class ChooseProviderDialog; } namespace ModPlatform { -enum class Provider; +enum class ResourceProvider; } class Mod; @@ -24,7 +24,7 @@ class ChooseProviderDialog : public QDialog { bool try_others = false; - ModPlatform::Provider chosen; + ModPlatform::ResourceProvider chosen; }; public: @@ -45,7 +45,7 @@ class ChooseProviderDialog : public QDialog { void addProviders(); void disableInput(); - auto getSelectedProvider() const -> ModPlatform::Provider; + auto getSelectedProvider() const -> ModPlatform::ResourceProvider; private: Ui::ChooseProviderDialog* ui; diff --git a/launcher/ui/dialogs/ModDownloadDialog.cpp b/launcher/ui/dialogs/ModDownloadDialog.cpp index 24d23ba9..8a77ef7f 100644 --- a/launcher/ui/dialogs/ModDownloadDialog.cpp +++ b/launcher/ui/dialogs/ModDownloadDialog.cpp @@ -19,184 +19,41 @@ #include "ModDownloadDialog.h" -#include -#include -#include - #include "Application.h" -#include "ReviewMessageBox.h" -#include -#include -#include -#include +#include "ui/pages/modplatform/flame/FlameResourcePages.h" +#include "ui/pages/modplatform/modrinth/ModrinthResourcePages.h" -#include "ModDownloadTask.h" -#include "ui/pages/modplatform/flame/FlameModPage.h" -#include "ui/pages/modplatform/modrinth/ModrinthModPage.h" -#include "ui/widgets/PageContainer.h" - -ModDownloadDialog::ModDownloadDialog(const std::shared_ptr& mods, QWidget* parent, BaseInstance* instance) - : QDialog(parent), mods(mods), m_verticalLayout(new QVBoxLayout(this)), m_instance(instance) +ModDownloadDialog::ModDownloadDialog(QWidget* parent, const std::shared_ptr& mods, BaseInstance* instance) + : ResourceDownloadDialog(parent, mods), m_instance(instance) { - setObjectName(QStringLiteral("ModDownloadDialog")); - m_verticalLayout->setObjectName(QStringLiteral("verticalLayout")); - - resize(std::max(0.5 * parent->width(), 400.0), std::max(0.75 * parent->height(), 400.0)); - - setWindowIcon(APPLICATION->getThemedIcon("new")); - // NOTE: m_buttons must be initialized before PageContainer, because it indirectly accesses m_buttons through setSuggestedPack! Do not - // move this below. - m_buttons = new QDialogButtonBox(QDialogButtonBox::Help | QDialogButtonBox::Ok | QDialogButtonBox::Cancel); - - m_container = new PageContainer(this); - m_container->setSizePolicy(QSizePolicy::Policy::Preferred, QSizePolicy::Policy::Expanding); - m_container->layout()->setContentsMargins(0, 0, 0, 0); - m_verticalLayout->addWidget(m_container); - - m_container->addButtons(m_buttons); - - connect(m_container, &PageContainer::selectedPageChanged, this, &ModDownloadDialog::selectedPageChanged); - - // Bonk Qt over its stupid head and make sure it understands which button is the default one... - // See: https://stackoverflow.com/questions/24556831/qbuttonbox-set-default-button - auto OkButton = m_buttons->button(QDialogButtonBox::Ok); - OkButton->setEnabled(false); - OkButton->setDefault(true); - OkButton->setAutoDefault(true); - OkButton->setText(tr("Review and confirm")); - OkButton->setShortcut(tr("Ctrl+Return")); - OkButton->setToolTip(tr("Opens a new popup to review your selected mods and confirm your selection. Shortcut: Ctrl+Return")); - connect(OkButton, &QPushButton::clicked, this, &ModDownloadDialog::confirm); - - auto CancelButton = m_buttons->button(QDialogButtonBox::Cancel); - CancelButton->setDefault(false); - CancelButton->setAutoDefault(false); - connect(CancelButton, &QPushButton::clicked, this, &ModDownloadDialog::reject); - - auto HelpButton = m_buttons->button(QDialogButtonBox::Help); - HelpButton->setDefault(false); - HelpButton->setAutoDefault(false); - connect(HelpButton, &QPushButton::clicked, m_container, &PageContainer::help); - - QMetaObject::connectSlotsByName(this); - setWindowModality(Qt::WindowModal); - setWindowTitle(dialogTitle()); + initializeContainer(); + connectButtons(); restoreGeometry(QByteArray::fromBase64(APPLICATION->settings()->get("ModDownloadGeometry").toByteArray())); } -QString ModDownloadDialog::dialogTitle() -{ - return tr("Download mods"); -} - -void ModDownloadDialog::reject() -{ - APPLICATION->settings()->set("ModDownloadGeometry", saveGeometry().toBase64()); - QDialog::reject(); -} - -void ModDownloadDialog::confirm() -{ - auto keys = modTask.keys(); - keys.sort(Qt::CaseInsensitive); - - auto confirm_dialog = ReviewMessageBox::create(this, tr("Confirm mods to download")); - - for (auto& task : keys) { - confirm_dialog->appendMod({ task, modTask.find(task).value()->getFilename() }); - } - - if (confirm_dialog->exec()) { - auto deselected = confirm_dialog->deselectedMods(); - for (auto name : deselected) { - modTask.remove(name); - } - - this->accept(); - } -} - void ModDownloadDialog::accept() { APPLICATION->settings()->set("ModDownloadGeometry", saveGeometry().toBase64()); QDialog::accept(); } +void ModDownloadDialog::reject() +{ + APPLICATION->settings()->set("ModDownloadGeometry", saveGeometry().toBase64()); + QDialog::reject(); +} + QList ModDownloadDialog::getPages() { QList pages; - pages.append(ModrinthModPage::create(this, m_instance)); + pages.append(ModrinthModPage::create(this, *m_instance)); if (APPLICATION->capabilities() & Application::SupportsFlame) - pages.append(FlameModPage::create(this, m_instance)); + pages.append(FlameModPage::create(this, *m_instance)); m_selectedPage = dynamic_cast(pages[0]); return pages; } - -void ModDownloadDialog::addSelectedMod(QString name, ModDownloadTask* task) -{ - removeSelectedMod(name); - modTask.insert(name, task); - - m_buttons->button(QDialogButtonBox::Ok)->setEnabled(!modTask.isEmpty()); -} - -void ModDownloadDialog::removeSelectedMod(QString name) -{ - if (modTask.contains(name)) - delete modTask.find(name).value(); - modTask.remove(name); - - m_buttons->button(QDialogButtonBox::Ok)->setEnabled(!modTask.isEmpty()); -} - -bool ModDownloadDialog::isModSelected(QString name, QString filename) const -{ - // FIXME: Is there a way to check for versions without checking the filename - // as a heuristic, other than adding such info to ModDownloadTask itself? - auto iter = modTask.find(name); - return iter != modTask.end() && (iter.value()->getFilename() == filename); -} - -bool ModDownloadDialog::isModSelected(QString name) const -{ - auto iter = modTask.find(name); - return iter != modTask.end(); -} - -const QList ModDownloadDialog::getTasks() -{ - return modTask.values(); -} - -void ModDownloadDialog::selectedPageChanged(BasePage* previous, BasePage* selected) -{ - auto* prev_page = dynamic_cast(previous); - if (!prev_page) { - qCritical() << "Page '" << previous->displayName() << "' in ModDownloadDialog is not a ModPage!"; - return; - } - - m_selectedPage = dynamic_cast(selected); - if (!m_selectedPage) { - qCritical() << "Page '" << selected->displayName() << "' in ModDownloadDialog is not a ModPage!"; - return; - } - - // Same effect as having a global search bar - m_selectedPage->setSearchTerm(prev_page->getSearchTerm()); -} - -bool ModDownloadDialog::selectPage(QString pageId) -{ - return m_container->selectPage(pageId); -} - -ModPage* ModDownloadDialog::getSelectedPage() -{ - return m_selectedPage; -} diff --git a/launcher/ui/dialogs/ModDownloadDialog.h b/launcher/ui/dialogs/ModDownloadDialog.h index fcf6f4fc..19036042 100644 --- a/launcher/ui/dialogs/ModDownloadDialog.h +++ b/launcher/ui/dialogs/ModDownloadDialog.h @@ -19,60 +19,29 @@ #pragma once -#include -#include - -#include "ModDownloadTask.h" #include "minecraft/mod/ModFolderModel.h" -#include "ui/pages/BasePageProvider.h" -namespace Ui -{ -class ModDownloadDialog; -} +#include "ui/dialogs/ResourceDownloadDialog.h" -class PageContainer; class QDialogButtonBox; -class ModPage; -class ModrinthModPage; -class ModDownloadDialog final : public QDialog, public BasePageProvider +class ModDownloadDialog final : public ResourceDownloadDialog { Q_OBJECT public: - explicit ModDownloadDialog(const std::shared_ptr& mods, QWidget* parent, BaseInstance* instance); + explicit ModDownloadDialog(QWidget* parent, const std::shared_ptr& mods, BaseInstance* instance); ~ModDownloadDialog() override = default; - QString dialogTitle() override; + //: String that gets appended to the mod download dialog title ("Download " + resourcesString()) + [[nodiscard]] QString resourceString() const override { return tr("mods"); } + QList getPages() override; - void addSelectedMod(QString name = QString(), ModDownloadTask* task = nullptr); - void removeSelectedMod(QString name = QString()); - bool isModSelected(QString name, QString filename) const; - bool isModSelected(QString name) const; - - const QList getTasks(); - const std::shared_ptr& mods; - - bool selectPage(QString pageId); - ModPage* getSelectedPage(); - public slots: - void confirm(); void accept() override; void reject() override; - private slots: - void selectedPageChanged(BasePage* previous, BasePage* selected); - private: - Ui::ModDownloadDialog* ui = nullptr; - PageContainer* m_container = nullptr; - QDialogButtonBox* m_buttons = nullptr; - QVBoxLayout* m_verticalLayout = nullptr; - ModPage* m_selectedPage = nullptr; - - QHash modTask; BaseInstance* m_instance; }; diff --git a/launcher/ui/dialogs/ModUpdateDialog.cpp b/launcher/ui/dialogs/ModUpdateDialog.cpp index 2704243e..4ef42d6c 100644 --- a/launcher/ui/dialogs/ModUpdateDialog.cpp +++ b/launcher/ui/dialogs/ModUpdateDialog.cpp @@ -21,6 +21,8 @@ #include #include +#include + static ModPlatform::ProviderCapabilities ProviderCaps; static std::list mcVersions(BaseInstance* inst) @@ -28,7 +30,7 @@ static std::list mcVersions(BaseInstance* inst) return { static_cast(inst)->getPackProfile()->getComponent("net.minecraft")->getVersion() }; } -static ModAPI::ModLoaderTypes mcLoaders(BaseInstance* inst) +static std::optional mcLoaders(BaseInstance* inst) { return { static_cast(inst)->getPackProfile()->getModLoaders() }; } @@ -212,14 +214,14 @@ auto ModUpdateDialog::ensureMetadata() -> bool bool confirm_rest = false; bool try_others_rest = false; bool skip_rest = false; - ModPlatform::Provider provider_rest = ModPlatform::Provider::MODRINTH; + ModPlatform::ResourceProvider provider_rest = ModPlatform::ResourceProvider::MODRINTH; - auto addToTmp = [&](Mod* m, ModPlatform::Provider p) { + auto addToTmp = [&](Mod* m, ModPlatform::ResourceProvider p) { switch (p) { - case ModPlatform::Provider::MODRINTH: + case ModPlatform::ResourceProvider::MODRINTH: modrinth_tmp.push_back(m); break; - case ModPlatform::Provider::FLAME: + case ModPlatform::ResourceProvider::FLAME: flame_tmp.push_back(m); break; } @@ -264,10 +266,10 @@ auto ModUpdateDialog::ensureMetadata() -> bool } if (!modrinth_tmp.empty()) { - auto* modrinth_task = new EnsureMetadataTask(modrinth_tmp, index_dir, ModPlatform::Provider::MODRINTH); + auto* modrinth_task = new EnsureMetadataTask(modrinth_tmp, index_dir, ModPlatform::ResourceProvider::MODRINTH); connect(modrinth_task, &EnsureMetadataTask::metadataReady, [this](Mod* candidate) { onMetadataEnsured(candidate); }); connect(modrinth_task, &EnsureMetadataTask::metadataFailed, [this, &should_try_others](Mod* candidate) { - onMetadataFailed(candidate, should_try_others.find(candidate->internal_id()).value(), ModPlatform::Provider::MODRINTH); + onMetadataFailed(candidate, should_try_others.find(candidate->internal_id()).value(), ModPlatform::ResourceProvider::MODRINTH); }); if (modrinth_task->getHashingTask()) @@ -277,10 +279,10 @@ auto ModUpdateDialog::ensureMetadata() -> bool } if (!flame_tmp.empty()) { - auto* flame_task = new EnsureMetadataTask(flame_tmp, index_dir, ModPlatform::Provider::FLAME); + auto* flame_task = new EnsureMetadataTask(flame_tmp, index_dir, ModPlatform::ResourceProvider::FLAME); connect(flame_task, &EnsureMetadataTask::metadataReady, [this](Mod* candidate) { onMetadataEnsured(candidate); }); connect(flame_task, &EnsureMetadataTask::metadataFailed, [this, &should_try_others](Mod* candidate) { - onMetadataFailed(candidate, should_try_others.find(candidate->internal_id()).value(), ModPlatform::Provider::FLAME); + onMetadataFailed(candidate, should_try_others.find(candidate->internal_id()).value(), ModPlatform::ResourceProvider::FLAME); }); if (flame_task->getHashingTask()) @@ -306,28 +308,28 @@ void ModUpdateDialog::onMetadataEnsured(Mod* mod) return; switch (mod->metadata()->provider) { - case ModPlatform::Provider::MODRINTH: + case ModPlatform::ResourceProvider::MODRINTH: m_modrinth_to_update.push_back(mod); break; - case ModPlatform::Provider::FLAME: + case ModPlatform::ResourceProvider::FLAME: m_flame_to_update.push_back(mod); break; } } -ModPlatform::Provider next(ModPlatform::Provider p) +ModPlatform::ResourceProvider next(ModPlatform::ResourceProvider p) { switch (p) { - case ModPlatform::Provider::MODRINTH: - return ModPlatform::Provider::FLAME; - case ModPlatform::Provider::FLAME: - return ModPlatform::Provider::MODRINTH; + case ModPlatform::ResourceProvider::MODRINTH: + return ModPlatform::ResourceProvider::FLAME; + case ModPlatform::ResourceProvider::FLAME: + return ModPlatform::ResourceProvider::MODRINTH; } - return ModPlatform::Provider::FLAME; + return ModPlatform::ResourceProvider::FLAME; } -void ModUpdateDialog::onMetadataFailed(Mod* mod, bool try_others, ModPlatform::Provider first_choice) +void ModUpdateDialog::onMetadataFailed(Mod* mod, bool try_others, ModPlatform::ResourceProvider first_choice) { if (try_others) { auto index_dir = indexDir(); @@ -368,7 +370,7 @@ void ModUpdateDialog::appendMod(CheckUpdateTask::UpdatableMod const& info) QString text = info.changelog; switch (info.provider) { - case ModPlatform::Provider::MODRINTH: { + case ModPlatform::ResourceProvider::MODRINTH: { text = markdownToHTML(info.changelog.toUtf8()); break; } @@ -386,9 +388,9 @@ void ModUpdateDialog::appendMod(CheckUpdateTask::UpdatableMod const& info) ui->modTreeWidget->addTopLevelItem(item_top); } -auto ModUpdateDialog::getTasks() -> const QList +auto ModUpdateDialog::getTasks() -> const QList { - QList list; + QList list; auto* item = ui->modTreeWidget->topLevelItem(0); diff --git a/launcher/ui/dialogs/ModUpdateDialog.h b/launcher/ui/dialogs/ModUpdateDialog.h index bd486f0d..3e3dd90d 100644 --- a/launcher/ui/dialogs/ModUpdateDialog.h +++ b/launcher/ui/dialogs/ModUpdateDialog.h @@ -1,7 +1,7 @@ #pragma once #include "BaseInstance.h" -#include "ModDownloadTask.h" +#include "ResourceDownloadTask.h" #include "ReviewMessageBox.h" #include "minecraft/mod/ModFolderModel.h" @@ -25,7 +25,7 @@ class ModUpdateDialog final : public ReviewMessageBox { void appendMod(const CheckUpdateTask::UpdatableMod& info); - const QList getTasks(); + const QList getTasks(); auto indexDir() const -> QDir { return m_mod_model->indexDir(); } auto noUpdates() const -> bool { return m_no_updates; }; @@ -36,7 +36,7 @@ class ModUpdateDialog final : public ReviewMessageBox { private slots: void onMetadataEnsured(Mod*); - void onMetadataFailed(Mod*, bool try_others = false, ModPlatform::Provider first_choice = ModPlatform::Provider::MODRINTH); + void onMetadataFailed(Mod*, bool try_others = false, ModPlatform::ResourceProvider first_choice = ModPlatform::ResourceProvider::MODRINTH); private: QWidget* m_parent; @@ -54,7 +54,7 @@ class ModUpdateDialog final : public ReviewMessageBox { QList> m_failed_metadata; QList> m_failed_check_update; - QHash m_tasks; + QHash m_tasks; BaseInstance* m_instance; bool m_no_updates = false; diff --git a/launcher/ui/dialogs/ResourceDownloadDialog.cpp b/launcher/ui/dialogs/ResourceDownloadDialog.cpp new file mode 100644 index 00000000..7367548f --- /dev/null +++ b/launcher/ui/dialogs/ResourceDownloadDialog.cpp @@ -0,0 +1,152 @@ +#include "ResourceDownloadDialog.h" + +#include + +#include "Application.h" +#include "ResourceDownloadTask.h" + +#include "ui/dialogs/ReviewMessageBox.h" +#include "ui/pages/modplatform/ResourcePage.h" +#include "ui/widgets/PageContainer.h" + +ResourceDownloadDialog::ResourceDownloadDialog(QWidget* parent, const std::shared_ptr base_model) + : QDialog(parent), m_base_model(base_model), m_buttons(QDialogButtonBox::Help | QDialogButtonBox::Ok | QDialogButtonBox::Cancel), m_vertical_layout(this) +{ + setObjectName(QStringLiteral("ResourceDownloadDialog")); + + resize(std::max(0.5 * parent->width(), 400.0), std::max(0.75 * parent->height(), 400.0)); + + setWindowIcon(APPLICATION->getThemedIcon("new")); + + // Bonk Qt over its stupid head and make sure it understands which button is the default one... + // See: https://stackoverflow.com/questions/24556831/qbuttonbox-set-default-button + auto OkButton = m_buttons.button(QDialogButtonBox::Ok); + OkButton->setEnabled(false); + OkButton->setDefault(true); + OkButton->setAutoDefault(true); + OkButton->setText(tr("Review and confirm")); + OkButton->setShortcut(tr("Ctrl+Return")); + + auto CancelButton = m_buttons.button(QDialogButtonBox::Cancel); + CancelButton->setDefault(false); + CancelButton->setAutoDefault(false); + + auto HelpButton = m_buttons.button(QDialogButtonBox::Help); + HelpButton->setDefault(false); + HelpButton->setAutoDefault(false); + + setWindowModality(Qt::WindowModal); + setWindowTitle(dialogTitle()); +} + +// NOTE: We can't have this in the ctor because PageContainer calls a virtual function, and so +// won't work with subclasses if we put it in this ctor. +void ResourceDownloadDialog::initializeContainer() +{ + m_container = new PageContainer(this); + m_container->setSizePolicy(QSizePolicy::Policy::Preferred, QSizePolicy::Policy::Expanding); + m_container->layout()->setContentsMargins(0, 0, 0, 0); + m_vertical_layout.addWidget(m_container); + + m_container->addButtons(&m_buttons); + + connect(m_container, &PageContainer::selectedPageChanged, this, &ResourceDownloadDialog::selectedPageChanged); +} + +void ResourceDownloadDialog::connectButtons() +{ + auto OkButton = m_buttons.button(QDialogButtonBox::Ok); + OkButton->setToolTip(tr("Opens a new popup to review your selected %1 and confirm your selection. Shortcut: Ctrl+Return").arg(resourceString())); + connect(OkButton, &QPushButton::clicked, this, &ResourceDownloadDialog::confirm); + + auto CancelButton = m_buttons.button(QDialogButtonBox::Cancel); + connect(CancelButton, &QPushButton::clicked, this, &ResourceDownloadDialog::reject); + + auto HelpButton = m_buttons.button(QDialogButtonBox::Help); + connect(HelpButton, &QPushButton::clicked, m_container, &PageContainer::help); +} + +void ResourceDownloadDialog::confirm() +{ + auto keys = m_selected.keys(); + keys.sort(Qt::CaseInsensitive); + + auto confirm_dialog = ReviewMessageBox::create(this, tr("Confirm %1 to download").arg(resourceString())); + + for (auto& task : keys) { + confirm_dialog->appendResource({ task, m_selected.find(task).value()->getFilename() }); + } + + if (confirm_dialog->exec()) { + auto deselected = confirm_dialog->deselectedResources(); + for (auto name : deselected) { + m_selected.remove(name); + } + + this->accept(); + } +} + +bool ResourceDownloadDialog::selectPage(QString pageId) +{ + return m_container->selectPage(pageId); +} + +ResourcePage* ResourceDownloadDialog::getSelectedPage() +{ + return m_selectedPage; +} + +void ResourceDownloadDialog::addResource(QString name, ResourceDownloadTask* task) +{ + removeResource(name); + m_selected.insert(name, task); + + m_buttons.button(QDialogButtonBox::Ok)->setEnabled(!m_selected.isEmpty()); +} + +void ResourceDownloadDialog::removeResource(QString name) +{ + if (m_selected.contains(name)) + m_selected.find(name).value()->deleteLater(); + m_selected.remove(name); + + m_buttons.button(QDialogButtonBox::Ok)->setEnabled(!m_selected.isEmpty()); +} + +bool ResourceDownloadDialog::isSelected(QString name, QString filename) const +{ + auto iter = m_selected.constFind(name); + if (iter == m_selected.constEnd()) + return false; + + // FIXME: Is there a way to check for versions without checking the filename + // as a heuristic, other than adding such info to ResourceDownloadTask itself? + if (!filename.isEmpty()) + return iter.value()->getFilename() == filename; + + return true; +} + +const QList ResourceDownloadDialog::getTasks() +{ + return m_selected.values(); +} + +void ResourceDownloadDialog::selectedPageChanged(BasePage* previous, BasePage* selected) +{ + auto* prev_page = dynamic_cast(previous); + if (!prev_page) { + qCritical() << "Page '" << previous->displayName() << "' in ResourceDownloadDialog is not a ResourcePage!"; + return; + } + + m_selectedPage = dynamic_cast(selected); + if (!m_selectedPage) { + qCritical() << "Page '" << selected->displayName() << "' in ResourceDownloadDialog is not a ResourcePage!"; + return; + } + + // Same effect as having a global search bar + m_selectedPage->setSearchTerm(prev_page->getSearchTerm()); +} diff --git a/launcher/ui/dialogs/ResourceDownloadDialog.h b/launcher/ui/dialogs/ResourceDownloadDialog.h new file mode 100644 index 00000000..d6b3938b --- /dev/null +++ b/launcher/ui/dialogs/ResourceDownloadDialog.h @@ -0,0 +1,55 @@ +#pragma once + +#include +#include +#include + +#include "ui/pages/BasePageProvider.h" + +class ResourceDownloadTask; +class ResourcePage; +class ResourceFolderModel; +class PageContainer; +class QVBoxLayout; +class QDialogButtonBox; + +class ResourceDownloadDialog : public QDialog, public BasePageProvider { + Q_OBJECT + + public: + ResourceDownloadDialog(QWidget* parent, const std::shared_ptr base_model); + + void initializeContainer(); + void connectButtons(); + + //: String that gets appended to the download dialog title ("Download " + resourcesString()) + [[nodiscard]] virtual QString resourceString() const { return tr("resources"); } + + QString dialogTitle() override { return tr("Download %1").arg(resourceString()); }; + + bool selectPage(QString pageId); + ResourcePage* getSelectedPage(); + + void addResource(QString name, ResourceDownloadTask* task); + void removeResource(QString name); + [[nodiscard]] bool isSelected(QString name, QString filename = "") const; + + const QList getTasks(); + [[nodiscard]] const std::shared_ptr getBaseModel() const { return m_base_model; } + + protected slots: + void selectedPageChanged(BasePage* previous, BasePage* selected); + + virtual void confirm(); + + protected: + const std::shared_ptr m_base_model; + + PageContainer* m_container = nullptr; + ResourcePage* m_selectedPage = nullptr; + + QDialogButtonBox m_buttons; + QVBoxLayout m_vertical_layout; + + QHash m_selected; +}; diff --git a/launcher/ui/dialogs/ReviewMessageBox.cpp b/launcher/ui/dialogs/ReviewMessageBox.cpp index 7c25c91c..f45a9c4a 100644 --- a/launcher/ui/dialogs/ReviewMessageBox.cpp +++ b/launcher/ui/dialogs/ReviewMessageBox.cpp @@ -25,7 +25,7 @@ auto ReviewMessageBox::create(QWidget* parent, QString&& title, QString&& icon) return new ReviewMessageBox(parent, title, icon); } -void ReviewMessageBox::appendMod(ModInformation&& info) +void ReviewMessageBox::appendResource(ResourceInformation&& info) { auto itemTop = new QTreeWidgetItem(ui->modTreeWidget); itemTop->setCheckState(0, Qt::CheckState::Checked); @@ -39,7 +39,7 @@ void ReviewMessageBox::appendMod(ModInformation&& info) ui->modTreeWidget->addTopLevelItem(itemTop); } -auto ReviewMessageBox::deselectedMods() -> QStringList +auto ReviewMessageBox::deselectedResources() -> QStringList { QStringList list; diff --git a/launcher/ui/dialogs/ReviewMessageBox.h b/launcher/ui/dialogs/ReviewMessageBox.h index 9cfa679a..e2d0ce37 100644 --- a/launcher/ui/dialogs/ReviewMessageBox.h +++ b/launcher/ui/dialogs/ReviewMessageBox.h @@ -12,15 +12,15 @@ class ReviewMessageBox : public QDialog { public: static auto create(QWidget* parent, QString&& title, QString&& icon = "") -> ReviewMessageBox*; - using ModInformation = struct { + using ResourceInformation = struct { QString name; QString filename; }; - void appendMod(ModInformation&& info); - auto deselectedMods() -> QStringList; + void appendResource(ResourceInformation&& info); + auto deselectedResources() -> QStringList; - ~ReviewMessageBox(); + ~ReviewMessageBox() override; protected: ReviewMessageBox(QWidget* parent, const QString& title, const QString& icon); diff --git a/launcher/ui/pages/instance/ModFolderPage.cpp b/launcher/ui/pages/instance/ModFolderPage.cpp index 627e71e5..1bce3c0d 100644 --- a/launcher/ui/pages/instance/ModFolderPage.cpp +++ b/launcher/ui/pages/instance/ModFolderPage.cpp @@ -59,7 +59,7 @@ #include "minecraft/mod/Mod.h" #include "minecraft/mod/ModFolderModel.h" -#include "modplatform/ModAPI.h" +#include "modplatform/ResourceAPI.h" #include "Version.h" #include "tasks/ConcurrentTask.h" @@ -153,12 +153,12 @@ void ModFolderPage::installMods() return; // this is a null instance or a legacy instance auto profile = static_cast(m_instance)->getPackProfile(); - if (profile->getModLoaders() == ModAPI::Unspecified) { + if (!profile->getModLoaders().has_value()) { QMessageBox::critical(this, tr("Error"), tr("Please install a mod loader first!")); return; } - ModDownloadDialog mdownload(m_model, this, m_instance); + ModDownloadDialog mdownload(this, m_model, m_instance); if (mdownload.exec()) { ConcurrentTask* tasks = new ConcurrentTask(this); connect(tasks, &Task::failed, [this, tasks](QString reason) { diff --git a/launcher/ui/pages/instance/ResourcePackPage.h b/launcher/ui/pages/instance/ResourcePackPage.h index 9633e3b4..db8af0c5 100644 --- a/launcher/ui/pages/instance/ResourcePackPage.h +++ b/launcher/ui/pages/instance/ResourcePackPage.h @@ -73,3 +73,4 @@ public: return true; } }; + diff --git a/launcher/ui/pages/modplatform/ModModel.cpp b/launcher/ui/pages/modplatform/ModModel.cpp index ed58eb32..31aae746 100644 --- a/launcher/ui/pages/modplatform/ModModel.cpp +++ b/launcher/ui/pages/modplatform/ModModel.cpp @@ -1,226 +1,81 @@ #include "ModModel.h" -#include "BuildConfig.h" #include "Json.h" #include "ModPage.h" #include "minecraft/MinecraftInstance.h" #include "minecraft/PackProfile.h" -#include "ui/dialogs/ModDownloadDialog.h" - -#include "ui/widgets/ProjectItem.h" #include namespace ModPlatform { -// HACK: We need this to prevent callbacks from calling the ListModel after it has already been deleted. -// This leaks a tiny bit of memory per time the user has opened the mod dialog. How to make this better? -static QHash s_running; - -ListModel::ListModel(ModPage* parent) : QAbstractListModel(parent), m_parent(parent) { s_running.insert(this, true); } - -ListModel::~ListModel() -{ - s_running.find(this).value() = false; -} - -auto ListModel::debugName() const -> QString -{ - return m_parent->debugName(); -} +ListModel::ListModel(ModPage* parent, ResourceAPI* api) : ResourceModel(parent, api) {} /******** Make data requests ********/ -void ListModel::fetchMore(const QModelIndex& parent) +ResourceAPI::SearchArgs ListModel::createSearchArguments() { - if (parent.isValid()) - return; - if (nextSearchOffset == 0) { - qWarning() << "fetchMore with 0 offset is wrong..."; - return; - } - performPaginatedSearch(); + auto profile = static_cast(m_associated_page->m_base_instance).getPackProfile(); + return { ModPlatform::ResourceType::MOD, m_next_search_offset, m_search_term, + getSorts()[currentSort], profile->getModLoaders(), getMineVersions() }; } - -auto ListModel::data(const QModelIndex& index, int role) const -> QVariant +ResourceAPI::SearchCallbacks ListModel::createSearchCallbacks() { - int pos = index.row(); - if (pos >= modpacks.size() || pos < 0 || !index.isValid()) { - return QString("INVALID INDEX %1").arg(pos); - } - - ModPlatform::IndexedPack pack = modpacks.at(pos); - switch (role) { - case Qt::ToolTipRole: { - if (pack.description.length() > 100) { - // some magic to prevent to long tooltips and replace html linebreaks - QString edit = pack.description.left(97); - edit = edit.left(edit.lastIndexOf("
    ")).left(edit.lastIndexOf(" ")).append("..."); - return edit; - } - return pack.description; - } - case Qt::DecorationRole: { - if (m_logoMap.contains(pack.logoName)) { - return m_logoMap.value(pack.logoName); - } - QIcon icon = APPLICATION->getThemedIcon("screenshot-placeholder"); - // un-const-ify this - ((ListModel*)this)->requestLogo(pack.logoName, pack.logoUrl); - return icon; - } - case Qt::SizeHintRole: - return QSize(0, 58); - case Qt::UserRole: { - QVariant v; - v.setValue(pack); - return v; - } - // Custom data - case UserDataTypes::TITLE: - return pack.name; - case UserDataTypes::DESCRIPTION: - return pack.description; - case UserDataTypes::SELECTED: - return m_parent->getDialog()->isModSelected(pack.name); - default: - break; - } - - return {}; -} - -bool ListModel::setData(const QModelIndex &index, const QVariant &value, int role) -{ - int pos = index.row(); - if (pos >= modpacks.size() || pos < 0 || !index.isValid()) - return false; - - modpacks[pos] = value.value(); - - return true; -} - -void ListModel::requestModVersions(ModPlatform::IndexedPack const& current, QModelIndex index) -{ - auto profile = (dynamic_cast((dynamic_cast(parent()))->m_instance))->getPackProfile(); - - m_parent->apiProvider()->getVersions({ current.addonId.toString(), getMineVersions(), profile->getModLoaders() }, - [this, current, index](QJsonDocument& doc, QString addonId) { - if (!s_running.constFind(this).value()) - return; - versionRequestSucceeded(doc, addonId, index); - }); -} - -void ListModel::performPaginatedSearch() -{ - auto profile = (dynamic_cast((dynamic_cast(parent()))->m_instance))->getPackProfile(); - - m_parent->apiProvider()->searchMods( - this, { nextSearchOffset, currentSearchTerm, getSorts()[currentSort], profile->getModLoaders(), getMineVersions() }); -} - -void ListModel::requestModInfo(ModPlatform::IndexedPack& current, QModelIndex index) -{ - m_parent->apiProvider()->getModInfo(current, [this, index](QJsonDocument& doc, ModPlatform::IndexedPack& pack) { - if (!s_running.constFind(this).value()) + return { [this](auto& doc) { + if (!s_running_models.constFind(this).value()) return; - infoRequestFinished(doc, pack, index); - }); + searchRequestFinished(doc); + } }; } -void ListModel::refresh() +ResourceAPI::VersionSearchArgs ListModel::createVersionsArguments(QModelIndex& entry) { - if (jobPtr) { - jobPtr->abort(); - searchState = ResetRequested; - return; - } else { - beginResetModel(); - modpacks.clear(); - endResetModel(); - searchState = None; - } - nextSearchOffset = 0; - performPaginatedSearch(); + auto const& pack = m_packs[entry.row()]; + auto profile = static_cast(m_associated_page->m_base_instance).getPackProfile(); + + return { pack.addonId.toString(), getMineVersions(), profile->getModLoaders() }; +} +ResourceAPI::VersionSearchCallbacks ListModel::createVersionsCallbacks(QModelIndex& entry) +{ + auto const& pack = m_packs[entry.row()]; + + return { [this, pack, entry](auto& doc, auto addonId) { + if (!s_running_models.constFind(this).value()) + return; + versionRequestSucceeded(doc, addonId, entry); + } }; +} + +ResourceAPI::ProjectInfoArgs ListModel::createInfoArguments(QModelIndex& entry) +{ + auto& pack = m_packs[entry.row()]; + return { pack }; +} +ResourceAPI::ProjectInfoCallbacks ListModel::createInfoCallbacks(QModelIndex& entry) +{ + return { [this, entry](auto& doc, auto& pack) { + if (!s_running_models.constFind(this).value()) + return; + infoRequestFinished(doc, pack, entry); + } }; } void ListModel::searchWithTerm(const QString& term, const int sort, const bool filter_changed) { - if (currentSearchTerm == term && currentSearchTerm.isNull() == term.isNull() && currentSort == sort && !filter_changed) { + if (m_search_term == term && m_search_term.isNull() == term.isNull() && currentSort == sort && !filter_changed) { return; } - currentSearchTerm = term; + setSearchTerm(term); currentSort = sort; refresh(); } -void ListModel::getLogo(const QString& logo, const QString& logoUrl, LogoCallback callback) -{ - if (m_logoMap.contains(logo)) { - callback(APPLICATION->metacache() - ->resolveEntry(m_parent->metaEntryBase(), QString("logos/%1").arg(logo.section(".", 0, 0))) - ->getFullPath()); - } else { - requestLogo(logo, logoUrl); - } -} - -void ListModel::requestLogo(QString logo, QString url) -{ - if (m_loadingLogos.contains(logo) || m_failedLogos.contains(logo) || url.isEmpty()) { - return; - } - - MetaEntryPtr entry = - APPLICATION->metacache()->resolveEntry(m_parent->metaEntryBase(), QString("logos/%1").arg(logo.section(".", 0, 0))); - auto job = new NetJob(QString("%1 Icon Download %2").arg(m_parent->debugName()).arg(logo), APPLICATION->network()); - job->addNetAction(Net::Download::makeCached(QUrl(url), entry)); - - auto fullPath = entry->getFullPath(); - QObject::connect(job, &NetJob::succeeded, this, [this, logo, fullPath, job] { - job->deleteLater(); - emit logoLoaded(logo, QIcon(fullPath)); - if (waitingCallbacks.contains(logo)) { - waitingCallbacks.value(logo)(fullPath); - } - }); - - QObject::connect(job, &NetJob::failed, this, [this, logo, job] { - job->deleteLater(); - emit logoFailed(logo); - }); - - job->start(); - m_loadingLogos.append(logo); -} - /******** Request callbacks ********/ -void ListModel::logoLoaded(QString logo, QIcon out) -{ - m_loadingLogos.removeAll(logo); - m_logoMap.insert(logo, out); - for (int i = 0; i < modpacks.size(); i++) { - if (modpacks[i].logoName == logo) { - emit dataChanged(createIndex(i, 0), createIndex(i, 0), { Qt::DecorationRole }); - } - } -} - -void ListModel::logoFailed(QString logo) -{ - m_failedLogos.append(logo); - m_loadingLogos.removeAll(logo); -} - void ListModel::searchRequestFinished(QJsonDocument& doc) { - jobPtr.reset(); - QList newList; auto packs = documentToArray(doc); @@ -232,62 +87,27 @@ void ListModel::searchRequestFinished(QJsonDocument& doc) loadIndexedPack(pack, packObj); newList.append(pack); } catch (const JSONValidationError& e) { - qWarning() << "Error while loading mod from " << m_parent->debugName() << ": " << e.cause(); + qWarning() << "Error while loading mod from " << m_associated_page->debugName() << ": " << e.cause(); continue; } } if (packs.size() < 25) { - searchState = Finished; + m_search_state = SearchState::Finished; } else { - nextSearchOffset += 25; - searchState = CanPossiblyFetchMore; + m_next_search_offset += 25; + m_search_state = SearchState::CanFetchMore; } // When you have a Qt build with assertions turned on, proceeding here will abort the application if (newList.size() == 0) return; - beginInsertRows(QModelIndex(), modpacks.size(), modpacks.size() + newList.size() - 1); - modpacks.append(newList); + beginInsertRows(QModelIndex(), m_packs.size(), m_packs.size() + newList.size() - 1); + m_packs.append(newList); endInsertRows(); } -void ListModel::searchRequestFailed(QString reason) -{ - auto failed_action = jobPtr->getFailedActions().at(0); - if (!failed_action->m_reply) { - // Network error - QMessageBox::critical(nullptr, tr("Error"), tr("A network error occurred. Could not load mods.")); - } else if (failed_action->m_reply && failed_action->m_reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 409) { - // 409 Gone, notify user to update - QMessageBox::critical(nullptr, tr("Error"), - //: %1 refers to the launcher itself - QString("%1 %2") - .arg(m_parent->displayName()) - .arg(tr("API version too old!\nPlease update %1!").arg(BuildConfig.LAUNCHER_DISPLAYNAME))); - } - - jobPtr.reset(); - searchState = Finished; -} - -void ListModel::searchRequestAborted() -{ - if (searchState != ResetRequested) - qCritical() << "Search task in ModModel aborted by an unknown reason!"; - - // Retry fetching - jobPtr.reset(); - - beginResetModel(); - modpacks.clear(); - endResetModel(); - - nextSearchOffset = 0; - performPaginatedSearch(); -} - void ListModel::infoRequestFinished(QJsonDocument& doc, ModPlatform::IndexedPack& pack, const QModelIndex& index) { qDebug() << "Loading mod info"; @@ -310,12 +130,12 @@ void ListModel::infoRequestFinished(QJsonDocument& doc, ModPlatform::IndexedPack } } - m_parent->updateUi(); + m_associated_page->updateUi(); } void ListModel::versionRequestSucceeded(QJsonDocument doc, QString addonId, const QModelIndex& index) { - auto& current = m_parent->getCurrent(); + auto current = m_associated_page->getCurrentPack(); if (addonId != current.addonId) { return; } @@ -336,15 +156,19 @@ void ListModel::versionRequestSucceeded(QJsonDocument doc, QString addonId, cons qWarning() << "Failed to cache mod versions!"; } - - m_parent->updateModVersions(); + m_associated_page->updateVersionList(); } } // namespace ModPlatform /******** Helpers ********/ -auto ModPlatform::ListModel::getMineVersions() const -> std::list +#define MOD_PAGE(x) static_cast(x) + +auto ModPlatform::ListModel::getMineVersions() const -> std::optional> { - return m_parent->getFilter()->versions; + auto versions = MOD_PAGE(m_associated_page)->getFilter()->versions; + if (!versions.empty()) + return versions; + return {}; } diff --git a/launcher/ui/pages/modplatform/ModModel.h b/launcher/ui/pages/modplatform/ModModel.h index 36840649..7c735d90 100644 --- a/launcher/ui/pages/modplatform/ModModel.h +++ b/launcher/ui/pages/modplatform/ModModel.h @@ -3,90 +3,52 @@ #include #include "modplatform/ModIndex.h" -#include "net/NetJob.h" +#include "modplatform/ResourceAPI.h" + +#include "ui/pages/modplatform/ResourceModel.h" class ModPage; class Version; namespace ModPlatform { -using LogoMap = QMap; -using LogoCallback = std::function; - -class ListModel : public QAbstractListModel { +class ListModel : public ResourceModel { Q_OBJECT public: - ListModel(ModPage* parent); - ~ListModel() override; - - inline auto rowCount(const QModelIndex& parent) const -> int override { return parent.isValid() ? 0 : modpacks.size(); }; - inline auto columnCount(const QModelIndex& parent) const -> int override { return parent.isValid() ? 0 : 1; }; - inline auto flags(const QModelIndex& index) const -> Qt::ItemFlags override { return QAbstractListModel::flags(index); }; - - auto debugName() const -> QString; - - /* Retrieve information from the model at a given index with the given role */ - auto data(const QModelIndex& index, int role) const -> QVariant override; - bool setData(const QModelIndex &index, const QVariant &value, int role) override; - - inline void setActiveJob(NetJob::Ptr ptr) { jobPtr = ptr; } - inline NetJob* activeJob() { return jobPtr.get(); } + ListModel(ModPage* parent, ResourceAPI* api); /* Ask the API for more information */ - void fetchMore(const QModelIndex& parent) override; - void refresh(); void searchWithTerm(const QString& term, const int sort, const bool filter_changed); - void requestModInfo(ModPlatform::IndexedPack& current, QModelIndex index); - void requestModVersions(const ModPlatform::IndexedPack& current, QModelIndex index); virtual void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) = 0; virtual void loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj) = 0; virtual void loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) = 0; - void getLogo(const QString& logo, const QString& logoUrl, LogoCallback callback); - - inline auto canFetchMore(const QModelIndex& parent) const -> bool override { return parent.isValid() ? false : searchState == CanPossiblyFetchMore; }; - public slots: void searchRequestFinished(QJsonDocument& doc); - void searchRequestFailed(QString reason); - void searchRequestAborted(); void infoRequestFinished(QJsonDocument& doc, ModPlatform::IndexedPack& pack, const QModelIndex& index); void versionRequestSucceeded(QJsonDocument doc, QString addonId, const QModelIndex& index); - protected slots: + public slots: + ResourceAPI::SearchArgs createSearchArguments() override; + ResourceAPI::SearchCallbacks createSearchCallbacks() override; - void logoFailed(QString logo); - void logoLoaded(QString logo, QIcon out); + ResourceAPI::VersionSearchArgs createVersionsArguments(QModelIndex&) override; + ResourceAPI::VersionSearchCallbacks createVersionsCallbacks(QModelIndex&) override; - void performPaginatedSearch(); + ResourceAPI::ProjectInfoArgs createInfoArguments(QModelIndex&) override; + ResourceAPI::ProjectInfoCallbacks createInfoCallbacks(QModelIndex&) override; protected: virtual auto documentToArray(QJsonDocument& obj) const -> QJsonArray = 0; virtual auto getSorts() const -> const char** = 0; - void requestLogo(QString file, QString url); - - inline auto getMineVersions() const -> std::list; + inline auto getMineVersions() const -> std::optional>; protected: - ModPage* m_parent; - - QList modpacks; - - LogoMap m_logoMap; - QMap waitingCallbacks; - QStringList m_failedLogos; - QStringList m_loadingLogos; - - QString currentSearchTerm; int currentSort = 0; - int nextSearchOffset = 0; - enum SearchState { None, CanPossiblyFetchMore, ResetRequested, Finished } searchState = None; - - NetJob::Ptr jobPtr; }; } // namespace ModPlatform diff --git a/launcher/ui/pages/modplatform/ModPage.cpp b/launcher/ui/pages/modplatform/ModPage.cpp index 0f30689e..853f2c54 100644 --- a/launcher/ui/pages/modplatform/ModPage.cpp +++ b/launcher/ui/pages/modplatform/ModPage.cpp @@ -35,59 +35,30 @@ */ #include "ModPage.h" -#include "Application.h" -#include "ui_ModPage.h" +#include "ui_ResourcePage.h" #include #include #include + #include +#include "Application.h" +#include "ResourceDownloadTask.h" + #include "minecraft/MinecraftInstance.h" #include "minecraft/PackProfile.h" + #include "ui/dialogs/ModDownloadDialog.h" -#include "ui/widgets/ProjectItem.h" -#include "Markdown.h" -ModPage::ModPage(ModDownloadDialog* dialog, BaseInstance* instance, ModAPI* api) - : QWidget(dialog) - , m_instance(instance) - , ui(new Ui::ModPage) - , dialog(dialog) - , m_fetch_progress(this, false) - , api(api) +#include "ui/pages/modplatform/ModModel.h" + +ModPage::ModPage(ModDownloadDialog* dialog, BaseInstance& instance) + : ResourcePage(dialog, instance) { - ui->setupUi(this); - - connect(ui->searchButton, &QPushButton::clicked, this, &ModPage::triggerSearch); - connect(ui->modFilterButton, &QPushButton::clicked, this, &ModPage::filterMods); - connect(ui->packView, &QListView::doubleClicked, this, &ModPage::onModSelected); - - m_search_timer.setTimerType(Qt::TimerType::CoarseTimer); - m_search_timer.setSingleShot(true); - - connect(&m_search_timer, &QTimer::timeout, this, &ModPage::triggerSearch); - - ui->searchEdit->installEventFilter(this); - - ui->versionSelectionBox->view()->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); - ui->versionSelectionBox->view()->parentWidget()->setMaximumHeight(300); - - m_fetch_progress.hideIfInactive(true); - m_fetch_progress.setFixedHeight(24); - m_fetch_progress.progressFormat(""); - - ui->gridLayout_3->addWidget(&m_fetch_progress, 0, 0, 1, ui->gridLayout_3->columnCount()); - - ui->packView->setItemDelegate(new ProjectItemDelegate(this)); - ui->packView->installEventFilter(this); - - connect(ui->packDescription, &QTextBrowser::anchorClicked, this, &ModPage::openUrl); -} - -ModPage::~ModPage() -{ - delete ui; + 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); } void ModPage::setFilterWidget(unique_qobject_ptr& widget) @@ -97,59 +68,19 @@ void ModPage::setFilterWidget(unique_qobject_ptr& widget) m_filter_widget.swap(widget); - ui->gridLayout_3->addWidget(m_filter_widget.get(), 0, 0, 1, ui->gridLayout_3->columnCount()); + m_ui->gridLayout_3->addWidget(m_filter_widget.get(), 0, 0, 1, m_ui->gridLayout_3->columnCount()); - m_filter_widget->setInstance(static_cast(m_instance)); + m_filter_widget->setInstance(&static_cast(m_base_instance)); m_filter = m_filter_widget->getFilter(); connect(m_filter_widget.get(), &ModFilterWidget::filterChanged, this, [&]{ - ui->searchButton->setStyleSheet("text-decoration: underline"); + m_ui->searchButton->setStyleSheet("text-decoration: underline"); }); connect(m_filter_widget.get(), &ModFilterWidget::filterUnchanged, this, [&]{ - ui->searchButton->setStyleSheet("text-decoration: none"); + m_ui->searchButton->setStyleSheet("text-decoration: none"); }); } - -/******** Qt things ********/ - -void ModPage::openedImpl() -{ - updateSelectionButton(); - triggerSearch(); -} - -auto ModPage::eventFilter(QObject* watched, QEvent* event) -> bool -{ - if (watched == ui->searchEdit && event->type() == QEvent::KeyPress) { - auto* keyEvent = dynamic_cast(event); - if (keyEvent->key() == Qt::Key_Return) { - triggerSearch(); - keyEvent->accept(); - return true; - } else { - if (m_search_timer.isActive()) - m_search_timer.stop(); - - m_search_timer.start(350); - } - } else if (watched == ui->packView && event->type() == QEvent::KeyPress) { - auto* keyEvent = dynamic_cast(event); - if (keyEvent->key() == Qt::Key_Return) { - onModSelected(); - - // To have the 'select mod' button outlined instead of the 'review and confirm' one - ui->modSelectionButton->setFocus(Qt::FocusReason::ShortcutFocusReason); - ui->packView->setFocus(Qt::FocusReason::NoFocusReason); - - keyEvent->accept(); - return true; - } - } - return QWidget::eventFilter(watched, event); -} - - /******** Callbacks to events in the UI (set up in the derived classes) ********/ void ModPage::filterMods() @@ -163,176 +94,37 @@ void ModPage::triggerSearch() m_filter = m_filter_widget->getFilter(); if (changed) { - ui->packView->clearSelection(); - ui->packDescription->clear(); - ui->versionSelectionBox->clear(); + m_ui->packView->clearSelection(); + m_ui->packDescription->clear(); + m_ui->versionSelectionBox->clear(); updateSelectionButton(); } - listModel->searchWithTerm(getSearchTerm(), ui->sortByBox->currentIndex(), changed); - m_fetch_progress.watch(listModel->activeJob()); + static_cast(m_model)->searchWithTerm(getSearchTerm(), m_ui->sortByBox->currentIndex(), changed); + m_fetch_progress.watch(&m_model->activeJob()); } -QString ModPage::getSearchTerm() const +QMap ModPage::urlHandlers() const { - return ui->searchEdit->text(); -} -void ModPage::setSearchTerm(QString term) -{ - ui->searchEdit->setText(term); -} - -void ModPage::onSelectionChanged(QModelIndex curr, QModelIndex prev) -{ - ui->versionSelectionBox->clear(); - - if (!curr.isValid()) { return; } - - current = listModel->data(curr, Qt::UserRole).value(); - - if (!current.versionsLoaded) { - qDebug() << QString("Loading %1 mod versions").arg(debugName()); - - ui->modSelectionButton->setText(tr("Loading versions...")); - ui->modSelectionButton->setEnabled(false); - - listModel->requestModVersions(current, curr); - } else { - for (int i = 0; i < current.versions.size(); i++) { - ui->versionSelectionBox->addItem(current.versions[i].version, QVariant(i)); - } - if (ui->versionSelectionBox->count() == 0) { ui->versionSelectionBox->addItem(tr("No valid version found."), QVariant(-1)); } - - updateSelectionButton(); - } - - if(!current.extraDataLoaded){ - qDebug() << QString("Loading %1 mod info").arg(debugName()); - - listModel->requestModInfo(current, curr); - } - - updateUi(); -} - -void ModPage::onVersionSelectionChanged(QString data) -{ - if (data.isNull() || data.isEmpty()) { - selectedVersion = -1; - return; - } - selectedVersion = ui->versionSelectionBox->currentData().toInt(); - updateSelectionButton(); -} - -void ModPage::onModSelected() -{ - if (selectedVersion < 0) - return; - - auto& version = current.versions[selectedVersion]; - if (dialog->isModSelected(current.name, version.fileName)) { - dialog->removeSelectedMod(current.name); - } else { - bool is_indexed = !APPLICATION->settings()->get("ModMetadataDisabled").toBool(); - dialog->addSelectedMod(current.name, new ModDownloadTask(current, version, dialog->mods, is_indexed)); - } - - updateSelectionButton(); - - /* Force redraw on the mods list when the selection changes */ - ui->packView->adjustSize(); -} - -static const QRegularExpression modrinth(QRegularExpression::anchoredPattern("(?:www\\.)?modrinth\\.com\\/mod\\/([^\\/]+)\\/?")); -static const QRegularExpression curseForge(QRegularExpression::anchoredPattern("(?:www\\.)?curseforge\\.com\\/minecraft\\/mc-mods\\/([^\\/]+)\\/?")); -static const QRegularExpression curseForgeOld(QRegularExpression::anchoredPattern("minecraft\\.curseforge\\.com\\/projects\\/([^\\/]+)\\/?")); - -void ModPage::openUrl(const QUrl& url) -{ - // do not allow other url schemes for security reasons - if (!(url.scheme() == "http" || url.scheme() == "https")) { - qWarning() << "Unsupported scheme" << url.scheme(); - return; - } - - // detect mod URLs and search instead - - const QString address = url.host() + url.path(); - QRegularExpressionMatch match; - QString page; - - match = modrinth.match(address); - if (match.hasMatch()) - page = "modrinth"; - else if (APPLICATION->capabilities() & Application::SupportsFlame) { - match = curseForge.match(address); - if (!match.hasMatch()) - match = curseForgeOld.match(address); - - if (match.hasMatch()) - page = "curseforge"; - } - - if (!page.isNull()) { - const QString slug = match.captured(1); - - // ensure the user isn't opening the same mod - if (slug != current.slug) { - dialog->selectPage(page); - - ModPage* newPage = dialog->getSelectedPage(); - - QLineEdit* searchEdit = newPage->ui->searchEdit; - ModPlatform::ListModel* model = newPage->listModel; - QListView* view = newPage->ui->packView; - - auto jump = [url, slug, model, view] { - for (int row = 0; row < model->rowCount({}); row++) { - const QModelIndex index = model->index(row); - const auto pack = model->data(index, Qt::UserRole).value(); - - if (pack.slug == slug) { - view->setCurrentIndex(index); - return; - } - } - - // The final fallback. - QDesktopServices::openUrl(url); - }; - - searchEdit->setText(slug); - newPage->triggerSearch(); - - if (model->activeJob()) - connect(model->activeJob(), &Task::finished, jump); - else - jump(); - - return; - } - } - - // open in the user's web browser - QDesktopServices::openUrl(url); + QMap map; + map.insert(QRegularExpression::anchoredPattern("(?:www\\.)?modrinth\\.com\\/mod\\/([^\\/]+)\\/?"), "modrinth"); + map.insert(QRegularExpression::anchoredPattern("(?:www\\.)?curseforge\\.com\\/minecraft\\/mc-mods\\/([^\\/]+)\\/?"), "curseforge"); + map.insert(QRegularExpression::anchoredPattern("minecraft\\.curseforge\\.com\\/projects\\/([^\\/]+)\\/?"), "curseforge"); + return map; } /******** Make changes to the UI ********/ -void ModPage::retranslate() +void ModPage::updateVersionList() { - ui->retranslateUi(this); -} - -void ModPage::updateModVersions(int prev_count) -{ - auto packProfile = (dynamic_cast(m_instance))->getPackProfile(); + m_ui->versionSelectionBox->clear(); + auto packProfile = (dynamic_cast(m_base_instance)).getPackProfile(); QString mcVersion = packProfile->getComponentVersion("net.minecraft"); - for (int i = 0; i < current.versions.size(); i++) { - auto version = current.versions[i]; + auto current_pack = getCurrentPack(); + 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){ //NOTE: Flame doesn't care about loader, so passing it changes nothing. @@ -344,87 +136,18 @@ void ModPage::updateModVersions(int prev_count) // 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)) - ui->versionSelectionBox->addItem(version.version, QVariant(i)); + m_ui->versionSelectionBox->addItem(version.version, QVariant(i)); } - if (ui->versionSelectionBox->count() == 0 && prev_count != 0) { - ui->versionSelectionBox->addItem(tr("No valid version found!"), QVariant(-1)); - ui->modSelectionButton->setText(tr("Cannot select invalid version :(")); + 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::updateSelectionButton() +void ModPage::addResourceToDialog(ModPlatform::IndexedPack& pack, ModPlatform::IndexedVersion& version) { - if (!isOpened || selectedVersion < 0) { - ui->modSelectionButton->setEnabled(false); - return; - } - - ui->modSelectionButton->setEnabled(true); - auto& version = current.versions[selectedVersion]; - if (!dialog->isModSelected(current.name, version.fileName)) { - ui->modSelectionButton->setText(tr("Select mod for download")); - } else { - ui->modSelectionButton->setText(tr("Deselect mod for download")); - } -} - -void ModPage::updateUi() -{ - QString text = ""; - QString name = current.name; - - if (current.websiteUrl.isEmpty()) - text = name; - else - text = "" + name + ""; - - if (!current.authors.empty()) { - auto authorToStr = [](ModPlatform::ModpackAuthor& author) -> QString { - if (author.url.isEmpty()) { return author.name; } - return QString("%2").arg(author.url, author.name); - }; - QStringList authorStrs; - for (auto& author : current.authors) { - authorStrs.push_back(authorToStr(author)); - } - text += "
    " + tr(" by ") + authorStrs.join(", "); - } - - if (current.extraDataLoaded) { - if (!current.extraData.donate.isEmpty()) { - text += "

    " + tr("Donate information: "); - auto donateToStr = [](ModPlatform::DonationData& donate) -> QString { - return QString("%2").arg(donate.url, donate.platform); - }; - QStringList donates; - for (auto& donate : current.extraData.donate) { - donates.append(donateToStr(donate)); - } - text += donates.join(", "); - } - - if (!current.extraData.issuesUrl.isEmpty() - || !current.extraData.sourceUrl.isEmpty() - || !current.extraData.wikiUrl.isEmpty() - || !current.extraData.discordUrl.isEmpty()) { - text += "

    " + tr("External links:") + "
    "; - } - - if (!current.extraData.issuesUrl.isEmpty()) - text += "- " + tr("Issues: %1").arg(current.extraData.issuesUrl) + "
    "; - if (!current.extraData.wikiUrl.isEmpty()) - text += "- " + tr("Wiki: %1").arg(current.extraData.wikiUrl) + "
    "; - if (!current.extraData.sourceUrl.isEmpty()) - text += "- " + tr("Source code: %1").arg(current.extraData.sourceUrl) + "
    "; - if (!current.extraData.discordUrl.isEmpty()) - text += "- " + tr("Discord: %1").arg(current.extraData.discordUrl) + "
    "; - } - - text += "
    "; - - ui->packDescription->setHtml(text + (current.extraData.body.isEmpty() ? current.description : markdownToHTML(current.extraData.body))); - ui->packDescription->flush(); + bool is_indexed = !APPLICATION->settings()->get("ModMetadataDisabled").toBool(); + m_parent_dialog->addResource(pack.name, new ResourceDownloadTask(pack, version, m_parent_dialog->getBaseModel(), is_indexed)); } diff --git a/launcher/ui/pages/modplatform/ModPage.h b/launcher/ui/pages/modplatform/ModPage.h index c9ccbaf2..8c1fec84 100644 --- a/launcher/ui/pages/modplatform/ModPage.h +++ b/launcher/ui/pages/modplatform/ModPage.h @@ -2,104 +2,58 @@ #include -#include "Application.h" -#include "modplatform/ModAPI.h" #include "modplatform/ModIndex.h" -#include "ui/pages/BasePage.h" -#include "ui/pages/modplatform/ModModel.h" + +#include "ui/pages/modplatform/ResourcePage.h" #include "ui/widgets/ModFilterWidget.h" -#include "ui/widgets/ProgressWidget.h" class ModDownloadDialog; namespace Ui { -class ModPage; +class ResourcePage; } /* This page handles most logic related to browsing and selecting mods to download. */ -class ModPage : public QWidget, public BasePage { +class ModPage : public ResourcePage { Q_OBJECT public: template - static T* create(ModDownloadDialog* dialog, BaseInstance* instance) + static T* create(ModDownloadDialog* dialog, BaseInstance& instance) { auto page = new T(dialog, instance); - auto filter_widget = ModFilterWidget::create(static_cast(instance)->getPackProfile()->getComponentVersion("net.minecraft"), page); + auto filter_widget = ModFilterWidget::create(static_cast(instance).getPackProfile()->getComponentVersion("net.minecraft"), page); page->setFilterWidget(filter_widget); return page; } - ~ModPage() override; + ~ModPage() override = default; - /* Affects what the user sees */ - auto displayName() const -> QString override = 0; - auto icon() const -> QIcon override = 0; - auto id() const -> QString override = 0; - auto helpPage() const -> QString override = 0; + [[nodiscard]] inline QString resourceString() const override { return tr("mod"); } - /* Used internally */ - virtual auto metaEntryBase() const -> QString = 0; - virtual auto debugName() const -> QString = 0; + [[nodiscard]] QMap urlHandlers() const override; + void addResourceToDialog(ModPlatform::IndexedPack&, ModPlatform::IndexedVersion&) override; - void retranslate() override; + virtual auto validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, std::optional loaders = {}) const -> bool = 0; - void updateUi(); - - auto shouldDisplay() const -> bool override = 0; - virtual auto validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, ModAPI::ModLoaderTypes loaders = ModAPI::Unspecified) const -> bool = 0; - virtual bool optedOut(ModPlatform::IndexedVersion& ver) const { return false; }; - - auto apiProvider() -> ModAPI* { return api.get(); }; + [[nodiscard]] bool supportsFiltering() const override { return true; }; auto getFilter() const -> const std::shared_ptr { return m_filter; } - auto getDialog() const -> const ModDownloadDialog* { return dialog; } - - /** Get the current term in the search bar. */ - auto getSearchTerm() const -> QString; - /** Programatically set the term in the search bar. */ - void setSearchTerm(QString); - void setFilterWidget(unique_qobject_ptr&); - auto getCurrent() -> ModPlatform::IndexedPack& { return current; } - void updateModVersions(int prev_count = -1); - - void openedImpl() override; - auto eventFilter(QObject* watched, QEvent* event) -> bool override; - - BaseInstance* m_instance; + public slots: + void updateVersionList() override; protected: - ModPage(ModDownloadDialog* dialog, BaseInstance* instance, ModAPI* api); - void updateSelectionButton(); + ModPage(ModDownloadDialog* dialog, BaseInstance& instance); protected slots: virtual void filterMods(); - void triggerSearch(); - void onSelectionChanged(QModelIndex first, QModelIndex second); - void onVersionSelectionChanged(QString data); - void onModSelected(); - virtual void openUrl(const QUrl& url); + void triggerSearch() override; protected: - Ui::ModPage* ui = nullptr; - ModDownloadDialog* dialog = nullptr; - unique_qobject_ptr m_filter_widget; std::shared_ptr m_filter; - - ProgressWidget m_fetch_progress; - - ModPlatform::ListModel* listModel = nullptr; - ModPlatform::IndexedPack current; - - std::unique_ptr api; - - int selectedVersion = -1; - - // Used to do instant searching with a delay to cache quick changes - QTimer m_search_timer; }; diff --git a/launcher/ui/pages/modplatform/ResourceModel.cpp b/launcher/ui/pages/modplatform/ResourceModel.cpp new file mode 100644 index 00000000..d672a2ac --- /dev/null +++ b/launcher/ui/pages/modplatform/ResourceModel.cpp @@ -0,0 +1,258 @@ +#include "ResourceModel.h" + +#include +#include +#include +#include +#include + +#include "Application.h" +#include "BuildConfig.h" + +#include "net/Download.h" +#include "net/NetJob.h" + +#include "minecraft/MinecraftInstance.h" +#include "minecraft/PackProfile.h" + +#include "modplatform/ModIndex.h" + +#include "ui/pages/modplatform/ResourcePage.h" +#include "ui/widgets/ProjectItem.h" + +QHash ResourceModel::s_running_models; + +ResourceModel::ResourceModel(ResourcePage* parent, ResourceAPI* api) : QAbstractListModel(), m_api(api), m_associated_page(parent) +{ + s_running_models.insert(this, true); +} + +ResourceModel::~ResourceModel() +{ + s_running_models.find(this).value() = false; +} + +auto ResourceModel::data(const QModelIndex& index, int role) const -> QVariant +{ + int pos = index.row(); + if (pos >= m_packs.size() || pos < 0 || !index.isValid()) { + return QString("INVALID INDEX %1").arg(pos); + } + + auto pack = m_packs.at(pos); + switch (role) { + case Qt::ToolTipRole: { + if (pack.description.length() > 100) { + // some magic to prevent to long tooltips and replace html linebreaks + QString edit = pack.description.left(97); + edit = edit.left(edit.lastIndexOf("
    ")).left(edit.lastIndexOf(" ")).append("..."); + return edit; + } + return pack.description; + } + case Qt::DecorationRole: { + if (auto icon_or_none = const_cast(this)->getIcon(const_cast(index), pack.logoUrl); + icon_or_none.has_value()) + return icon_or_none.value(); + + return APPLICATION->getThemedIcon("screenshot-placeholder"); + } + case Qt::SizeHintRole: + return QSize(0, 58); + case Qt::UserRole: { + QVariant v; + v.setValue(pack); + return v; + } + // Custom data + case UserDataTypes::TITLE: + return pack.name; + case UserDataTypes::DESCRIPTION: + return pack.description; + case UserDataTypes::SELECTED: + return isPackSelected(pack); + default: + break; + } + + return {}; +} + +bool ResourceModel::setData(const QModelIndex& index, const QVariant& value, int role) +{ + int pos = index.row(); + if (pos >= m_packs.size() || pos < 0 || !index.isValid()) + return false; + + m_packs[pos] = value.value(); + + return true; +} + +QString ResourceModel::debugName() const +{ + return m_associated_page->debugName() + " (Model)"; +} + +void ResourceModel::fetchMore(const QModelIndex& parent) +{ + if (parent.isValid()) + return; + + Q_ASSERT(m_next_search_offset != 0); + + search(); +} + +void ResourceModel::search() +{ + if (!m_current_job.isRunning()) + m_current_job.clear(); + + auto args{ createSearchArguments() }; + + auto callbacks{ createSearchCallbacks() }; + Q_ASSERT(callbacks.on_succeed); + + // Use defaults if no callbacks are set + if (!callbacks.on_fail) + callbacks.on_fail = [this](QString reason, int network_error_code) { + if (!s_running_models.constFind(this).value()) + return; + searchRequestFailed(reason, network_error_code); + }; + if (!callbacks.on_abort) + callbacks.on_abort = [this] { + if (!s_running_models.constFind(this).value()) + return; + searchRequestAborted(); + }; + + if (auto job = m_api->searchProjects(std::move(args), std::move(callbacks)); job) + addActiveJob(job); +} + +void ResourceModel::loadEntry(QModelIndex& entry) +{ + auto const& pack = m_packs[entry.row()]; + + if (!m_current_job.isRunning()) + m_current_job.clear(); + + if (!pack.versionsLoaded) { + auto args{ createVersionsArguments(entry) }; + auto callbacks{ createVersionsCallbacks(entry) }; + + if (auto job = m_api->getProjectVersions(std::move(args), std::move(callbacks)); job) + addActiveJob(job); + } + + if (!pack.extraDataLoaded) { + auto args{ createInfoArguments(entry) }; + auto callbacks{ createInfoCallbacks(entry) }; + + if (auto job = m_api->getProjectInfo(std::move(args), std::move(callbacks)); job) + addActiveJob(job); + } +} + +void ResourceModel::refresh() +{ + if (m_current_job.isRunning()) { + m_current_job.abort(); + m_search_state = SearchState::ResetRequested; + return; + } + + clearData(); + m_search_state = SearchState::None; + + m_next_search_offset = 0; + search(); +} + +void ResourceModel::clearData() +{ + beginResetModel(); + m_packs.clear(); + endResetModel(); +} + +std::optional ResourceModel::getIcon(QModelIndex& index, const QUrl& url) +{ + QPixmap pixmap; + if (QPixmapCache::find(url.toString(), &pixmap)) + return { pixmap }; + + if (!m_current_icon_job) + m_current_icon_job = new NetJob("IconJob", APPLICATION->network()); + + if (m_currently_running_icon_actions.contains(url)) + return {}; + if (m_failed_icon_actions.contains(url)) + return {}; + + auto cache_entry = APPLICATION->metacache()->resolveEntry( + m_associated_page->metaEntryBase(), + QString("logos/%1").arg(QString(QCryptographicHash::hash(url.toEncoded(), QCryptographicHash::Algorithm::Sha1).toHex()))); + auto icon_fetch_action = Net::Download::makeCached(url, cache_entry); + + auto full_file_path = cache_entry->getFullPath(); + connect(icon_fetch_action.get(), &NetAction::succeeded, this, [=] { + auto icon = QIcon(full_file_path); + QPixmapCache::insert(url.toString(), icon.pixmap(icon.actualSize({ 64, 64 }))); + + m_currently_running_icon_actions.remove(url); + + emit dataChanged(index, index, { Qt::DecorationRole }); + }); + connect(icon_fetch_action.get(), &NetAction::failed, this, [=] { + m_currently_running_icon_actions.remove(url); + m_failed_icon_actions.insert(url); + }); + + m_currently_running_icon_actions.insert(url); + + m_current_icon_job->addNetAction(icon_fetch_action); + if (!m_current_icon_job->isRunning()) + QMetaObject::invokeMethod(m_current_icon_job.get(), &NetJob::start); + + return {}; +} + +bool ResourceModel::isPackSelected(const ModPlatform::IndexedPack& pack) const +{ + return m_associated_page->isPackSelected(pack); +} + +void ResourceModel::searchRequestFailed(QString reason, int network_error_code) +{ + switch (network_error_code) { + default: + // Network error + QMessageBox::critical(nullptr, tr("Error"), tr("A network error occurred. Could not load mods.")); + break; + case 409: + // 409 Gone, notify user to update + QMessageBox::critical(nullptr, tr("Error"), + //: %1 refers to the launcher itself + QString("%1 %2") + .arg(m_associated_page->displayName()) + .arg(tr("API version too old!\nPlease update %1!").arg(BuildConfig.LAUNCHER_DISPLAYNAME))); + break; + } + + m_search_state = SearchState::Finished; +} + +void ResourceModel::searchRequestAborted() +{ + if (m_search_state != SearchState::ResetRequested) + qCritical() << "Search task in" << debugName() << "aborted by an unknown reason!"; + + // Retry fetching + clearData(); + + m_next_search_offset = 0; + search(); +} diff --git a/launcher/ui/pages/modplatform/ResourceModel.h b/launcher/ui/pages/modplatform/ResourceModel.h new file mode 100644 index 00000000..af0e9f55 --- /dev/null +++ b/launcher/ui/pages/modplatform/ResourceModel.h @@ -0,0 +1,101 @@ +#pragma once + +#include + +#include + +#include "QObjectPtr.h" +#include "modplatform/ResourceAPI.h" +#include "tasks/ConcurrentTask.h" + +class NetJob; +class ResourcePage; +class ResourceAPI; + +namespace ModPlatform { +struct IndexedPack; +} + + +class ResourceModel : public QAbstractListModel { + Q_OBJECT + + public: + ResourceModel(ResourcePage* parent, ResourceAPI* api); + ~ResourceModel() override; + + [[nodiscard]] auto data(const QModelIndex&, int role) const -> QVariant override; + bool setData(const QModelIndex& index, const QVariant& value, int role) override; + + [[nodiscard]] auto debugName() const -> QString; + + [[nodiscard]] inline int rowCount(const QModelIndex& parent) const override { return parent.isValid() ? 0 : m_packs.size(); } + [[nodiscard]] inline int columnCount(const QModelIndex& parent) const override { return parent.isValid() ? 0 : 1; }; + [[nodiscard]] inline auto flags(const QModelIndex& index) const -> Qt::ItemFlags override { return QAbstractListModel::flags(index); }; + + inline void addActiveJob(Task::Ptr ptr) { m_current_job.addTask(ptr); if (!m_current_job.isRunning()) m_current_job.start(); } + inline Task const& activeJob() { return m_current_job; } + + public slots: + void fetchMore(const QModelIndex& parent) override; + [[nodiscard]] inline bool canFetchMore(const QModelIndex& parent) const override + { + return parent.isValid() ? false : m_search_state == SearchState::CanFetchMore; + } + + void setSearchTerm(QString term) { m_search_term = term; } + + virtual ResourceAPI::SearchArgs createSearchArguments() = 0; + virtual ResourceAPI::SearchCallbacks createSearchCallbacks() = 0; + + virtual ResourceAPI::VersionSearchArgs createVersionsArguments(QModelIndex&) = 0; + virtual ResourceAPI::VersionSearchCallbacks createVersionsCallbacks(QModelIndex&) = 0; + + virtual ResourceAPI::ProjectInfoArgs createInfoArguments(QModelIndex&) = 0; + virtual ResourceAPI::ProjectInfoCallbacks createInfoCallbacks(QModelIndex&) = 0; + + /** Requests the API for more entries. */ + virtual void search(); + + /** Applies any processing / extra requests needed to fully load the specified entry's information. */ + virtual void loadEntry(QModelIndex&); + + /** Schedule a refresh, clearing the current state. */ + void refresh(); + + /** Gets the icon at the URL for the given index. If it's not fetched yet, fetch it and update when fisinhed. */ + std::optional getIcon(QModelIndex&, const QUrl&); + + protected: + /** Resets the model's data. */ + void clearData(); + + [[nodiscard]] bool isPackSelected(const ModPlatform::IndexedPack&) const; + + protected: + /* Basic search parameters */ + enum class SearchState { None, CanFetchMore, ResetRequested, Finished } m_search_state = SearchState::None; + int m_next_search_offset = 0; + QString m_search_term; + + std::unique_ptr m_api; + + ConcurrentTask m_current_job; + + shared_qobject_ptr m_current_icon_job; + QSet m_currently_running_icon_actions; + QSet m_failed_icon_actions; + + ResourcePage* m_associated_page = nullptr; + + QList m_packs; + + // HACK: We need this to prevent callbacks from calling the model after it has already been deleted. + // This leaks a tiny bit of memory per time the user has opened a resource dialog. How to make this better? + static QHash s_running_models; + + private: + /* Default search request callbacks */ + void searchRequestFailed(QString reason, int network_error_code); + void searchRequestAborted(); +}; diff --git a/launcher/ui/pages/modplatform/ResourcePage.cpp b/launcher/ui/pages/modplatform/ResourcePage.cpp new file mode 100644 index 00000000..3b382d20 --- /dev/null +++ b/launcher/ui/pages/modplatform/ResourcePage.cpp @@ -0,0 +1,347 @@ +#include "ResourcePage.h" +#include "ui_ResourcePage.h" + +#include +#include + +#include "Markdown.h" +#include "ResourceDownloadTask.h" + +#include "minecraft/MinecraftInstance.h" + +#include "ui/dialogs/ResourceDownloadDialog.h" +#include "ui/pages/modplatform/ResourceModel.h" +#include "ui/widgets/ProjectItem.h" + +ResourcePage::ResourcePage(ResourceDownloadDialog* parent, BaseInstance& base_instance) + : QWidget(parent), m_base_instance(base_instance), m_ui(new Ui::ResourcePage), m_parent_dialog(parent), m_fetch_progress(this, false) +{ + m_ui->setupUi(this); + + m_ui->searchEdit->installEventFilter(this); + + m_ui->versionSelectionBox->view()->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); + m_ui->versionSelectionBox->view()->parentWidget()->setMaximumHeight(300); + + m_search_timer.setTimerType(Qt::TimerType::CoarseTimer); + m_search_timer.setSingleShot(true); + + connect(&m_search_timer, &QTimer::timeout, this, &ResourcePage::triggerSearch); + + 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->packView->setItemDelegate(new ProjectItemDelegate(this)); + m_ui->packView->installEventFilter(this); + + connect(m_ui->packDescription, &QTextBrowser::anchorClicked, this, &ResourcePage::openUrl); +} + +ResourcePage::~ResourcePage() +{ + delete m_ui; +} + +void ResourcePage::retranslate() +{ + m_ui->retranslateUi(this); +} + +void ResourcePage::openedImpl() +{ + if (!supportsFiltering()) + m_ui->resourceFilterButton->setVisible(false); + + updateSelectionButton(); + triggerSearch(); +} + +auto ResourcePage::eventFilter(QObject* watched, QEvent* event) -> bool +{ + if (event->type() == QEvent::KeyPress) { + auto* keyEvent = static_cast(event); + if (watched == m_ui->searchEdit) { + if (keyEvent->key() == Qt::Key_Return) { + triggerSearch(); + keyEvent->accept(); + return true; + } else { + if (m_search_timer.isActive()) + m_search_timer.stop(); + + m_search_timer.start(350); + } + } else if (watched == m_ui->packView) { + if (keyEvent->key() == Qt::Key_Return) { + onResourceSelected(); + + // To have the 'select mod' button outlined instead of the 'review and confirm' one + m_ui->resourceSelectionButton->setFocus(Qt::FocusReason::ShortcutFocusReason); + m_ui->packView->setFocus(Qt::FocusReason::NoFocusReason); + + keyEvent->accept(); + return true; + } + } + } + + return QWidget::eventFilter(watched, event); +} + +QString ResourcePage::getSearchTerm() const +{ + return m_ui->searchEdit->text(); +} + +void ResourcePage::setSearchTerm(QString term) +{ + m_ui->searchEdit->setText(term); +} + +ModPlatform::IndexedPack ResourcePage::getCurrentPack() const +{ + return m_model->data(m_ui->packView->currentIndex(), Qt::UserRole).value(); +} + +bool ResourcePage::isPackSelected(const ModPlatform::IndexedPack& pack, int version) const +{ + if (version < 0 || !pack.versionsLoaded) + return m_parent_dialog->isSelected(pack.name); + + return m_parent_dialog->isSelected(pack.name, pack.versions[version].fileName); +} + +void ResourcePage::updateUi() +{ + auto current_pack = getCurrentPack(); + + QString text = ""; + QString name = current_pack.name; + + if (current_pack.websiteUrl.isEmpty()) + text = name; + else + text = "" + name + ""; + + if (!current_pack.authors.empty()) { + auto authorToStr = [](ModPlatform::ModpackAuthor& author) -> QString { + if (author.url.isEmpty()) { + return author.name; + } + return QString("%2").arg(author.url, author.name); + }; + QStringList authorStrs; + for (auto& author : current_pack.authors) { + authorStrs.push_back(authorToStr(author)); + } + text += "
    " + tr(" by ") + authorStrs.join(", "); + } + + if (current_pack.extraDataLoaded) { + if (!current_pack.extraData.donate.isEmpty()) { + text += "

    " + tr("Donate information: "); + auto donateToStr = [](ModPlatform::DonationData& donate) -> QString { + return QString("%2").arg(donate.url, donate.platform); + }; + QStringList donates; + for (auto& donate : current_pack.extraData.donate) { + donates.append(donateToStr(donate)); + } + text += donates.join(", "); + } + + if (!current_pack.extraData.issuesUrl.isEmpty() || !current_pack.extraData.sourceUrl.isEmpty() || + !current_pack.extraData.wikiUrl.isEmpty() || !current_pack.extraData.discordUrl.isEmpty()) { + text += "

    " + tr("External links:") + "
    "; + } + + if (!current_pack.extraData.issuesUrl.isEmpty()) + text += "- " + tr("Issues: %1").arg(current_pack.extraData.issuesUrl) + "
    "; + if (!current_pack.extraData.wikiUrl.isEmpty()) + text += "- " + tr("Wiki: %1").arg(current_pack.extraData.wikiUrl) + "
    "; + if (!current_pack.extraData.sourceUrl.isEmpty()) + text += "- " + tr("Source code: %1").arg(current_pack.extraData.sourceUrl) + "
    "; + if (!current_pack.extraData.discordUrl.isEmpty()) + text += "- " + tr("Discord: %1").arg(current_pack.extraData.discordUrl) + "
    "; + } + + text += "
    "; + + m_ui->packDescription->setHtml( + text + (current_pack.extraData.body.isEmpty() ? current_pack.description : markdownToHTML(current_pack.extraData.body))); + m_ui->packDescription->flush(); +} + +void ResourcePage::updateSelectionButton() +{ + if (!isOpened || m_selected_version_index < 0) { + m_ui->resourceSelectionButton->setEnabled(false); + return; + } + + m_ui->resourceSelectionButton->setEnabled(true); + if (!isPackSelected(getCurrentPack(), m_selected_version_index)) { + m_ui->resourceSelectionButton->setText(tr("Select %1 for download").arg(resourceString())); + } else { + m_ui->resourceSelectionButton->setText(tr("Deselect %1 for download").arg(resourceString())); + } +} + +void ResourcePage::updateVersionList() +{ + auto current_pack = getCurrentPack(); + + m_ui->versionSelectionBox->blockSignals(true); + m_ui->versionSelectionBox->clear(); + m_ui->versionSelectionBox->blockSignals(false); + + for (int i = 0; i < current_pack.versions.size(); i++) { + auto& version = current_pack.versions[i]; + if (optedOut(version)) + continue; + + m_ui->versionSelectionBox->addItem(current_pack.versions[i].version, 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 ResourcePage::onSelectionChanged(QModelIndex curr, QModelIndex prev) +{ + if (!curr.isValid()) { + return; + } + + auto current_pack = getCurrentPack(); + + bool request_load = false; + if (!current_pack.versionsLoaded) { + m_ui->resourceSelectionButton->setText(tr("Loading versions...")); + m_ui->resourceSelectionButton->setEnabled(false); + + request_load = true; + } else { + updateVersionList(); + } + + if (!current_pack.extraDataLoaded) + request_load = true; + + if (request_load) + m_model->loadEntry(curr); + + updateUi(); +} + +void ResourcePage::onVersionSelectionChanged(QString data) +{ + if (data.isNull() || data.isEmpty()) { + m_selected_version_index = -1; + return; + } + + m_selected_version_index = m_ui->versionSelectionBox->currentData().toInt(); + updateSelectionButton(); +} + +void ResourcePage::addResourceToDialog(ModPlatform::IndexedPack& pack, ModPlatform::IndexedVersion& version) +{ + m_parent_dialog->addResource(pack.name, new ResourceDownloadTask(pack, version, m_parent_dialog->getBaseModel())); +} + +void ResourcePage::removeResourceFromDialog(ModPlatform::IndexedPack& pack, ModPlatform::IndexedVersion&) +{ + m_parent_dialog->removeResource(pack.name); +} + +void ResourcePage::onResourceSelected() +{ + if (m_selected_version_index < 0) + return; + + auto current_pack = getCurrentPack(); + + auto& version = current_pack.versions[m_selected_version_index]; + if (m_parent_dialog->isSelected(current_pack.name, version.fileName)) + removeResourceFromDialog(current_pack, version); + else + addResourceToDialog(current_pack, version); + + updateSelectionButton(); + + /* Force redraw on the resource list when the selection changes */ + m_ui->packView->adjustSize(); +} + +void ResourcePage::openUrl(const QUrl& url) +{ + // do not allow other url schemes for security reasons + if (!(url.scheme() == "http" || url.scheme() == "https")) { + qWarning() << "Unsupported scheme" << url.scheme(); + return; + } + + // detect URLs and search instead + + const QString address = url.host() + url.path(); + QRegularExpressionMatch match; + QString page; + + for (auto&& [regex, candidate] : urlHandlers().asKeyValueRange()) { + if (match = QRegularExpression(regex).match(address); match.hasMatch()) { + page = candidate; + break; + } + } + + if (!page.isNull()) { + const QString slug = match.captured(1); + + // ensure the user isn't opening the same mod + if (slug != getCurrentPack().slug) { + m_parent_dialog->selectPage(page); + + auto newPage = m_parent_dialog->getSelectedPage(); + + QLineEdit* searchEdit = newPage->m_ui->searchEdit; + auto model = newPage->m_model; + QListView* view = newPage->m_ui->packView; + + auto jump = [url, slug, model, view] { + for (int row = 0; row < model->rowCount({}); row++) { + const QModelIndex index = model->index(row); + const auto pack = model->data(index, Qt::UserRole).value(); + + if (pack.slug == slug) { + view->setCurrentIndex(index); + return; + } + } + + // The final fallback. + QDesktopServices::openUrl(url); + }; + + searchEdit->setText(slug); + newPage->triggerSearch(); + + if (model->activeJob().isRunning()) + connect(&model->activeJob(), &Task::finished, jump); + else + jump(); + + return; + } + } + + // open in the user's web browser + QDesktopServices::openUrl(url); +} diff --git a/launcher/ui/pages/modplatform/ResourcePage.h b/launcher/ui/pages/modplatform/ResourcePage.h new file mode 100644 index 00000000..32aad3d9 --- /dev/null +++ b/launcher/ui/pages/modplatform/ResourcePage.h @@ -0,0 +1,95 @@ +#pragma once + +#include +#include + +#include "modplatform/ModIndex.h" +#include "modplatform/ResourceAPI.h" + +#include "ui/pages/BasePage.h" +#include "ui/widgets/ProgressWidget.h" + +namespace Ui { +class ResourcePage; +} + +class BaseInstance; +class ResourceModel; +class ResourceDownloadDialog; + +class ResourcePage : public QWidget, public BasePage { + Q_OBJECT + public: + ~ResourcePage() override; + + /* Affects what the user sees */ + [[nodiscard]] auto displayName() const -> QString override = 0; + [[nodiscard]] auto icon() const -> QIcon override = 0; + [[nodiscard]] auto id() const -> QString override = 0; + [[nodiscard]] auto helpPage() const -> QString override = 0; + [[nodiscard]] bool shouldDisplay() const override = 0; + + /* Used internally */ + [[nodiscard]] virtual auto metaEntryBase() const -> QString = 0; + [[nodiscard]] virtual auto debugName() const -> QString = 0; + + [[nodiscard]] virtual inline QString resourceString() const { return tr("resource"); } + + /* Features this resource's page supports */ + [[nodiscard]] virtual bool supportsFiltering() const = 0; + + void retranslate() override; + void openedImpl() override; + auto eventFilter(QObject* watched, QEvent* event) -> bool override; + + /** Get the current term in the search bar. */ + [[nodiscard]] auto getSearchTerm() const -> QString; + /** Programatically set the term in the search bar. */ + void setSearchTerm(QString); + + [[nodiscard]] bool isPackSelected(const ModPlatform::IndexedPack&, int version = -1) const; + [[nodiscard]] auto getCurrentPack() const -> ModPlatform::IndexedPack; + + [[nodiscard]] auto getDialog() const -> const ResourceDownloadDialog* { return m_parent_dialog; } + + protected: + ResourcePage(ResourceDownloadDialog* parent, BaseInstance&); + + public slots: + virtual void updateUi(); + virtual void updateSelectionButton(); + virtual void updateVersionList(); + + virtual void addResourceToDialog(ModPlatform::IndexedPack&, ModPlatform::IndexedVersion&); + virtual void removeResourceFromDialog(ModPlatform::IndexedPack&, ModPlatform::IndexedVersion&); + + protected slots: + virtual void triggerSearch() {} + + void onSelectionChanged(QModelIndex first, QModelIndex second); + void onVersionSelectionChanged(QString data); + void onResourceSelected(); + + /** Associates regex expressions to pages in the order they're given in the map. */ + [[nodiscard]] 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 { return false; }; + + public: + BaseInstance& m_base_instance; + + protected: + Ui::ResourcePage* m_ui; + + ResourceDownloadDialog* m_parent_dialog = nullptr; + ResourceModel* m_model = nullptr; + + int m_selected_version_index = -1; + + ProgressWidget m_fetch_progress; + + // Used to do instant searching with a delay to cache quick changes + QTimer m_search_timer; +}; diff --git a/launcher/ui/pages/modplatform/ModPage.ui b/launcher/ui/pages/modplatform/ResourcePage.ui similarity index 90% rename from launcher/ui/pages/modplatform/ModPage.ui rename to launcher/ui/pages/modplatform/ResourcePage.ui index 94365aa5..8fe1d613 100644 --- a/launcher/ui/pages/modplatform/ModPage.ui +++ b/launcher/ui/pages/modplatform/ResourcePage.ui @@ -1,7 +1,7 @@ - ModPage - + ResourcePage + 0 @@ -51,7 +51,7 @@ - Search for mods... + Search for resources... @@ -74,16 +74,16 @@ - + - Select mod for download + Select resource for download - + Filter options diff --git a/launcher/ui/pages/modplatform/flame/FlameModModel.cpp b/launcher/ui/pages/modplatform/flame/FlameResourceModels.cpp similarity index 92% rename from launcher/ui/pages/modplatform/flame/FlameModModel.cpp rename to launcher/ui/pages/modplatform/flame/FlameResourceModels.cpp index bc2c686c..b602dfac 100644 --- a/launcher/ui/pages/modplatform/flame/FlameModModel.cpp +++ b/launcher/ui/pages/modplatform/flame/FlameResourceModels.cpp @@ -1,4 +1,4 @@ -#include "FlameModModel.h" +#include "FlameResourceModels.h" #include "Json.h" #include "modplatform/flame/FlameModIndex.h" @@ -20,7 +20,7 @@ void ListModel::loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj) void ListModel::loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) { - FlameMod::loadIndexedPackVersions(m, arr, APPLICATION->network(), m_parent->m_instance); + FlameMod::loadIndexedPackVersions(m, arr, APPLICATION->network(), &m_associated_page->m_base_instance); } auto ListModel::documentToArray(QJsonDocument& obj) const -> QJsonArray diff --git a/launcher/ui/pages/modplatform/flame/FlameModModel.h b/launcher/ui/pages/modplatform/flame/FlameResourceModels.h similarity index 92% rename from launcher/ui/pages/modplatform/flame/FlameModModel.h rename to launcher/ui/pages/modplatform/flame/FlameResourceModels.h index 6a6aef2e..b94377d3 100644 --- a/launcher/ui/pages/modplatform/flame/FlameModModel.h +++ b/launcher/ui/pages/modplatform/flame/FlameResourceModels.h @@ -1,6 +1,6 @@ #pragma once -#include "FlameModPage.h" +#include "modplatform/flame/FlameAPI.h" namespace FlameMod { @@ -8,7 +8,7 @@ class ListModel : public ModPlatform::ListModel { Q_OBJECT public: - ListModel(FlameModPage* parent) : ModPlatform::ListModel(parent) {} + ListModel(FlameModPage* parent) : ModPlatform::ListModel(parent, new FlameAPI) {} ~ListModel() override = default; private: diff --git a/launcher/ui/pages/modplatform/flame/FlameModPage.cpp b/launcher/ui/pages/modplatform/flame/FlameResourcePages.cpp similarity index 71% rename from launcher/ui/pages/modplatform/flame/FlameModPage.cpp rename to launcher/ui/pages/modplatform/flame/FlameResourcePages.cpp index bad78c97..490578ad 100644 --- a/launcher/ui/pages/modplatform/flame/FlameModPage.cpp +++ b/launcher/ui/pages/modplatform/flame/FlameResourcePages.cpp @@ -34,37 +34,37 @@ * limitations under the License. */ -#include "FlameModPage.h" -#include "ui_ModPage.h" +#include "FlameResourcePages.h" +#include "ui_ResourcePage.h" -#include "FlameModModel.h" +#include "FlameResourceModels.h" #include "ui/dialogs/ModDownloadDialog.h" -FlameModPage::FlameModPage(ModDownloadDialog* dialog, BaseInstance* instance) - : ModPage(dialog, instance, new FlameAPI()) +FlameModPage::FlameModPage(ModDownloadDialog* dialog, BaseInstance& instance) + : ModPage(dialog, instance) { - listModel = new FlameMod::ListModel(this); - ui->packView->setModel(listModel); + m_model = new FlameMod::ListModel(this); + m_ui->packView->setModel(m_model); // index is used to set the sorting with the flame api - ui->sortByBox->addItem(tr("Sort by Featured")); - ui->sortByBox->addItem(tr("Sort by Popularity")); - ui->sortByBox->addItem(tr("Sort by Last Updated")); - ui->sortByBox->addItem(tr("Sort by Name")); - ui->sortByBox->addItem(tr("Sort by Author")); - ui->sortByBox->addItem(tr("Sort by Downloads")); + m_ui->sortByBox->addItem(tr("Sort by Featured")); + m_ui->sortByBox->addItem(tr("Sort by Popularity")); + m_ui->sortByBox->addItem(tr("Sort by Last Updated")); + m_ui->sortByBox->addItem(tr("Sort by Name")); + m_ui->sortByBox->addItem(tr("Sort by Author")); + m_ui->sortByBox->addItem(tr("Sort by Downloads")); // sometimes Qt just ignores virtual slots and doesn't work as intended it seems, // so it's best not to connect them in the parent's contructor... - connect(ui->sortByBox, SIGNAL(currentIndexChanged(int)), this, SLOT(triggerSearch())); - connect(ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &FlameModPage::onSelectionChanged); - connect(ui->versionSelectionBox, &QComboBox::currentTextChanged, this, &FlameModPage::onVersionSelectionChanged); - connect(ui->modSelectionButton, &QPushButton::clicked, this, &FlameModPage::onModSelected); + connect(m_ui->sortByBox, SIGNAL(currentIndexChanged(int)), this, SLOT(triggerSearch())); + connect(m_ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &FlameModPage::onSelectionChanged); + connect(m_ui->versionSelectionBox, &QComboBox::currentTextChanged, this, &FlameModPage::onVersionSelectionChanged); + connect(m_ui->resourceSelectionButton, &QPushButton::clicked, this, &FlameModPage::onResourceSelected); - ui->packDescription->setMetaEntry(metaEntryBase()); + m_ui->packDescription->setMetaEntry(metaEntryBase()); } -auto FlameModPage::validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, ModAPI::ModLoaderTypes loaders) const -> bool +auto FlameModPage::validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, std::optional loaders) const -> bool { Q_UNUSED(loaders); return ver.mcVersion.contains(mineVer) && !ver.downloadUrl.isEmpty(); diff --git a/launcher/ui/pages/modplatform/flame/FlameModPage.h b/launcher/ui/pages/modplatform/flame/FlameResourcePages.h similarity index 91% rename from launcher/ui/pages/modplatform/flame/FlameModPage.h rename to launcher/ui/pages/modplatform/flame/FlameResourcePages.h index 58479ab9..597a0c25 100644 --- a/launcher/ui/pages/modplatform/flame/FlameModPage.h +++ b/launcher/ui/pages/modplatform/flame/FlameResourcePages.h @@ -36,21 +36,22 @@ #pragma once -#include "modplatform/ModAPI.h" -#include "ui/pages/modplatform/ModPage.h" +#include "Application.h" -#include "modplatform/flame/FlameAPI.h" +#include "modplatform/ResourceAPI.h" + +#include "ui/pages/modplatform/ModPage.h" class FlameModPage : public ModPage { Q_OBJECT public: - static FlameModPage* create(ModDownloadDialog* dialog, BaseInstance* instance) + static FlameModPage* create(ModDownloadDialog* dialog, BaseInstance& instance) { return ModPage::create(dialog, instance); } - FlameModPage(ModDownloadDialog* dialog, BaseInstance* instance); + FlameModPage(ModDownloadDialog* dialog, BaseInstance& instance); ~FlameModPage() override = default; inline auto displayName() const -> QString override { return "CurseForge"; } @@ -61,7 +62,7 @@ class FlameModPage : public ModPage { inline auto debugName() const -> QString override { return "Flame"; } inline auto metaEntryBase() const -> QString override { return "FlameMods"; }; - auto validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, ModAPI::ModLoaderTypes loaders = ModAPI::Unspecified) const -> bool override; + auto validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, std::optional loaders = {}) const -> bool override; bool optedOut(ModPlatform::IndexedVersion& ver) const override; auto shouldDisplay() const -> bool override; diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthModModel.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthResourceModels.cpp similarity index 88% rename from launcher/ui/pages/modplatform/modrinth/ModrinthModModel.cpp rename to launcher/ui/pages/modplatform/modrinth/ModrinthResourceModels.cpp index af92e63e..51278546 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthModModel.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthResourceModels.cpp @@ -16,8 +16,11 @@ * along with this program. If not, see . */ -#include "ModrinthModModel.h" +#include "ModrinthResourceModels.h" +#include "ui/pages/modplatform/modrinth/ModrinthResourcePages.h" + +#include "modplatform/modrinth/ModrinthAPI.h" #include "modplatform/modrinth/ModrinthPackIndex.h" namespace Modrinth { @@ -37,7 +40,7 @@ void ListModel::loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj) void ListModel::loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) { - Modrinth::loadIndexedPackVersions(m, arr, APPLICATION->network(), m_parent->m_instance); + Modrinth::loadIndexedPackVersions(m, arr, APPLICATION->network(), &m_associated_page->m_base_instance); } auto ListModel::documentToArray(QJsonDocument& obj) const -> QJsonArray @@ -46,3 +49,5 @@ auto ListModel::documentToArray(QJsonDocument& obj) const -> QJsonArray } } // namespace Modrinth + + diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthModModel.h b/launcher/ui/pages/modplatform/modrinth/ModrinthResourceModels.h similarity index 88% rename from launcher/ui/pages/modplatform/modrinth/ModrinthModModel.h rename to launcher/ui/pages/modplatform/modrinth/ModrinthResourceModels.h index 386897fd..bf62d22f 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthModModel.h +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthResourceModels.h @@ -18,7 +18,11 @@ #pragma once -#include "ModrinthModPage.h" +#include "ui/pages/modplatform/ModModel.h" + +#include "ui/pages/modplatform/modrinth/ModrinthResourcePages.h" + +#include "modplatform/modrinth/ModrinthAPI.h" namespace Modrinth { @@ -26,7 +30,7 @@ class ListModel : public ModPlatform::ListModel { Q_OBJECT public: - ListModel(ModrinthModPage* parent) : ModPlatform::ListModel(parent){}; + ListModel(ModrinthModPage* parent) : ModPlatform::ListModel(parent, new ModrinthAPI){}; ~ListModel() override = default; private: @@ -42,3 +46,4 @@ class ListModel : public ModPlatform::ListModel { }; } // namespace Modrinth + diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthModPage.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.cpp similarity index 61% rename from launcher/ui/pages/modplatform/modrinth/ModrinthModPage.cpp rename to launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.cpp index c531ea90..17f0bc93 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthModPage.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.cpp @@ -33,48 +33,52 @@ * limitations under the License. */ -#include "ModrinthModPage.h" -#include "modplatform/modrinth/ModrinthAPI.h" -#include "ui_ModPage.h" +#include "ModrinthResourcePages.h" +#include "ui_ResourcePage.h" -#include "ModrinthModModel.h" +#include "modplatform/modrinth/ModrinthAPI.h" + +#include "ModrinthResourceModels.h" #include "ui/dialogs/ModDownloadDialog.h" -ModrinthModPage::ModrinthModPage(ModDownloadDialog* dialog, BaseInstance* instance) - : ModPage(dialog, instance, new ModrinthAPI()) +ModrinthModPage::ModrinthModPage(ModDownloadDialog* dialog, BaseInstance& instance) + : ModPage(dialog, instance) { - listModel = new Modrinth::ListModel(this); - ui->packView->setModel(listModel); + m_model = new Modrinth::ListModel(this); + m_ui->packView->setModel(m_model); // index is used to set the sorting with the modrinth api - ui->sortByBox->addItem(tr("Sort by Relevance")); - ui->sortByBox->addItem(tr("Sort by Downloads")); - ui->sortByBox->addItem(tr("Sort by Follows")); - ui->sortByBox->addItem(tr("Sort by Last Updated")); - ui->sortByBox->addItem(tr("Sort by Newest")); + m_ui->sortByBox->addItem(tr("Sort by Relevance")); + m_ui->sortByBox->addItem(tr("Sort by Downloads")); + m_ui->sortByBox->addItem(tr("Sort by Follows")); + m_ui->sortByBox->addItem(tr("Sort by Last Updated")); + m_ui->sortByBox->addItem(tr("Sort by Newest")); // sometimes Qt just ignores virtual slots and doesn't work as intended it seems, // so it's best not to connect them in the parent's constructor... - connect(ui->sortByBox, SIGNAL(currentIndexChanged(int)), this, SLOT(triggerSearch())); - connect(ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &ModrinthModPage::onSelectionChanged); - connect(ui->versionSelectionBox, &QComboBox::currentTextChanged, this, &ModrinthModPage::onVersionSelectionChanged); - connect(ui->modSelectionButton, &QPushButton::clicked, this, &ModrinthModPage::onModSelected); + connect(m_ui->sortByBox, SIGNAL(currentIndexChanged(int)), this, SLOT(triggerSearch())); + connect(m_ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &ModrinthModPage::onSelectionChanged); + connect(m_ui->versionSelectionBox, &QComboBox::currentTextChanged, this, &ModrinthModPage::onVersionSelectionChanged); + connect(m_ui->resourceSelectionButton, &QPushButton::clicked, this, &ModrinthModPage::onResourceSelected); - ui->packDescription->setMetaEntry(metaEntryBase()); + m_ui->packDescription->setMetaEntry(metaEntryBase()); } -auto ModrinthModPage::validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, ModAPI::ModLoaderTypes loaders) const -> bool +auto ModrinthModPage::validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, std::optional loaders) const -> bool { - auto loaderStrings = ModrinthAPI::getModLoaderStrings(loaders); + auto loaderCompatible = !loaders.has_value(); - auto loaderCompatible = false; - for (auto remoteLoader : ver.loaders) - { - if (loaderStrings.contains(remoteLoader)) { - loaderCompatible = true; - break; + if (!loaderCompatible) { + auto loaderStrings = ModrinthAPI::getModLoaderStrings(loaders.value()); + for (auto remoteLoader : ver.loaders) + { + if (loaderStrings.contains(remoteLoader)) { + loaderCompatible = true; + break; + } } } + return ver.mcVersion.contains(mineVer) && loaderCompatible; } @@ -82,3 +86,4 @@ auto ModrinthModPage::validateVersion(ModPlatform::IndexedVersion& ver, QString // other mod providers start loading before being selected, at least with // my Qt, so we need to implement this in every derived class... auto ModrinthModPage::shouldDisplay() const -> bool { return true; } + diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthModPage.h b/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.h similarity index 64% rename from launcher/ui/pages/modplatform/modrinth/ModrinthModPage.h rename to launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.h index 40d82e6f..6f816cfd 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthModPage.h +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.h @@ -35,32 +35,38 @@ #pragma once -#include "modplatform/ModAPI.h" +#include "Application.h" + +#include "modplatform/ResourceAPI.h" + #include "ui/pages/modplatform/ModPage.h" -#include "modplatform/modrinth/ModrinthAPI.h" +static inline QString displayName() { return "Modrinth"; } +static inline QIcon icon() { return APPLICATION->getThemedIcon("modrinth"); } +static inline QString id() { return "modrinth"; } +static inline QString debugName() { return "Modrinth"; } +static inline QString metaEntryBase() { return "ModrinthPacks"; }; class ModrinthModPage : public ModPage { Q_OBJECT public: - static ModrinthModPage* create(ModDownloadDialog* dialog, BaseInstance* instance) + static ModrinthModPage* create(ModDownloadDialog* dialog, BaseInstance& instance) { return ModPage::create(dialog, instance); } - ModrinthModPage(ModDownloadDialog* dialog, BaseInstance* instance); + ModrinthModPage(ModDownloadDialog* dialog, BaseInstance& instance); ~ModrinthModPage() override = default; - inline auto displayName() const -> QString override { return "Modrinth"; } - inline auto icon() const -> QIcon override { return APPLICATION->getThemedIcon("modrinth"); } - inline auto id() const -> QString override { return "modrinth"; } + [[nodiscard]] bool shouldDisplay() const override; + + [[nodiscard]] inline auto displayName() const -> QString override { return ::displayName(); } \ + [[nodiscard]] inline auto icon() const -> QIcon override { return ::icon(); } \ + [[nodiscard]] inline auto id() const -> QString override { return ::id(); } \ + [[nodiscard]] inline auto debugName() const -> QString override { return ::debugName(); } \ + [[nodiscard]] inline auto metaEntryBase() const -> QString override { return ::metaEntryBase(); } inline auto helpPage() const -> QString override { return "Mod-platform"; } - inline auto debugName() const -> QString override { return "Modrinth"; } - inline auto metaEntryBase() const -> QString override { return "ModrinthPacks"; }; - - auto validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, ModAPI::ModLoaderTypes loaders = ModAPI::Unspecified) const -> bool override; - - auto shouldDisplay() const -> bool override; + auto validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, std::optional loaders = {}) const -> bool override; }; diff --git a/launcher/ui/widgets/ProgressWidget.cpp b/launcher/ui/widgets/ProgressWidget.cpp index b60d9a7a..18b51fc3 100644 --- a/launcher/ui/widgets/ProgressWidget.cpp +++ b/launcher/ui/widgets/ProgressWidget.cpp @@ -39,7 +39,7 @@ void ProgressWidget::progressFormat(QString format) m_bar->setFormat(format); } -void ProgressWidget::watch(Task* task) +void ProgressWidget::watch(const Task* task) { if (!task) return; @@ -57,11 +57,11 @@ void ProgressWidget::watch(Task* task) show(); } -void ProgressWidget::start(Task* task) +void ProgressWidget::start(const Task* task) { watch(task); if (!m_task->isRunning()) - QMetaObject::invokeMethod(m_task, "start", Qt::QueuedConnection); + QMetaObject::invokeMethod(const_cast(m_task), "start", Qt::QueuedConnection); } bool ProgressWidget::exec(std::shared_ptr task) diff --git a/launcher/ui/widgets/ProgressWidget.h b/launcher/ui/widgets/ProgressWidget.h index 4d9097b8..b0458f33 100644 --- a/launcher/ui/widgets/ProgressWidget.h +++ b/launcher/ui/widgets/ProgressWidget.h @@ -27,10 +27,10 @@ class ProgressWidget : public QWidget { public slots: /** Watch the progress of a task. */ - void watch(Task* task); + void watch(const Task* task); /** Watch the progress of a task, and start it if needed */ - void start(Task* task); + void start(const Task* task); /** Blocking way of waiting for a task to finish. */ bool exec(std::shared_ptr task); @@ -50,7 +50,7 @@ class ProgressWidget : public QWidget { private: QLabel* m_label = nullptr; QProgressBar* m_bar = nullptr; - Task* m_task = nullptr; + const Task* m_task = nullptr; bool m_hide_if_inactive = false; }; diff --git a/tests/Packwiz_test.cpp b/tests/Packwiz_test.cpp index 098e8f89..29289469 100644 --- a/tests/Packwiz_test.cpp +++ b/tests/Packwiz_test.cpp @@ -48,7 +48,7 @@ class PackwizTest : public QObject { QCOMPARE(metadata.hash_format, "sha512"); QCOMPARE(metadata.hash, "c8fe6e15ddea32668822dddb26e1851e5f03834be4bcb2eff9c0da7fdc086a9b6cead78e31a44d3bc66335cba11144ee0337c6d5346f1ba63623064499b3188d"); - QCOMPARE(metadata.provider, ModPlatform::Provider::MODRINTH); + QCOMPARE(metadata.provider, ModPlatform::ResourceProvider::MODRINTH); QCOMPARE(metadata.version(), "ug2qKTPR"); QCOMPARE(metadata.mod_id(), "kYq5qkSL"); } @@ -76,7 +76,7 @@ class PackwizTest : public QObject { QCOMPARE(metadata.hash_format, "murmur2"); QCOMPARE(metadata.hash, "1781245820"); - QCOMPARE(metadata.provider, ModPlatform::Provider::FLAME); + QCOMPARE(metadata.provider, ModPlatform::ResourceProvider::FLAME); QCOMPARE(metadata.file_id, 3509043); QCOMPARE(metadata.project_id, 327154); } From 433a802c6ed3070b1b2f4435937a456eb4192f78 Mon Sep 17 00:00:00 2001 From: flow Date: Fri, 16 Dec 2022 19:03:52 -0300 Subject: [PATCH 087/152] refactor: put resource downloading classes in common namespace Puts them all inside the 'ResourceDownload' namespace, so that it's a bit clearer from the outside that those belong to the same 'module'. Signed-off-by: flow --- launcher/ui/dialogs/ModDownloadDialog.cpp | 4 +++ launcher/ui/dialogs/ModDownloadDialog.h | 7 +++-- .../ui/dialogs/ResourceDownloadDialog.cpp | 4 +++ launcher/ui/dialogs/ResourceDownloadDialog.h | 7 ++++- launcher/ui/pages/instance/ModFolderPage.cpp | 2 +- launcher/ui/pages/modplatform/ModModel.cpp | 30 +++++++++---------- launcher/ui/pages/modplatform/ModModel.h | 12 ++++---- launcher/ui/pages/modplatform/ModPage.cpp | 6 +++- launcher/ui/pages/modplatform/ModPage.h | 8 +++-- .../ui/pages/modplatform/ResourceModel.cpp | 4 +++ launcher/ui/pages/modplatform/ResourceModel.h | 6 +++- .../ui/pages/modplatform/ResourcePage.cpp | 4 +++ launcher/ui/pages/modplatform/ResourcePage.h | 7 ++++- .../modplatform/flame/FlameResourceModels.cpp | 18 ++++++----- .../modplatform/flame/FlameResourceModels.h | 14 +++++---- .../modplatform/flame/FlameResourcePages.cpp | 6 +++- .../modplatform/flame/FlameResourcePages.h | 30 +++++++++++++------ .../modrinth/ModrinthResourceModels.cpp | 24 +++++++-------- .../modrinth/ModrinthResourceModels.h | 17 +++++------ .../modrinth/ModrinthResourcePages.cpp | 8 +++-- .../modrinth/ModrinthResourcePages.h | 19 ++++++++---- 21 files changed, 156 insertions(+), 81 deletions(-) diff --git a/launcher/ui/dialogs/ModDownloadDialog.cpp b/launcher/ui/dialogs/ModDownloadDialog.cpp index 8a77ef7f..89b87300 100644 --- a/launcher/ui/dialogs/ModDownloadDialog.cpp +++ b/launcher/ui/dialogs/ModDownloadDialog.cpp @@ -24,6 +24,8 @@ #include "ui/pages/modplatform/flame/FlameResourcePages.h" #include "ui/pages/modplatform/modrinth/ModrinthResourcePages.h" +namespace ResourceDownload { + ModDownloadDialog::ModDownloadDialog(QWidget* parent, const std::shared_ptr& mods, BaseInstance* instance) : ResourceDownloadDialog(parent, mods), m_instance(instance) { @@ -57,3 +59,5 @@ QList ModDownloadDialog::getPages() return pages; } + +} // namespace ResourceDownload diff --git a/launcher/ui/dialogs/ModDownloadDialog.h b/launcher/ui/dialogs/ModDownloadDialog.h index 19036042..b378b5a9 100644 --- a/launcher/ui/dialogs/ModDownloadDialog.h +++ b/launcher/ui/dialogs/ModDownloadDialog.h @@ -25,8 +25,9 @@ class QDialogButtonBox; -class ModDownloadDialog final : public ResourceDownloadDialog -{ +namespace ResourceDownload { + +class ModDownloadDialog final : public ResourceDownloadDialog { Q_OBJECT public: @@ -45,3 +46,5 @@ class ModDownloadDialog final : public ResourceDownloadDialog private: BaseInstance* m_instance; }; + +} // namespace ResourceDownload diff --git a/launcher/ui/dialogs/ResourceDownloadDialog.cpp b/launcher/ui/dialogs/ResourceDownloadDialog.cpp index 7367548f..b143750b 100644 --- a/launcher/ui/dialogs/ResourceDownloadDialog.cpp +++ b/launcher/ui/dialogs/ResourceDownloadDialog.cpp @@ -9,6 +9,8 @@ #include "ui/pages/modplatform/ResourcePage.h" #include "ui/widgets/PageContainer.h" +namespace ResourceDownload { + ResourceDownloadDialog::ResourceDownloadDialog(QWidget* parent, const std::shared_ptr base_model) : QDialog(parent), m_base_model(base_model), m_buttons(QDialogButtonBox::Help | QDialogButtonBox::Ok | QDialogButtonBox::Cancel), m_vertical_layout(this) { @@ -150,3 +152,5 @@ void ResourceDownloadDialog::selectedPageChanged(BasePage* previous, BasePage* s // Same effect as having a global search bar m_selectedPage->setSearchTerm(prev_page->getSearchTerm()); } + +} // namespace ResourceDownload diff --git a/launcher/ui/dialogs/ResourceDownloadDialog.h b/launcher/ui/dialogs/ResourceDownloadDialog.h index d6b3938b..3b234cd1 100644 --- a/launcher/ui/dialogs/ResourceDownloadDialog.h +++ b/launcher/ui/dialogs/ResourceDownloadDialog.h @@ -7,12 +7,15 @@ #include "ui/pages/BasePageProvider.h" class ResourceDownloadTask; -class ResourcePage; class ResourceFolderModel; class PageContainer; class QVBoxLayout; class QDialogButtonBox; +namespace ResourceDownload { + +class ResourcePage; + class ResourceDownloadDialog : public QDialog, public BasePageProvider { Q_OBJECT @@ -53,3 +56,5 @@ class ResourceDownloadDialog : public QDialog, public BasePageProvider { QHash m_selected; }; + +} // namespace ResourceDownload diff --git a/launcher/ui/pages/instance/ModFolderPage.cpp b/launcher/ui/pages/instance/ModFolderPage.cpp index 1bce3c0d..7c4b8952 100644 --- a/launcher/ui/pages/instance/ModFolderPage.cpp +++ b/launcher/ui/pages/instance/ModFolderPage.cpp @@ -158,7 +158,7 @@ void ModFolderPage::installMods() return; } - ModDownloadDialog mdownload(this, m_model, m_instance); + ResourceDownload::ModDownloadDialog mdownload(this, m_model, m_instance); if (mdownload.exec()) { ConcurrentTask* tasks = new ConcurrentTask(this); connect(tasks, &Task::failed, [this, tasks](QString reason) { diff --git a/launcher/ui/pages/modplatform/ModModel.cpp b/launcher/ui/pages/modplatform/ModModel.cpp index 31aae746..59399c59 100644 --- a/launcher/ui/pages/modplatform/ModModel.cpp +++ b/launcher/ui/pages/modplatform/ModModel.cpp @@ -7,19 +7,19 @@ #include -namespace ModPlatform { +namespace ResourceDownload { -ListModel::ListModel(ModPage* parent, ResourceAPI* api) : ResourceModel(parent, api) {} +ModModel::ModModel(ModPage* parent, ResourceAPI* api) : ResourceModel(parent, api) {} /******** Make data requests ********/ -ResourceAPI::SearchArgs ListModel::createSearchArguments() +ResourceAPI::SearchArgs ModModel::createSearchArguments() { auto profile = static_cast(m_associated_page->m_base_instance).getPackProfile(); return { ModPlatform::ResourceType::MOD, m_next_search_offset, m_search_term, getSorts()[currentSort], profile->getModLoaders(), getMineVersions() }; } -ResourceAPI::SearchCallbacks ListModel::createSearchCallbacks() +ResourceAPI::SearchCallbacks ModModel::createSearchCallbacks() { return { [this](auto& doc) { if (!s_running_models.constFind(this).value()) @@ -28,14 +28,14 @@ ResourceAPI::SearchCallbacks ListModel::createSearchCallbacks() } }; } -ResourceAPI::VersionSearchArgs ListModel::createVersionsArguments(QModelIndex& entry) +ResourceAPI::VersionSearchArgs ModModel::createVersionsArguments(QModelIndex& entry) { auto const& pack = m_packs[entry.row()]; auto profile = static_cast(m_associated_page->m_base_instance).getPackProfile(); return { pack.addonId.toString(), getMineVersions(), profile->getModLoaders() }; } -ResourceAPI::VersionSearchCallbacks ListModel::createVersionsCallbacks(QModelIndex& entry) +ResourceAPI::VersionSearchCallbacks ModModel::createVersionsCallbacks(QModelIndex& entry) { auto const& pack = m_packs[entry.row()]; @@ -46,12 +46,12 @@ ResourceAPI::VersionSearchCallbacks ListModel::createVersionsCallbacks(QModelInd } }; } -ResourceAPI::ProjectInfoArgs ListModel::createInfoArguments(QModelIndex& entry) +ResourceAPI::ProjectInfoArgs ModModel::createInfoArguments(QModelIndex& entry) { auto& pack = m_packs[entry.row()]; return { pack }; } -ResourceAPI::ProjectInfoCallbacks ListModel::createInfoCallbacks(QModelIndex& entry) +ResourceAPI::ProjectInfoCallbacks ModModel::createInfoCallbacks(QModelIndex& entry) { return { [this, entry](auto& doc, auto& pack) { if (!s_running_models.constFind(this).value()) @@ -60,7 +60,7 @@ ResourceAPI::ProjectInfoCallbacks ListModel::createInfoCallbacks(QModelIndex& en } }; } -void ListModel::searchWithTerm(const QString& term, const int sort, const bool filter_changed) +void ModModel::searchWithTerm(const QString& term, const int sort, const bool filter_changed) { if (m_search_term == term && m_search_term.isNull() == term.isNull() && currentSort == sort && !filter_changed) { return; @@ -74,7 +74,7 @@ void ListModel::searchWithTerm(const QString& term, const int sort, const bool f /******** Request callbacks ********/ -void ListModel::searchRequestFinished(QJsonDocument& doc) +void ModModel::searchRequestFinished(QJsonDocument& doc) { QList newList; auto packs = documentToArray(doc); @@ -108,7 +108,7 @@ void ListModel::searchRequestFinished(QJsonDocument& doc) endInsertRows(); } -void ListModel::infoRequestFinished(QJsonDocument& doc, ModPlatform::IndexedPack& pack, const QModelIndex& index) +void ModModel::infoRequestFinished(QJsonDocument& doc, ModPlatform::IndexedPack& pack, const QModelIndex& index) { qDebug() << "Loading mod info"; @@ -133,7 +133,7 @@ void ListModel::infoRequestFinished(QJsonDocument& doc, ModPlatform::IndexedPack m_associated_page->updateUi(); } -void ListModel::versionRequestSucceeded(QJsonDocument doc, QString addonId, const QModelIndex& index) +void ModModel::versionRequestSucceeded(QJsonDocument doc, QString addonId, const QModelIndex& index) { auto current = m_associated_page->getCurrentPack(); if (addonId != current.addonId) { @@ -159,16 +159,16 @@ void ListModel::versionRequestSucceeded(QJsonDocument doc, QString addonId, cons m_associated_page->updateVersionList(); } -} // namespace ModPlatform - /******** Helpers ********/ #define MOD_PAGE(x) static_cast(x) -auto ModPlatform::ListModel::getMineVersions() const -> std::optional> +auto ModModel::getMineVersions() const -> std::optional> { auto versions = MOD_PAGE(m_associated_page)->getFilter()->versions; if (!versions.empty()) return versions; return {}; } + +} // namespace ResourceDownload diff --git a/launcher/ui/pages/modplatform/ModModel.h b/launcher/ui/pages/modplatform/ModModel.h index 7c735d90..e3d760a2 100644 --- a/launcher/ui/pages/modplatform/ModModel.h +++ b/launcher/ui/pages/modplatform/ModModel.h @@ -7,16 +7,17 @@ #include "ui/pages/modplatform/ResourceModel.h" -class ModPage; class Version; -namespace ModPlatform { +namespace ResourceDownload { -class ListModel : public ResourceModel { +class ModPage; + +class ModModel : public ResourceModel { Q_OBJECT public: - ListModel(ModPage* parent, ResourceAPI* api); + ModModel(ModPage* parent, ResourceAPI* api); /* Ask the API for more information */ void searchWithTerm(const QString& term, const int sort, const bool filter_changed); @@ -51,4 +52,5 @@ class ListModel : public ResourceModel { protected: int currentSort = 0; }; -} // namespace ModPlatform + +} // namespace ResourceDownload diff --git a/launcher/ui/pages/modplatform/ModPage.cpp b/launcher/ui/pages/modplatform/ModPage.cpp index 853f2c54..8941d9b7 100644 --- a/launcher/ui/pages/modplatform/ModPage.cpp +++ b/launcher/ui/pages/modplatform/ModPage.cpp @@ -53,6 +53,8 @@ #include "ui/pages/modplatform/ModModel.h" +namespace ResourceDownload { + ModPage::ModPage(ModDownloadDialog* dialog, BaseInstance& instance) : ResourcePage(dialog, instance) { @@ -100,7 +102,7 @@ void ModPage::triggerSearch() updateSelectionButton(); } - static_cast(m_model)->searchWithTerm(getSearchTerm(), m_ui->sortByBox->currentIndex(), changed); + static_cast(m_model)->searchWithTerm(getSearchTerm(), m_ui->sortByBox->currentIndex(), changed); m_fetch_progress.watch(&m_model->activeJob()); } @@ -151,3 +153,5 @@ void ModPage::addResourceToDialog(ModPlatform::IndexedPack& pack, ModPlatform::I bool is_indexed = !APPLICATION->settings()->get("ModMetadataDisabled").toBool(); m_parent_dialog->addResource(pack.name, new ResourceDownloadTask(pack, version, m_parent_dialog->getBaseModel(), is_indexed)); } + +} // namespace ResourceDownload diff --git a/launcher/ui/pages/modplatform/ModPage.h b/launcher/ui/pages/modplatform/ModPage.h index 8c1fec84..137a6046 100644 --- a/launcher/ui/pages/modplatform/ModPage.h +++ b/launcher/ui/pages/modplatform/ModPage.h @@ -7,12 +7,14 @@ #include "ui/pages/modplatform/ResourcePage.h" #include "ui/widgets/ModFilterWidget.h" -class ModDownloadDialog; - namespace Ui { class ResourcePage; } +namespace ResourceDownload { + +class ModDownloadDialog; + /* This page handles most logic related to browsing and selecting mods to download. */ class ModPage : public ResourcePage { Q_OBJECT @@ -57,3 +59,5 @@ class ModPage : public ResourcePage { unique_qobject_ptr m_filter_widget; std::shared_ptr m_filter; }; + +} // namespace ResourceDownload diff --git a/launcher/ui/pages/modplatform/ResourceModel.cpp b/launcher/ui/pages/modplatform/ResourceModel.cpp index d672a2ac..e8af0e7a 100644 --- a/launcher/ui/pages/modplatform/ResourceModel.cpp +++ b/launcher/ui/pages/modplatform/ResourceModel.cpp @@ -20,6 +20,8 @@ #include "ui/pages/modplatform/ResourcePage.h" #include "ui/widgets/ProjectItem.h" +namespace ResourceDownload { + QHash ResourceModel::s_running_models; ResourceModel::ResourceModel(ResourcePage* parent, ResourceAPI* api) : QAbstractListModel(), m_api(api), m_associated_page(parent) @@ -256,3 +258,5 @@ void ResourceModel::searchRequestAborted() m_next_search_offset = 0; search(); } + +} // namespace ResourceDownload diff --git a/launcher/ui/pages/modplatform/ResourceModel.h b/launcher/ui/pages/modplatform/ResourceModel.h index af0e9f55..6a94c399 100644 --- a/launcher/ui/pages/modplatform/ResourceModel.h +++ b/launcher/ui/pages/modplatform/ResourceModel.h @@ -9,13 +9,15 @@ #include "tasks/ConcurrentTask.h" class NetJob; -class ResourcePage; class ResourceAPI; namespace ModPlatform { struct IndexedPack; } +namespace ResourceDownload { + +class ResourcePage; class ResourceModel : public QAbstractListModel { Q_OBJECT @@ -99,3 +101,5 @@ class ResourceModel : public QAbstractListModel { void searchRequestFailed(QString reason, int network_error_code); void searchRequestAborted(); }; + +} // namespace ResourceDownload diff --git a/launcher/ui/pages/modplatform/ResourcePage.cpp b/launcher/ui/pages/modplatform/ResourcePage.cpp index 3b382d20..161b5c22 100644 --- a/launcher/ui/pages/modplatform/ResourcePage.cpp +++ b/launcher/ui/pages/modplatform/ResourcePage.cpp @@ -13,6 +13,8 @@ #include "ui/pages/modplatform/ResourceModel.h" #include "ui/widgets/ProjectItem.h" +namespace ResourceDownload { + ResourcePage::ResourcePage(ResourceDownloadDialog* parent, BaseInstance& base_instance) : QWidget(parent), m_base_instance(base_instance), m_ui(new Ui::ResourcePage), m_parent_dialog(parent), m_fetch_progress(this, false) { @@ -345,3 +347,5 @@ void ResourcePage::openUrl(const QUrl& url) // open in the user's web browser QDesktopServices::openUrl(url); } + +} // namespace ResourceDownload diff --git a/launcher/ui/pages/modplatform/ResourcePage.h b/launcher/ui/pages/modplatform/ResourcePage.h index 32aad3d9..f731cf56 100644 --- a/launcher/ui/pages/modplatform/ResourcePage.h +++ b/launcher/ui/pages/modplatform/ResourcePage.h @@ -14,8 +14,11 @@ class ResourcePage; } class BaseInstance; -class ResourceModel; + +namespace ResourceDownload { + class ResourceDownloadDialog; +class ResourceModel; class ResourcePage : public QWidget, public BasePage { Q_OBJECT @@ -93,3 +96,5 @@ class ResourcePage : public QWidget, public BasePage { // Used to do instant searching with a delay to cache quick changes QTimer m_search_timer; }; + +} // namespace ResourceDownload diff --git a/launcher/ui/pages/modplatform/flame/FlameResourceModels.cpp b/launcher/ui/pages/modplatform/flame/FlameResourceModels.cpp index b602dfac..cfe4080a 100644 --- a/launcher/ui/pages/modplatform/flame/FlameResourceModels.cpp +++ b/launcher/ui/pages/modplatform/flame/FlameResourceModels.cpp @@ -1,31 +1,35 @@ #include "FlameResourceModels.h" + #include "Json.h" + #include "modplatform/flame/FlameModIndex.h" -namespace FlameMod { +namespace ResourceDownload { // NOLINTNEXTLINE(modernize-avoid-c-arrays) -const char* ListModel::sorts[6]{ "Featured", "Popularity", "LastUpdated", "Name", "Author", "TotalDownloads" }; +const char* FlameModModel::sorts[6]{ "Featured", "Popularity", "LastUpdated", "Name", "Author", "TotalDownloads" }; -void ListModel::loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) +FlameModModel::FlameModModel(FlameModPage* parent) : ModModel(parent, new FlameAPI) {} + +void FlameModModel::loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) { FlameMod::loadIndexedPack(m, obj); } // We already deal with the URLs when initializing the pack, due to the API response's structure -void ListModel::loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj) +void FlameModModel::loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj) { FlameMod::loadBody(m, obj); } -void ListModel::loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) +void FlameModModel::loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) { FlameMod::loadIndexedPackVersions(m, arr, APPLICATION->network(), &m_associated_page->m_base_instance); } -auto ListModel::documentToArray(QJsonDocument& obj) const -> QJsonArray +auto FlameModModel::documentToArray(QJsonDocument& obj) const -> QJsonArray { return Json::ensureArray(obj.object(), "data"); } -} // namespace FlameMod +} // namespace ResourceDownload diff --git a/launcher/ui/pages/modplatform/flame/FlameResourceModels.h b/launcher/ui/pages/modplatform/flame/FlameResourceModels.h index b94377d3..501937e2 100644 --- a/launcher/ui/pages/modplatform/flame/FlameResourceModels.h +++ b/launcher/ui/pages/modplatform/flame/FlameResourceModels.h @@ -2,14 +2,18 @@ #include "modplatform/flame/FlameAPI.h" -namespace FlameMod { +#include "ui/pages/modplatform/ModModel.h" -class ListModel : public ModPlatform::ListModel { +#include "ui/pages/modplatform/flame/FlameResourcePages.h" + +namespace ResourceDownload { + +class FlameModModel : public ModModel { Q_OBJECT public: - ListModel(FlameModPage* parent) : ModPlatform::ListModel(parent, new FlameAPI) {} - ~ListModel() override = default; + FlameModModel(FlameModPage* parent); + ~FlameModModel() override = default; private: void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) override; @@ -23,4 +27,4 @@ class ListModel : public ModPlatform::ListModel { inline auto getSorts() const -> const char** override { return sorts; }; }; -} // namespace FlameMod +} // namespace ResourceDownload diff --git a/launcher/ui/pages/modplatform/flame/FlameResourcePages.cpp b/launcher/ui/pages/modplatform/flame/FlameResourcePages.cpp index 490578ad..723819fb 100644 --- a/launcher/ui/pages/modplatform/flame/FlameResourcePages.cpp +++ b/launcher/ui/pages/modplatform/flame/FlameResourcePages.cpp @@ -40,10 +40,12 @@ #include "FlameResourceModels.h" #include "ui/dialogs/ModDownloadDialog.h" +namespace ResourceDownload { + FlameModPage::FlameModPage(ModDownloadDialog* dialog, BaseInstance& instance) : ModPage(dialog, instance) { - m_model = new FlameMod::ListModel(this); + m_model = new FlameModModel(this); m_ui->packView->setModel(m_model); // index is used to set the sorting with the flame api @@ -95,3 +97,5 @@ void FlameModPage::openUrl(const QUrl& url) ModPage::openUrl(url); } + +} // namespace ResourceDownload diff --git a/launcher/ui/pages/modplatform/flame/FlameResourcePages.h b/launcher/ui/pages/modplatform/flame/FlameResourcePages.h index 597a0c25..6c7d0247 100644 --- a/launcher/ui/pages/modplatform/flame/FlameResourcePages.h +++ b/launcher/ui/pages/modplatform/flame/FlameResourcePages.h @@ -42,6 +42,16 @@ #include "ui/pages/modplatform/ModPage.h" +namespace ResourceDownload { + +namespace Flame { +static inline QString displayName() { return "CurseForge"; } +static inline QIcon icon() { return APPLICATION->getThemedIcon("flame"); } +static inline QString id() { return "curseforge"; } +static inline QString debugName() { return "Flame"; } +static inline QString metaEntryBase() { return "FlameMods"; }; +} + class FlameModPage : public ModPage { Q_OBJECT @@ -54,18 +64,20 @@ class FlameModPage : public ModPage { FlameModPage(ModDownloadDialog* dialog, BaseInstance& instance); ~FlameModPage() override = default; - inline auto displayName() const -> QString override { return "CurseForge"; } - inline auto icon() const -> QIcon override { return APPLICATION->getThemedIcon("flame"); } - inline auto id() const -> QString override { return "curseforge"; } - inline auto helpPage() const -> QString override { return "Mod-platform"; } + [[nodiscard]] bool shouldDisplay() const override; - inline auto debugName() const -> QString override { return "Flame"; } - inline auto metaEntryBase() const -> QString override { return "FlameMods"; }; + [[nodiscard]] inline auto displayName() const -> QString override { return Flame::displayName(); } + [[nodiscard]] inline auto icon() const -> QIcon override { return Flame::icon(); } + [[nodiscard]] inline auto id() const -> QString override { return Flame::id(); } + [[nodiscard]] inline auto debugName() const -> QString override { return Flame::debugName(); } + [[nodiscard]] inline auto metaEntryBase() const -> QString override { return Flame::metaEntryBase(); } - auto validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, std::optional loaders = {}) const -> bool override; + [[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; - auto shouldDisplay() const -> bool override; - void openUrl(const QUrl& url) override; }; + +} // namespace ResourceDownload diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthResourceModels.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthResourceModels.cpp index 51278546..ee96f0de 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthResourceModels.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthResourceModels.cpp @@ -23,31 +23,31 @@ #include "modplatform/modrinth/ModrinthAPI.h" #include "modplatform/modrinth/ModrinthPackIndex.h" -namespace Modrinth { +namespace ResourceDownload { // NOLINTNEXTLINE(modernize-avoid-c-arrays) -const char* ListModel::sorts[5]{ "relevance", "downloads", "follows", "updated", "newest" }; +const char* ModrinthModModel::sorts[5]{ "relevance", "downloads", "follows", "updated", "newest" }; -void ListModel::loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) +ModrinthModModel::ModrinthModModel(ModrinthModPage* parent) : ModModel(parent, new ModrinthAPI){}; + +void ModrinthModModel::loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) { - Modrinth::loadIndexedPack(m, obj); + ::Modrinth::loadIndexedPack(m, obj); } -void ListModel::loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj) +void ModrinthModModel::loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj) { - Modrinth::loadExtraPackData(m, obj); + ::Modrinth::loadExtraPackData(m, obj); } -void ListModel::loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) +void ModrinthModModel::loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) { - Modrinth::loadIndexedPackVersions(m, arr, APPLICATION->network(), &m_associated_page->m_base_instance); + ::Modrinth::loadIndexedPackVersions(m, arr, APPLICATION->network(), &m_associated_page->m_base_instance); } -auto ListModel::documentToArray(QJsonDocument& obj) const -> QJsonArray +auto ModrinthModModel::documentToArray(QJsonDocument& obj) const -> QJsonArray { return obj.object().value("hits").toArray(); } -} // namespace Modrinth - - +} // namespace ResourceDownload diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthResourceModels.h b/launcher/ui/pages/modplatform/modrinth/ModrinthResourceModels.h index bf62d22f..b0088a73 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthResourceModels.h +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthResourceModels.h @@ -20,24 +20,22 @@ #include "ui/pages/modplatform/ModModel.h" -#include "ui/pages/modplatform/modrinth/ModrinthResourcePages.h" +namespace ResourceDownload { -#include "modplatform/modrinth/ModrinthAPI.h" +class ModrinthModPage; -namespace Modrinth { - -class ListModel : public ModPlatform::ListModel { +class ModrinthModModel : public ModModel { Q_OBJECT public: - ListModel(ModrinthModPage* parent) : ModPlatform::ListModel(parent, new ModrinthAPI){}; - ~ListModel() override = default; + ModrinthModModel(ModrinthModPage* parent); + ~ModrinthModModel() override = default; private: void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) override; void loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj) override; void loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) override; - + auto documentToArray(QJsonDocument& obj) const -> QJsonArray override; // NOLINTNEXTLINE(modernize-avoid-c-arrays) @@ -45,5 +43,4 @@ class ListModel : public ModPlatform::ListModel { inline auto getSorts() const -> const char** override { return sorts; }; }; -} // namespace Modrinth - +} // namespace ResourceDownload diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.cpp index 17f0bc93..5d2680b0 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.cpp @@ -38,13 +38,16 @@ #include "modplatform/modrinth/ModrinthAPI.h" -#include "ModrinthResourceModels.h" #include "ui/dialogs/ModDownloadDialog.h" +#include "ui/pages/modplatform/modrinth/ModrinthResourceModels.h" + +namespace ResourceDownload { + ModrinthModPage::ModrinthModPage(ModDownloadDialog* dialog, BaseInstance& instance) : ModPage(dialog, instance) { - m_model = new Modrinth::ListModel(this); + m_model = new ModrinthModModel(this); m_ui->packView->setModel(m_model); // index is used to set the sorting with the modrinth api @@ -87,3 +90,4 @@ auto ModrinthModPage::validateVersion(ModPlatform::IndexedVersion& ver, QString // my Qt, so we need to implement this in every derived class... auto ModrinthModPage::shouldDisplay() const -> bool { return true; } +} // namespace ResourceDownload diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.h b/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.h index 6f816cfd..07b32c0c 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.h +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.h @@ -41,11 +41,15 @@ #include "ui/pages/modplatform/ModPage.h" +namespace ResourceDownload { + +namespace Modrinth { static inline QString displayName() { return "Modrinth"; } static inline QIcon icon() { return APPLICATION->getThemedIcon("modrinth"); } static inline QString id() { return "modrinth"; } static inline QString debugName() { return "Modrinth"; } static inline QString metaEntryBase() { return "ModrinthPacks"; }; +} class ModrinthModPage : public ModPage { Q_OBJECT @@ -61,12 +65,15 @@ class ModrinthModPage : public ModPage { [[nodiscard]] bool shouldDisplay() const override; - [[nodiscard]] inline auto displayName() const -> QString override { return ::displayName(); } \ - [[nodiscard]] inline auto icon() const -> QIcon override { return ::icon(); } \ - [[nodiscard]] inline auto id() const -> QString override { return ::id(); } \ - [[nodiscard]] inline auto debugName() const -> QString override { return ::debugName(); } \ - [[nodiscard]] inline auto metaEntryBase() const -> QString override { return ::metaEntryBase(); } - inline auto helpPage() const -> QString override { return "Mod-platform"; } + [[nodiscard]] inline auto displayName() const -> QString override { return Modrinth::displayName(); } + [[nodiscard]] inline auto icon() const -> QIcon override { return Modrinth::icon(); } + [[nodiscard]] inline auto id() const -> QString override { return Modrinth::id(); } + [[nodiscard]] inline auto debugName() const -> QString override { return Modrinth::debugName(); } + [[nodiscard]] inline auto metaEntryBase() const -> QString override { return Modrinth::metaEntryBase(); } + + [[nodiscard]] inline auto helpPage() const -> QString override { return "Mod-platform"; } auto validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, std::optional loaders = {}) const -> bool override; }; + +} // namespace ResourceDownload From ef87bdf18acb549c1ad9a3eda69d8dff5ad8da8e Mon Sep 17 00:00:00 2001 From: flow Date: Fri, 16 Dec 2022 20:19:33 -0300 Subject: [PATCH 088/152] fix(RD): prevent weird behavior of progress widget when i.e. clicking on links or just using the downloader at all, this prevents some flickering and the widget never getting hidden in some cases. Signed-off-by: flow --- launcher/ui/widgets/ProgressWidget.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/launcher/ui/widgets/ProgressWidget.cpp b/launcher/ui/widgets/ProgressWidget.cpp index 18b51fc3..f736af08 100644 --- a/launcher/ui/widgets/ProgressWidget.cpp +++ b/launcher/ui/widgets/ProgressWidget.cpp @@ -54,7 +54,10 @@ void ProgressWidget::watch(const Task* task) connect(m_task, &Task::progress, this, &ProgressWidget::handleTaskProgress); connect(m_task, &Task::destroyed, this, &ProgressWidget::taskDestroyed); - show(); + if (m_task->isRunning()) + show(); + else + connect(m_task, &Task::started, this, &ProgressWidget::show); } void ProgressWidget::start(const Task* task) From 39b7ac90d40eb53d7b88ef99b0fa46fb3e1840b9 Mon Sep 17 00:00:00 2001 From: flow Date: Fri, 16 Dec 2022 21:44:21 -0300 Subject: [PATCH 089/152] refactor(RD): unify download dialogs into a single file No need for multiple files since the subclasses are so small now Signed-off-by: flow --- launcher/CMakeLists.txt | 2 - launcher/ui/dialogs/ModDownloadDialog.cpp | 63 ----------------- launcher/ui/dialogs/ModDownloadDialog.h | 50 -------------- .../ui/dialogs/ResourceDownloadDialog.cpp | 67 +++++++++++++++++++ launcher/ui/dialogs/ResourceDownloadDialog.h | 51 +++++++++++++- launcher/ui/pages/instance/ModFolderPage.cpp | 2 +- launcher/ui/pages/modplatform/ModPage.cpp | 2 +- .../modplatform/flame/FlameResourcePages.cpp | 2 +- .../modplatform/modrinth/ModrinthModel.cpp | 1 - .../modrinth/ModrinthResourcePages.cpp | 2 +- 10 files changed, 120 insertions(+), 122 deletions(-) delete mode 100644 launcher/ui/dialogs/ModDownloadDialog.cpp delete mode 100644 launcher/ui/dialogs/ModDownloadDialog.h diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index a1a68f5b..77c69106 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -876,8 +876,6 @@ SET(LAUNCHER_SOURCES ui/dialogs/SkinUploadDialog.h ui/dialogs/ResourceDownloadDialog.cpp ui/dialogs/ResourceDownloadDialog.h - ui/dialogs/ModDownloadDialog.cpp - ui/dialogs/ModDownloadDialog.h ui/dialogs/ScrollMessageBox.cpp ui/dialogs/ScrollMessageBox.h ui/dialogs/BlockedModsDialog.cpp diff --git a/launcher/ui/dialogs/ModDownloadDialog.cpp b/launcher/ui/dialogs/ModDownloadDialog.cpp deleted file mode 100644 index 89b87300..00000000 --- a/launcher/ui/dialogs/ModDownloadDialog.cpp +++ /dev/null @@ -1,63 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-only -/* - * Prism Launcher - Minecraft Launcher - * Copyright (C) 2022 Sefa Eyeoglu - * Copyright (C) 2022 TheKodeToad - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * 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 "ModDownloadDialog.h" - -#include "Application.h" - -#include "ui/pages/modplatform/flame/FlameResourcePages.h" -#include "ui/pages/modplatform/modrinth/ModrinthResourcePages.h" - -namespace ResourceDownload { - -ModDownloadDialog::ModDownloadDialog(QWidget* parent, const std::shared_ptr& mods, BaseInstance* instance) - : ResourceDownloadDialog(parent, mods), m_instance(instance) -{ - initializeContainer(); - connectButtons(); - - restoreGeometry(QByteArray::fromBase64(APPLICATION->settings()->get("ModDownloadGeometry").toByteArray())); -} - -void ModDownloadDialog::accept() -{ - APPLICATION->settings()->set("ModDownloadGeometry", saveGeometry().toBase64()); - QDialog::accept(); -} - -void ModDownloadDialog::reject() -{ - APPLICATION->settings()->set("ModDownloadGeometry", saveGeometry().toBase64()); - QDialog::reject(); -} - -QList ModDownloadDialog::getPages() -{ - QList pages; - - pages.append(ModrinthModPage::create(this, *m_instance)); - if (APPLICATION->capabilities() & Application::SupportsFlame) - pages.append(FlameModPage::create(this, *m_instance)); - - m_selectedPage = dynamic_cast(pages[0]); - - return pages; -} - -} // namespace ResourceDownload diff --git a/launcher/ui/dialogs/ModDownloadDialog.h b/launcher/ui/dialogs/ModDownloadDialog.h deleted file mode 100644 index b378b5a9..00000000 --- a/launcher/ui/dialogs/ModDownloadDialog.h +++ /dev/null @@ -1,50 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-only -/* - * Prism Launcher - Minecraft Launcher - * Copyright (C) 2022 Sefa Eyeoglu - * Copyright (C) 2022 TheKodeToad - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * 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 "minecraft/mod/ModFolderModel.h" - -#include "ui/dialogs/ResourceDownloadDialog.h" - -class QDialogButtonBox; - -namespace ResourceDownload { - -class ModDownloadDialog final : public ResourceDownloadDialog { - Q_OBJECT - - public: - explicit ModDownloadDialog(QWidget* parent, const std::shared_ptr& mods, BaseInstance* instance); - ~ModDownloadDialog() override = default; - - //: String that gets appended to the mod download dialog title ("Download " + resourcesString()) - [[nodiscard]] QString resourceString() const override { return tr("mods"); } - - QList getPages() override; - - public slots: - void accept() override; - void reject() override; - - private: - BaseInstance* m_instance; -}; - -} // namespace ResourceDownload diff --git a/launcher/ui/dialogs/ResourceDownloadDialog.cpp b/launcher/ui/dialogs/ResourceDownloadDialog.cpp index b143750b..523a1636 100644 --- a/launcher/ui/dialogs/ResourceDownloadDialog.cpp +++ b/launcher/ui/dialogs/ResourceDownloadDialog.cpp @@ -1,3 +1,22 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu + * Copyright (C) 2022 TheKodeToad + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * 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 "ResourceDownloadDialog.h" #include @@ -5,8 +24,15 @@ #include "Application.h" #include "ResourceDownloadTask.h" +#include "minecraft/mod/ModFolderModel.h" + #include "ui/dialogs/ReviewMessageBox.h" + #include "ui/pages/modplatform/ResourcePage.h" + +#include "ui/pages/modplatform/flame/FlameResourcePages.h" +#include "ui/pages/modplatform/modrinth/ModrinthResourcePages.h" + #include "ui/widgets/PageContainer.h" namespace ResourceDownload { @@ -41,6 +67,22 @@ ResourceDownloadDialog::ResourceDownloadDialog(QWidget* parent, const std::share setWindowTitle(dialogTitle()); } +void ResourceDownloadDialog::accept() +{ + if (!geometrySaveKey().isEmpty()) + APPLICATION->settings()->set(geometrySaveKey(), saveGeometry().toBase64()); + + QDialog::accept(); +} + +void ResourceDownloadDialog::reject() +{ + if (!geometrySaveKey().isEmpty()) + APPLICATION->settings()->set(geometrySaveKey(), saveGeometry().toBase64()); + + QDialog::reject(); +} + // NOTE: We can't have this in the ctor because PageContainer calls a virtual function, and so // won't work with subclasses if we put it in this ctor. void ResourceDownloadDialog::initializeContainer() @@ -153,4 +195,29 @@ void ResourceDownloadDialog::selectedPageChanged(BasePage* previous, BasePage* s m_selectedPage->setSearchTerm(prev_page->getSearchTerm()); } + + +ModDownloadDialog::ModDownloadDialog(QWidget* parent, const std::shared_ptr& mods, BaseInstance* instance) + : ResourceDownloadDialog(parent, mods), m_instance(instance) +{ + initializeContainer(); + connectButtons(); + + if (!geometrySaveKey().isEmpty()) + restoreGeometry(QByteArray::fromBase64(APPLICATION->settings()->get(geometrySaveKey()).toByteArray())); +} + +QList ModDownloadDialog::getPages() +{ + QList pages; + + pages.append(ModrinthModPage::create(this, *m_instance)); + if (APPLICATION->capabilities() & Application::SupportsFlame) + pages.append(FlameModPage::create(this, *m_instance)); + + m_selectedPage = dynamic_cast(pages[0]); + + return pages; +} + } // namespace ResourceDownload diff --git a/launcher/ui/dialogs/ResourceDownloadDialog.h b/launcher/ui/dialogs/ResourceDownloadDialog.h index 3b234cd1..29813493 100644 --- a/launcher/ui/dialogs/ResourceDownloadDialog.h +++ b/launcher/ui/dialogs/ResourceDownloadDialog.h @@ -1,3 +1,22 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu + * Copyright (C) 2022 TheKodeToad + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * 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 @@ -6,11 +25,13 @@ #include "ui/pages/BasePageProvider.h" -class ResourceDownloadTask; -class ResourceFolderModel; +class BaseInstance; +class ModFolderModel; class PageContainer; class QVBoxLayout; class QDialogButtonBox; +class ResourceDownloadTask; +class ResourceFolderModel; namespace ResourceDownload { @@ -40,11 +61,18 @@ class ResourceDownloadDialog : public QDialog, public BasePageProvider { const QList getTasks(); [[nodiscard]] const std::shared_ptr getBaseModel() const { return m_base_model; } + public slots: + void accept() override; + void reject() override; + protected slots: void selectedPageChanged(BasePage* previous, BasePage* selected); virtual void confirm(); + protected: + [[nodiscard]] virtual QString geometrySaveKey() const { return ""; } + protected: const std::shared_ptr m_base_model; @@ -57,4 +85,23 @@ class ResourceDownloadDialog : public QDialog, public BasePageProvider { QHash m_selected; }; + + +class ModDownloadDialog final : public ResourceDownloadDialog { + Q_OBJECT + + public: + explicit ModDownloadDialog(QWidget* parent, const std::shared_ptr& mods, BaseInstance* instance); + ~ModDownloadDialog() override = default; + + //: String that gets appended to the mod download dialog title ("Download " + resourcesString()) + [[nodiscard]] QString resourceString() const override { return tr("mods"); } + [[nodiscard]] QString geometrySaveKey() const override { return "ModDownloadGeometry"; } + + QList getPages() override; + + private: + BaseInstance* m_instance; +}; + } // namespace ResourceDownload diff --git a/launcher/ui/pages/instance/ModFolderPage.cpp b/launcher/ui/pages/instance/ModFolderPage.cpp index 7c4b8952..d9069915 100644 --- a/launcher/ui/pages/instance/ModFolderPage.cpp +++ b/launcher/ui/pages/instance/ModFolderPage.cpp @@ -49,8 +49,8 @@ #include "ui/GuiUtil.h" #include "ui/dialogs/CustomMessageBox.h" -#include "ui/dialogs/ModDownloadDialog.h" #include "ui/dialogs/ModUpdateDialog.h" +#include "ui/dialogs/ResourceDownloadDialog.h" #include "DesktopServices.h" diff --git a/launcher/ui/pages/modplatform/ModPage.cpp b/launcher/ui/pages/modplatform/ModPage.cpp index 8941d9b7..8d441546 100644 --- a/launcher/ui/pages/modplatform/ModPage.cpp +++ b/launcher/ui/pages/modplatform/ModPage.cpp @@ -49,7 +49,7 @@ #include "minecraft/MinecraftInstance.h" #include "minecraft/PackProfile.h" -#include "ui/dialogs/ModDownloadDialog.h" +#include "ui/dialogs/ResourceDownloadDialog.h" #include "ui/pages/modplatform/ModModel.h" diff --git a/launcher/ui/pages/modplatform/flame/FlameResourcePages.cpp b/launcher/ui/pages/modplatform/flame/FlameResourcePages.cpp index 723819fb..2a8ab526 100644 --- a/launcher/ui/pages/modplatform/flame/FlameResourcePages.cpp +++ b/launcher/ui/pages/modplatform/flame/FlameResourcePages.cpp @@ -38,7 +38,7 @@ #include "ui_ResourcePage.h" #include "FlameResourceModels.h" -#include "ui/dialogs/ModDownloadDialog.h" +#include "ui/dialogs/ResourceDownloadDialog.h" namespace ResourceDownload { diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp index e6704eef..80850b4c 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp @@ -40,7 +40,6 @@ #include "Json.h" #include "minecraft/MinecraftInstance.h" #include "minecraft/PackProfile.h" -#include "ui/dialogs/ModDownloadDialog.h" #include "ui/widgets/ProjectItem.h" #include diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.cpp index 5d2680b0..1352e2f6 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.cpp @@ -38,7 +38,7 @@ #include "modplatform/modrinth/ModrinthAPI.h" -#include "ui/dialogs/ModDownloadDialog.h" +#include "ui/dialogs/ResourceDownloadDialog.h" #include "ui/pages/modplatform/modrinth/ModrinthResourceModels.h" From 45d1319891ce87cc1546a316ad550f892d411633 Mon Sep 17 00:00:00 2001 From: flow Date: Sun, 18 Dec 2022 15:41:46 -0300 Subject: [PATCH 090/152] refactor(RD): decouple ResourceModels from ResourcePages This makes it so that we don't need a reference to the parent page in the model. It will be useful once we change the page from a widget-based one to a QML page. It also makes tasks be created in the dialog instead of the page, so that the dialog can also have the necessary information to mark versions as selected / deselected easily. It also makes the task pointers into smart pointers. Signed-off-by: flow --- launcher/ResourceDownloadTask.h | 1 + launcher/modplatform/ModIndex.h | 20 +++++ launcher/modplatform/ResourceAPI.h | 11 ++- launcher/modplatform/flame/FlameAPI.cpp | 2 +- launcher/modplatform/flame/FlameAPI.h | 2 +- .../modplatform/flame/FlameCheckUpdate.cpp | 3 +- launcher/modplatform/flame/FlameModIndex.cpp | 4 +- launcher/modplatform/flame/FlameModIndex.h | 2 +- .../helpers/NetworkResourceAPI.cpp | 4 +- launcher/modplatform/modrinth/ModrinthAPI.h | 2 +- .../modrinth/ModrinthPackIndex.cpp | 4 +- .../modplatform/modrinth/ModrinthPackIndex.h | 2 +- .../ui/dialogs/ResourceDownloadDialog.cpp | 50 +++++++----- launcher/ui/dialogs/ResourceDownloadDialog.h | 13 +-- launcher/ui/pages/modplatform/ModModel.cpp | 81 ++++++++++--------- launcher/ui/pages/modplatform/ModModel.h | 13 +-- launcher/ui/pages/modplatform/ModPage.cpp | 4 +- launcher/ui/pages/modplatform/ModPage.h | 6 ++ .../ui/pages/modplatform/ResourceModel.cpp | 19 ++--- launcher/ui/pages/modplatform/ResourceModel.h | 18 +++-- .../ui/pages/modplatform/ResourcePage.cpp | 31 ++++--- launcher/ui/pages/modplatform/ResourcePage.h | 4 +- .../modplatform/flame/FlameResourceModels.cpp | 5 +- .../modplatform/flame/FlameResourceModels.h | 8 +- .../modplatform/flame/FlameResourcePages.cpp | 2 +- .../modrinth/ModrinthResourceModels.cpp | 6 +- .../modrinth/ModrinthResourceModels.h | 6 +- .../modrinth/ModrinthResourcePages.cpp | 2 +- 28 files changed, 187 insertions(+), 138 deletions(-) diff --git a/launcher/ResourceDownloadTask.h b/launcher/ResourceDownloadTask.h index 350c2edd..275ddbe1 100644 --- a/launcher/ResourceDownloadTask.h +++ b/launcher/ResourceDownloadTask.h @@ -32,6 +32,7 @@ class ResourceDownloadTask : public SequentialTask { public: explicit ResourceDownloadTask(ModPlatform::IndexedPack pack, ModPlatform::IndexedVersion version, const std::shared_ptr packs, bool is_indexed = true); const QString& getFilename() const { return m_pack_version.fileName; } + const QVariant& getVersionID() const { return m_pack_version.fileId; } private: ModPlatform::IndexedPack m_pack; diff --git a/launcher/modplatform/ModIndex.h b/launcher/modplatform/ModIndex.h index f65a6a4b..cd40a6ba 100644 --- a/launcher/modplatform/ModIndex.h +++ b/launcher/modplatform/ModIndex.h @@ -65,6 +65,9 @@ struct IndexedVersion { QString hash; bool is_preferred = true; QString changelog; + + // For internal use, not provided by APIs + bool is_currently_selected = false; }; struct ExtraPackData { @@ -95,6 +98,23 @@ struct IndexedPack { // Don't load by default, since some modplatform don't have that info bool extraDataLoaded = true; ExtraPackData extraData; + + // For internal use, not provided by APIs + [[nodiscard]] bool isVersionSelected(size_t index) const + { + if (!versionsLoaded) + return false; + + return versions.at(index).is_currently_selected; + } + [[nodiscard]] bool isAnyVersionSelected() const + { + if (!versionsLoaded) + return false; + + return std::any_of(versions.constBegin(), versions.constEnd(), + [](auto const& v) { return v.is_currently_selected; }); + } }; } // namespace ModPlatform diff --git a/launcher/modplatform/ResourceAPI.h b/launcher/modplatform/ResourceAPI.h index d18a2caa..49aac712 100644 --- a/launcher/modplatform/ResourceAPI.h +++ b/launcher/modplatform/ResourceAPI.h @@ -69,13 +69,20 @@ class ResourceAPI { }; struct VersionSearchArgs { - QString addonId; + ModPlatform::IndexedPack& pack; std::optional > mcVersions; std::optional loaders; + + void operator=(VersionSearchArgs other) + { + pack = other.pack; + mcVersions = other.mcVersions; + loaders = other.loaders; + } }; struct VersionSearchCallbacks { - std::function on_succeed; + std::function on_succeed; }; struct ProjectInfoArgs { diff --git a/launcher/modplatform/flame/FlameAPI.cpp b/launcher/modplatform/flame/FlameAPI.cpp index ae401399..89249c41 100644 --- a/launcher/modplatform/flame/FlameAPI.cpp +++ b/launcher/modplatform/flame/FlameAPI.cpp @@ -114,7 +114,7 @@ auto FlameAPI::getLatestVersion(VersionSearchArgs&& args) -> ModPlatform::Indexe QEventLoop loop; - auto netJob = new NetJob(QString("Flame::GetLatestVersion(%1)").arg(args.addonId), APPLICATION->network()); + auto netJob = new NetJob(QString("Flame::GetLatestVersion(%1)").arg(args.pack.name), APPLICATION->network()); auto response = new QByteArray(); ModPlatform::IndexedVersion ver; diff --git a/launcher/modplatform/flame/FlameAPI.h b/launcher/modplatform/flame/FlameAPI.h index 114a2716..f3cc0bbf 100644 --- a/launcher/modplatform/flame/FlameAPI.h +++ b/launcher/modplatform/flame/FlameAPI.h @@ -78,7 +78,7 @@ class FlameAPI : public NetworkResourceAPI { [[nodiscard]] std::optional getVersionsURL(VersionSearchArgs const& args) const override { - QString url{QString("https://api.curseforge.com/v1/mods/%1/files?pageSize=10000&").arg(args.addonId)}; + QString url{QString("https://api.curseforge.com/v1/mods/%1/files?pageSize=10000&").arg(args.pack.addonId.toString())}; QStringList get_parameters; if (args.mcVersions.has_value()) diff --git a/launcher/modplatform/flame/FlameCheckUpdate.cpp b/launcher/modplatform/flame/FlameCheckUpdate.cpp index 285fa49f..7aee4f4c 100644 --- a/launcher/modplatform/flame/FlameCheckUpdate.cpp +++ b/launcher/modplatform/flame/FlameCheckUpdate.cpp @@ -129,7 +129,8 @@ void FlameCheckUpdate::executeTask() setStatus(tr("Getting API response from CurseForge for '%1'...").arg(mod->name())); setProgress(i++, m_mods.size()); - auto latest_ver = api.getLatestVersion({ mod->metadata()->project_id.toString(), m_game_versions, m_loaders }); + ModPlatform::IndexedPack pack{ mod->metadata()->project_id.toString() }; + auto latest_ver = api.getLatestVersion({ pack, m_game_versions, m_loaders }); // Check if we were aborted while getting the latest version if (m_was_aborted) { diff --git a/launcher/modplatform/flame/FlameModIndex.cpp b/launcher/modplatform/flame/FlameModIndex.cpp index 617b98ce..7498e830 100644 --- a/launcher/modplatform/flame/FlameModIndex.cpp +++ b/launcher/modplatform/flame/FlameModIndex.cpp @@ -76,10 +76,10 @@ static QString enumToString(int hash_algorithm) void FlameMod::loadIndexedPackVersions(ModPlatform::IndexedPack& pack, QJsonArray& arr, const shared_qobject_ptr& network, - BaseInstance* inst) + const BaseInstance* inst) { QVector unsortedVersions; - auto profile = (dynamic_cast(inst))->getPackProfile(); + auto profile = (dynamic_cast(inst))->getPackProfile(); QString mcVersion = profile->getComponentVersion("net.minecraft"); for (auto versionIter : arr) { diff --git a/launcher/modplatform/flame/FlameModIndex.h b/launcher/modplatform/flame/FlameModIndex.h index db63cdbb..33c4a529 100644 --- a/launcher/modplatform/flame/FlameModIndex.h +++ b/launcher/modplatform/flame/FlameModIndex.h @@ -17,7 +17,7 @@ void loadBody(ModPlatform::IndexedPack& m, QJsonObject& obj); void loadIndexedPackVersions(ModPlatform::IndexedPack& pack, QJsonArray& arr, const shared_qobject_ptr& network, - BaseInstance* inst); + const BaseInstance* inst); auto loadIndexedPackVersion(QJsonObject& obj, bool load_changelog = false) -> ModPlatform::IndexedVersion; } // namespace FlameMod diff --git a/launcher/modplatform/helpers/NetworkResourceAPI.cpp b/launcher/modplatform/helpers/NetworkResourceAPI.cpp index eb17008c..77b085c0 100644 --- a/launcher/modplatform/helpers/NetworkResourceAPI.cpp +++ b/launcher/modplatform/helpers/NetworkResourceAPI.cpp @@ -79,7 +79,7 @@ NetJob::Ptr NetworkResourceAPI::getProjectVersions(VersionSearchArgs&& args, Ver auto versions_url = versions_url_optional.value(); - auto netJob = new NetJob(QString("%1::Versions").arg(args.addonId), APPLICATION->network()); + auto netJob = new NetJob(QString("%1::Versions").arg(args.pack.name), APPLICATION->network()); auto response = new QByteArray(); netJob->addNetAction(Net::Download::makeByteArray(versions_url, response)); @@ -94,7 +94,7 @@ NetJob::Ptr NetworkResourceAPI::getProjectVersions(VersionSearchArgs&& args, Ver return; } - callbacks.on_succeed(doc, args.addonId); + callbacks.on_succeed(doc, args.pack); }); QObject::connect(netJob, &NetJob::finished, [response] { diff --git a/launcher/modplatform/modrinth/ModrinthAPI.h b/launcher/modplatform/modrinth/ModrinthAPI.h index bd84fb54..ec38d9ee 100644 --- a/launcher/modplatform/modrinth/ModrinthAPI.h +++ b/launcher/modplatform/modrinth/ModrinthAPI.h @@ -141,7 +141,7 @@ class ModrinthAPI : public NetworkResourceAPI { get_arguments.append(QString("loaders=[\"%1\"]").arg(getModLoaderStrings(args.loaders.value()).join("\",\""))); return QString("%1/project/%2/version%3%4") - .arg(BuildConfig.MODRINTH_PROD_URL, args.addonId, get_arguments.isEmpty() ? "" : "?", get_arguments.join('&')); + .arg(BuildConfig.MODRINTH_PROD_URL, args.pack.addonId.toString(), get_arguments.isEmpty() ? "" : "?", get_arguments.join('&')); }; auto getGameVersionsArray(std::list mcVersions) const -> QString diff --git a/launcher/modplatform/modrinth/ModrinthPackIndex.cpp b/launcher/modplatform/modrinth/ModrinthPackIndex.cpp index a0161089..f270f470 100644 --- a/launcher/modplatform/modrinth/ModrinthPackIndex.cpp +++ b/launcher/modplatform/modrinth/ModrinthPackIndex.cpp @@ -95,10 +95,10 @@ void Modrinth::loadExtraPackData(ModPlatform::IndexedPack& pack, QJsonObject& ob void Modrinth::loadIndexedPackVersions(ModPlatform::IndexedPack& pack, QJsonArray& arr, const shared_qobject_ptr& network, - BaseInstance* inst) + const BaseInstance* inst) { QVector unsortedVersions; - QString mcVersion = (static_cast(inst))->getPackProfile()->getComponentVersion("net.minecraft"); + QString mcVersion = (static_cast(inst))->getPackProfile()->getComponentVersion("net.minecraft"); for (auto versionIter : arr) { auto obj = versionIter.toObject(); diff --git a/launcher/modplatform/modrinth/ModrinthPackIndex.h b/launcher/modplatform/modrinth/ModrinthPackIndex.h index 31881414..e73e4b18 100644 --- a/launcher/modplatform/modrinth/ModrinthPackIndex.h +++ b/launcher/modplatform/modrinth/ModrinthPackIndex.h @@ -29,7 +29,7 @@ void loadExtraPackData(ModPlatform::IndexedPack& m, QJsonObject& obj); void loadIndexedPackVersions(ModPlatform::IndexedPack& pack, QJsonArray& arr, const shared_qobject_ptr& network, - BaseInstance* inst); + const BaseInstance* inst); auto loadIndexedPackVersion(QJsonObject& obj, QString hash_type = "sha512", QString filename_prefer = "") -> ModPlatform::IndexedVersion; } // namespace Modrinth diff --git a/launcher/ui/dialogs/ResourceDownloadDialog.cpp b/launcher/ui/dialogs/ResourceDownloadDialog.cpp index 523a1636..2eb85928 100644 --- a/launcher/ui/dialogs/ResourceDownloadDialog.cpp +++ b/launcher/ui/dialogs/ResourceDownloadDialog.cpp @@ -141,38 +141,44 @@ ResourcePage* ResourceDownloadDialog::getSelectedPage() return m_selectedPage; } -void ResourceDownloadDialog::addResource(QString name, ResourceDownloadTask* task) +void ResourceDownloadDialog::addResource(ModPlatform::IndexedPack& pack, ModPlatform::IndexedVersion& ver, bool is_indexed) { - removeResource(name); - m_selected.insert(name, task); + removeResource(pack, ver); + + ver.is_currently_selected = true; + m_selected.insert(pack.name, new ResourceDownloadTask(pack, ver, getBaseModel(), is_indexed)); m_buttons.button(QDialogButtonBox::Ok)->setEnabled(!m_selected.isEmpty()); } -void ResourceDownloadDialog::removeResource(QString name) +static ModPlatform::IndexedVersion& getVersionWithID(ModPlatform::IndexedPack& pack, QVariant id) { - if (m_selected.contains(name)) - m_selected.find(name).value()->deleteLater(); - m_selected.remove(name); + Q_ASSERT(pack.versionsLoaded); + auto it = std::find_if(pack.versions.begin(), pack.versions.end(), [id](auto const& v) { return v.fileId == id; }); + Q_ASSERT(it != pack.versions.end()); + return *it; +} + +void ResourceDownloadDialog::removeResource(ModPlatform::IndexedPack& pack, ModPlatform::IndexedVersion& ver) +{ + if (auto selected_task_it = m_selected.find(pack.name); selected_task_it != m_selected.end()) { + auto selected_task = *selected_task_it; + auto old_version_id = selected_task->getVersionID(); + + // If the new and old version IDs don't match, search for the old one and deselect it. + if (ver.fileId != old_version_id) + getVersionWithID(pack, old_version_id).is_currently_selected = false; + } + + // Deselect the new version too, since all versions of that pack got removed. + ver.is_currently_selected = false; + + m_selected.remove(pack.name); m_buttons.button(QDialogButtonBox::Ok)->setEnabled(!m_selected.isEmpty()); } -bool ResourceDownloadDialog::isSelected(QString name, QString filename) const -{ - auto iter = m_selected.constFind(name); - if (iter == m_selected.constEnd()) - return false; - - // FIXME: Is there a way to check for versions without checking the filename - // as a heuristic, other than adding such info to ResourceDownloadTask itself? - if (!filename.isEmpty()) - return iter.value()->getFilename() == filename; - - return true; -} - -const QList ResourceDownloadDialog::getTasks() +const QList ResourceDownloadDialog::getTasks() { return m_selected.values(); } diff --git a/launcher/ui/dialogs/ResourceDownloadDialog.h b/launcher/ui/dialogs/ResourceDownloadDialog.h index 29813493..95a5e628 100644 --- a/launcher/ui/dialogs/ResourceDownloadDialog.h +++ b/launcher/ui/dialogs/ResourceDownloadDialog.h @@ -23,6 +23,8 @@ #include #include +#include "QObjectPtr.h" +#include "modplatform/ModIndex.h" #include "ui/pages/BasePageProvider.h" class BaseInstance; @@ -41,6 +43,8 @@ class ResourceDownloadDialog : public QDialog, public BasePageProvider { Q_OBJECT public: + using DownloadTaskPtr = shared_qobject_ptr; + ResourceDownloadDialog(QWidget* parent, const std::shared_ptr base_model); void initializeContainer(); @@ -54,11 +58,10 @@ class ResourceDownloadDialog : public QDialog, public BasePageProvider { bool selectPage(QString pageId); ResourcePage* getSelectedPage(); - void addResource(QString name, ResourceDownloadTask* task); - void removeResource(QString name); - [[nodiscard]] bool isSelected(QString name, QString filename = "") const; + void addResource(ModPlatform::IndexedPack&, ModPlatform::IndexedVersion&, bool is_indexed = false); + void removeResource(ModPlatform::IndexedPack&, ModPlatform::IndexedVersion&); - const QList getTasks(); + const QList getTasks(); [[nodiscard]] const std::shared_ptr getBaseModel() const { return m_base_model; } public slots: @@ -82,7 +85,7 @@ class ResourceDownloadDialog : public QDialog, public BasePageProvider { QDialogButtonBox m_buttons; QVBoxLayout m_vertical_layout; - QHash m_selected; + QHash m_selected; }; diff --git a/launcher/ui/pages/modplatform/ModModel.cpp b/launcher/ui/pages/modplatform/ModModel.cpp index 59399c59..c9dee449 100644 --- a/launcher/ui/pages/modplatform/ModModel.cpp +++ b/launcher/ui/pages/modplatform/ModModel.cpp @@ -1,7 +1,7 @@ #include "ModModel.h" #include "Json.h" -#include "ModPage.h" + #include "minecraft/MinecraftInstance.h" #include "minecraft/PackProfile.h" @@ -9,15 +9,23 @@ namespace ResourceDownload { -ModModel::ModModel(ModPage* parent, ResourceAPI* api) : ResourceModel(parent, api) {} +ModModel::ModModel(BaseInstance const& base_inst, ResourceAPI* api) : ResourceModel(base_inst, api) {} /******** Make data requests ********/ ResourceAPI::SearchArgs ModModel::createSearchArguments() { - auto profile = static_cast(m_associated_page->m_base_instance).getPackProfile(); + auto profile = static_cast(m_base_instance).getPackProfile(); + + Q_ASSERT(profile); + Q_ASSERT(m_filter); + + std::optional> versions {}; + if (!m_filter->versions.empty()) + versions = m_filter->versions; + return { ModPlatform::ResourceType::MOD, m_next_search_offset, m_search_term, - getSorts()[currentSort], profile->getModLoaders(), getMineVersions() }; + getSorts()[currentSort], profile->getModLoaders(), versions }; } ResourceAPI::SearchCallbacks ModModel::createSearchCallbacks() { @@ -30,19 +38,24 @@ ResourceAPI::SearchCallbacks ModModel::createSearchCallbacks() ResourceAPI::VersionSearchArgs ModModel::createVersionsArguments(QModelIndex& entry) { - auto const& pack = m_packs[entry.row()]; - auto profile = static_cast(m_associated_page->m_base_instance).getPackProfile(); + auto& pack = m_packs[entry.row()]; + auto profile = static_cast(m_base_instance).getPackProfile(); - return { pack.addonId.toString(), getMineVersions(), profile->getModLoaders() }; + Q_ASSERT(profile); + Q_ASSERT(m_filter); + + std::optional> versions {}; + if (!m_filter->versions.empty()) + versions = m_filter->versions; + + return { pack, versions, profile->getModLoaders() }; } ResourceAPI::VersionSearchCallbacks ModModel::createVersionsCallbacks(QModelIndex& entry) { - auto const& pack = m_packs[entry.row()]; - - return { [this, pack, entry](auto& doc, auto addonId) { + return { [this, entry](auto& doc, auto& pack) { if (!s_running_models.constFind(this).value()) return; - versionRequestSucceeded(doc, addonId, entry); + versionRequestSucceeded(doc, pack, entry); } }; } @@ -87,7 +100,7 @@ void ModModel::searchRequestFinished(QJsonDocument& doc) loadIndexedPack(pack, packObj); newList.append(pack); } catch (const JSONValidationError& e) { - qWarning() << "Error while loading mod from " << m_associated_page->debugName() << ": " << e.cause(); + qWarning() << "Error while loading mod from " << debugName() << ": " << e.cause(); continue; } } @@ -127,48 +140,36 @@ void ModModel::infoRequestFinished(QJsonDocument& doc, ModPlatform::IndexedPack& new_pack.setValue(pack); if (!setData(index, new_pack, Qt::UserRole)) { qWarning() << "Failed to cache mod info!"; + return; } + + emit projectInfoUpdated(); } - - m_associated_page->updateUi(); } -void ModModel::versionRequestSucceeded(QJsonDocument doc, QString addonId, const QModelIndex& index) +void ModModel::versionRequestSucceeded(QJsonDocument doc, ModPlatform::IndexedPack& pack, const QModelIndex& index) { - auto current = m_associated_page->getCurrentPack(); - if (addonId != current.addonId) { - return; - } - auto arr = doc.isObject() ? Json::ensureArray(doc.object(), "data") : doc.array(); try { - loadIndexedPackVersions(current, arr); + loadIndexedPackVersions(pack, arr); } catch (const JSONValidationError& e) { qDebug() << doc; qWarning() << "Error while reading " << debugName() << " mod version: " << e.cause(); } - // Cache info :^) - QVariant new_pack; - new_pack.setValue(current); - if (!setData(index, new_pack, Qt::UserRole)) { - qWarning() << "Failed to cache mod versions!"; + // Check if the index is still valid for this mod or not + if (pack.addonId == data(index, Qt::UserRole).value().addonId) { + // Cache info :^) + QVariant new_pack; + new_pack.setValue(pack); + if (!setData(index, new_pack, Qt::UserRole)) { + qWarning() << "Failed to cache mod versions!"; + return; + } + + emit versionListUpdated(); } - - m_associated_page->updateVersionList(); -} - -/******** Helpers ********/ - -#define MOD_PAGE(x) static_cast(x) - -auto ModModel::getMineVersions() const -> std::optional> -{ - auto versions = MOD_PAGE(m_associated_page)->getFilter()->versions; - if (!versions.empty()) - return versions; - return {}; } } // namespace ResourceDownload diff --git a/launcher/ui/pages/modplatform/ModModel.h b/launcher/ui/pages/modplatform/ModModel.h index e3d760a2..39d062f9 100644 --- a/launcher/ui/pages/modplatform/ModModel.h +++ b/launcher/ui/pages/modplatform/ModModel.h @@ -6,6 +6,7 @@ #include "modplatform/ResourceAPI.h" #include "ui/pages/modplatform/ResourceModel.h" +#include "ui/widgets/ModFilterWidget.h" class Version; @@ -17,7 +18,7 @@ class ModModel : public ResourceModel { Q_OBJECT public: - ModModel(ModPage* parent, ResourceAPI* api); + ModModel(const BaseInstance&, ResourceAPI* api); /* Ask the API for more information */ void searchWithTerm(const QString& term, const int sort, const bool filter_changed); @@ -26,12 +27,12 @@ class ModModel : public ResourceModel { virtual void loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj) = 0; virtual void loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) = 0; + void setFilter(std::shared_ptr filter) { m_filter = filter; } + public slots: void searchRequestFinished(QJsonDocument& doc); - void infoRequestFinished(QJsonDocument& doc, ModPlatform::IndexedPack& pack, const QModelIndex& index); - - void versionRequestSucceeded(QJsonDocument doc, QString addonId, const QModelIndex& index); + void versionRequestSucceeded(QJsonDocument doc, ModPlatform::IndexedPack& pack, const QModelIndex& index); public slots: ResourceAPI::SearchArgs createSearchArguments() override; @@ -47,10 +48,10 @@ class ModModel : public ResourceModel { virtual auto documentToArray(QJsonDocument& obj) const -> QJsonArray = 0; virtual auto getSorts() const -> const char** = 0; - inline auto getMineVersions() const -> std::optional>; - protected: int currentSort = 0; + + std::shared_ptr m_filter = nullptr; }; } // namespace ResourceDownload diff --git a/launcher/ui/pages/modplatform/ModPage.cpp b/launcher/ui/pages/modplatform/ModPage.cpp index 8d441546..556bd642 100644 --- a/launcher/ui/pages/modplatform/ModPage.cpp +++ b/launcher/ui/pages/modplatform/ModPage.cpp @@ -51,8 +51,6 @@ #include "ui/dialogs/ResourceDownloadDialog.h" -#include "ui/pages/modplatform/ModModel.h" - namespace ResourceDownload { ModPage::ModPage(ModDownloadDialog* dialog, BaseInstance& instance) @@ -151,7 +149,7 @@ void ModPage::updateVersionList() void ModPage::addResourceToDialog(ModPlatform::IndexedPack& pack, ModPlatform::IndexedVersion& version) { bool is_indexed = !APPLICATION->settings()->get("ModMetadataDisabled").toBool(); - m_parent_dialog->addResource(pack.name, new ResourceDownloadTask(pack, version, m_parent_dialog->getBaseModel(), is_indexed)); + m_parent_dialog->addResource(pack, version, is_indexed); } } // namespace ResourceDownload diff --git a/launcher/ui/pages/modplatform/ModPage.h b/launcher/ui/pages/modplatform/ModPage.h index 137a6046..2fda3b68 100644 --- a/launcher/ui/pages/modplatform/ModPage.h +++ b/launcher/ui/pages/modplatform/ModPage.h @@ -5,6 +5,7 @@ #include "modplatform/ModIndex.h" #include "ui/pages/modplatform/ResourcePage.h" +#include "ui/pages/modplatform/ModModel.h" #include "ui/widgets/ModFilterWidget.h" namespace Ui { @@ -24,9 +25,14 @@ class ModPage : public ResourcePage { static T* create(ModDownloadDialog* dialog, BaseInstance& instance) { 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); page->setFilterWidget(filter_widget); + model->setFilter(page->getFilter()); + + connect(model, &ResourceModel::versionListUpdated, page, &ResourcePage::updateVersionList); + connect(model, &ResourceModel::projectInfoUpdated, page, &ResourcePage::updateUi); return page; } diff --git a/launcher/ui/pages/modplatform/ResourceModel.cpp b/launcher/ui/pages/modplatform/ResourceModel.cpp index e8af0e7a..cf40fef2 100644 --- a/launcher/ui/pages/modplatform/ResourceModel.cpp +++ b/launcher/ui/pages/modplatform/ResourceModel.cpp @@ -17,14 +17,13 @@ #include "modplatform/ModIndex.h" -#include "ui/pages/modplatform/ResourcePage.h" #include "ui/widgets/ProjectItem.h" namespace ResourceDownload { QHash ResourceModel::s_running_models; -ResourceModel::ResourceModel(ResourcePage* parent, ResourceAPI* api) : QAbstractListModel(), m_api(api), m_associated_page(parent) +ResourceModel::ResourceModel(BaseInstance const& base_inst, ResourceAPI* api) : QAbstractListModel(), m_base_instance(base_inst), m_api(api) { s_running_models.insert(this, true); } @@ -72,7 +71,7 @@ auto ResourceModel::data(const QModelIndex& index, int role) const -> QVariant case UserDataTypes::DESCRIPTION: return pack.description; case UserDataTypes::SELECTED: - return isPackSelected(pack); + return pack.isAnyVersionSelected(); default: break; } @@ -87,13 +86,14 @@ bool ResourceModel::setData(const QModelIndex& index, const QVariant& value, int return false; m_packs[pos] = value.value(); + emit dataChanged(index, index); return true; } QString ResourceModel::debugName() const { - return m_associated_page->debugName() + " (Model)"; + return "ResourceDownload (Model)"; } void ResourceModel::fetchMore(const QModelIndex& parent) @@ -195,7 +195,7 @@ std::optional ResourceModel::getIcon(QModelIndex& index, const QUrl& url) return {}; auto cache_entry = APPLICATION->metacache()->resolveEntry( - m_associated_page->metaEntryBase(), + metaEntryBase(), QString("logos/%1").arg(QString(QCryptographicHash::hash(url.toEncoded(), QCryptographicHash::Algorithm::Sha1).toHex()))); auto icon_fetch_action = Net::Download::makeCached(url, cache_entry); @@ -222,11 +222,6 @@ std::optional ResourceModel::getIcon(QModelIndex& index, const QUrl& url) return {}; } -bool ResourceModel::isPackSelected(const ModPlatform::IndexedPack& pack) const -{ - return m_associated_page->isPackSelected(pack); -} - void ResourceModel::searchRequestFailed(QString reason, int network_error_code) { switch (network_error_code) { @@ -237,9 +232,7 @@ void ResourceModel::searchRequestFailed(QString reason, int network_error_code) case 409: // 409 Gone, notify user to update QMessageBox::critical(nullptr, tr("Error"), - //: %1 refers to the launcher itself - QString("%1 %2") - .arg(m_associated_page->displayName()) + QString("%1") .arg(tr("API version too old!\nPlease update %1!").arg(BuildConfig.LAUNCHER_DISPLAYNAME))); break; } diff --git a/launcher/ui/pages/modplatform/ResourceModel.h b/launcher/ui/pages/modplatform/ResourceModel.h index 6a94c399..af33bf55 100644 --- a/launcher/ui/pages/modplatform/ResourceModel.h +++ b/launcher/ui/pages/modplatform/ResourceModel.h @@ -5,6 +5,7 @@ #include #include "QObjectPtr.h" +#include "BaseInstance.h" #include "modplatform/ResourceAPI.h" #include "tasks/ConcurrentTask.h" @@ -17,19 +18,18 @@ struct IndexedPack; namespace ResourceDownload { -class ResourcePage; - class ResourceModel : public QAbstractListModel { Q_OBJECT public: - ResourceModel(ResourcePage* parent, ResourceAPI* api); + ResourceModel(BaseInstance const&, ResourceAPI* api); ~ResourceModel() override; [[nodiscard]] auto data(const QModelIndex&, int role) const -> QVariant override; bool setData(const QModelIndex& index, const QVariant& value, int role) override; - [[nodiscard]] auto debugName() const -> QString; + [[nodiscard]] virtual auto debugName() const -> QString; + [[nodiscard]] virtual auto metaEntryBase() const -> QString = 0; [[nodiscard]] inline int rowCount(const QModelIndex& parent) const override { return parent.isValid() ? 0 : m_packs.size(); } [[nodiscard]] inline int columnCount(const QModelIndex& parent) const override { return parent.isValid() ? 0 : 1; }; @@ -38,6 +38,10 @@ class ResourceModel : public QAbstractListModel { inline void addActiveJob(Task::Ptr ptr) { m_current_job.addTask(ptr); if (!m_current_job.isRunning()) m_current_job.start(); } inline Task const& activeJob() { return m_current_job; } + signals: + void versionListUpdated(); + void projectInfoUpdated(); + public slots: void fetchMore(const QModelIndex& parent) override; [[nodiscard]] inline bool canFetchMore(const QModelIndex& parent) const override @@ -72,9 +76,9 @@ class ResourceModel : public QAbstractListModel { /** Resets the model's data. */ void clearData(); - [[nodiscard]] bool isPackSelected(const ModPlatform::IndexedPack&) const; - protected: + const BaseInstance& m_base_instance; + /* Basic search parameters */ enum class SearchState { None, CanFetchMore, ResetRequested, Finished } m_search_state = SearchState::None; int m_next_search_offset = 0; @@ -88,8 +92,6 @@ class ResourceModel : public QAbstractListModel { QSet m_currently_running_icon_actions; QSet m_failed_icon_actions; - ResourcePage* m_associated_page = nullptr; - QList m_packs; // HACK: We need this to prevent callbacks from calling the model after it has already been deleted. diff --git a/launcher/ui/pages/modplatform/ResourcePage.cpp b/launcher/ui/pages/modplatform/ResourcePage.cpp index 161b5c22..e04278af 100644 --- a/launcher/ui/pages/modplatform/ResourcePage.cpp +++ b/launcher/ui/pages/modplatform/ResourcePage.cpp @@ -103,19 +103,18 @@ void ResourcePage::setSearchTerm(QString term) m_ui->searchEdit->setText(term); } +bool ResourcePage::setCurrentPack(ModPlatform::IndexedPack pack) +{ + QVariant v; + v.setValue(pack); + return m_model->setData(m_ui->packView->currentIndex(), v, Qt::UserRole); +} + ModPlatform::IndexedPack ResourcePage::getCurrentPack() const { return m_model->data(m_ui->packView->currentIndex(), Qt::UserRole).value(); } -bool ResourcePage::isPackSelected(const ModPlatform::IndexedPack& pack, int version) const -{ - if (version < 0 || !pack.versionsLoaded) - return m_parent_dialog->isSelected(pack.name); - - return m_parent_dialog->isSelected(pack.name, pack.versions[version].fileName); -} - void ResourcePage::updateUi() { auto current_pack = getCurrentPack(); @@ -185,7 +184,7 @@ void ResourcePage::updateSelectionButton() } m_ui->resourceSelectionButton->setEnabled(true); - if (!isPackSelected(getCurrentPack(), m_selected_version_index)) { + if (!getCurrentPack().isVersionSelected(m_selected_version_index)) { m_ui->resourceSelectionButton->setText(tr("Select %1 for download").arg(resourceString())); } else { m_ui->resourceSelectionButton->setText(tr("Deselect %1 for download").arg(resourceString())); @@ -256,12 +255,12 @@ void ResourcePage::onVersionSelectionChanged(QString data) void ResourcePage::addResourceToDialog(ModPlatform::IndexedPack& pack, ModPlatform::IndexedVersion& version) { - m_parent_dialog->addResource(pack.name, new ResourceDownloadTask(pack, version, m_parent_dialog->getBaseModel())); + m_parent_dialog->addResource(pack, version); } -void ResourcePage::removeResourceFromDialog(ModPlatform::IndexedPack& pack, ModPlatform::IndexedVersion&) +void ResourcePage::removeResourceFromDialog(ModPlatform::IndexedPack& pack, ModPlatform::IndexedVersion& version) { - m_parent_dialog->removeResource(pack.name); + m_parent_dialog->removeResource(pack, version); } void ResourcePage::onResourceSelected() @@ -270,13 +269,19 @@ void ResourcePage::onResourceSelected() return; auto current_pack = getCurrentPack(); + if (!current_pack.versionsLoaded) + return; auto& version = current_pack.versions[m_selected_version_index]; - if (m_parent_dialog->isSelected(current_pack.name, version.fileName)) + if (version.is_currently_selected) removeResourceFromDialog(current_pack, version); else addResourceToDialog(current_pack, version); + // Save the modified pack (and prevent warning in release build) + [[maybe_unused]] bool set = setCurrentPack(current_pack); + Q_ASSERT(set); + updateSelectionButton(); /* Force redraw on the resource list when the selection changes */ diff --git a/launcher/ui/pages/modplatform/ResourcePage.h b/launcher/ui/pages/modplatform/ResourcePage.h index f731cf56..b51c7ccb 100644 --- a/launcher/ui/pages/modplatform/ResourcePage.h +++ b/launcher/ui/pages/modplatform/ResourcePage.h @@ -50,11 +50,13 @@ class ResourcePage : public QWidget, public BasePage { /** Programatically set the term in the search bar. */ void setSearchTerm(QString); - [[nodiscard]] bool isPackSelected(const ModPlatform::IndexedPack&, int version = -1) const; + [[nodiscard]] bool setCurrentPack(ModPlatform::IndexedPack); [[nodiscard]] auto getCurrentPack() const -> ModPlatform::IndexedPack; [[nodiscard]] auto getDialog() const -> const ResourceDownloadDialog* { return m_parent_dialog; } + [[nodiscard]] auto getModel() const -> ResourceModel* { return m_model; } + protected: ResourcePage(ResourceDownloadDialog* parent, BaseInstance&); diff --git a/launcher/ui/pages/modplatform/flame/FlameResourceModels.cpp b/launcher/ui/pages/modplatform/flame/FlameResourceModels.cpp index cfe4080a..d0f109de 100644 --- a/launcher/ui/pages/modplatform/flame/FlameResourceModels.cpp +++ b/launcher/ui/pages/modplatform/flame/FlameResourceModels.cpp @@ -2,6 +2,7 @@ #include "Json.h" +#include "modplatform/flame/FlameAPI.h" #include "modplatform/flame/FlameModIndex.h" namespace ResourceDownload { @@ -9,7 +10,7 @@ namespace ResourceDownload { // NOLINTNEXTLINE(modernize-avoid-c-arrays) const char* FlameModModel::sorts[6]{ "Featured", "Popularity", "LastUpdated", "Name", "Author", "TotalDownloads" }; -FlameModModel::FlameModModel(FlameModPage* parent) : ModModel(parent, new FlameAPI) {} +FlameModModel::FlameModModel(BaseInstance const& base) : ModModel(base, new FlameAPI) {} void FlameModModel::loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) { @@ -24,7 +25,7 @@ void FlameModModel::loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& void FlameModModel::loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) { - FlameMod::loadIndexedPackVersions(m, arr, APPLICATION->network(), &m_associated_page->m_base_instance); + FlameMod::loadIndexedPackVersions(m, arr, APPLICATION->network(), &m_base_instance); } auto FlameModModel::documentToArray(QJsonDocument& obj) const -> QJsonArray diff --git a/launcher/ui/pages/modplatform/flame/FlameResourceModels.h b/launcher/ui/pages/modplatform/flame/FlameResourceModels.h index 501937e2..7b253dce 100644 --- a/launcher/ui/pages/modplatform/flame/FlameResourceModels.h +++ b/launcher/ui/pages/modplatform/flame/FlameResourceModels.h @@ -1,9 +1,6 @@ #pragma once -#include "modplatform/flame/FlameAPI.h" - #include "ui/pages/modplatform/ModModel.h" - #include "ui/pages/modplatform/flame/FlameResourcePages.h" namespace ResourceDownload { @@ -12,10 +9,13 @@ class FlameModModel : public ModModel { Q_OBJECT public: - FlameModModel(FlameModPage* parent); + FlameModModel(const BaseInstance&); ~FlameModModel() override = default; private: + [[nodiscard]] QString debugName() const override { return Flame::debugName() + " (Model)"; } + [[nodiscard]] QString metaEntryBase() const override { return Flame::metaEntryBase(); } + void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) override; void loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj) override; void loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) override; diff --git a/launcher/ui/pages/modplatform/flame/FlameResourcePages.cpp b/launcher/ui/pages/modplatform/flame/FlameResourcePages.cpp index 2a8ab526..67737a76 100644 --- a/launcher/ui/pages/modplatform/flame/FlameResourcePages.cpp +++ b/launcher/ui/pages/modplatform/flame/FlameResourcePages.cpp @@ -45,7 +45,7 @@ namespace ResourceDownload { FlameModPage::FlameModPage(ModDownloadDialog* dialog, BaseInstance& instance) : ModPage(dialog, instance) { - m_model = new FlameModModel(this); + m_model = new FlameModModel(instance); m_ui->packView->setModel(m_model); // index is used to set the sorting with the flame api diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthResourceModels.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthResourceModels.cpp index ee96f0de..9d26ae05 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthResourceModels.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthResourceModels.cpp @@ -18,8 +18,6 @@ #include "ModrinthResourceModels.h" -#include "ui/pages/modplatform/modrinth/ModrinthResourcePages.h" - #include "modplatform/modrinth/ModrinthAPI.h" #include "modplatform/modrinth/ModrinthPackIndex.h" @@ -28,7 +26,7 @@ namespace ResourceDownload { // NOLINTNEXTLINE(modernize-avoid-c-arrays) const char* ModrinthModModel::sorts[5]{ "relevance", "downloads", "follows", "updated", "newest" }; -ModrinthModModel::ModrinthModModel(ModrinthModPage* parent) : ModModel(parent, new ModrinthAPI){}; +ModrinthModModel::ModrinthModModel(BaseInstance const& base) : ModModel(base, new ModrinthAPI){}; void ModrinthModModel::loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) { @@ -42,7 +40,7 @@ void ModrinthModModel::loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObjec void ModrinthModModel::loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) { - ::Modrinth::loadIndexedPackVersions(m, arr, APPLICATION->network(), &m_associated_page->m_base_instance); + ::Modrinth::loadIndexedPackVersions(m, arr, APPLICATION->network(), &m_base_instance); } auto ModrinthModModel::documentToArray(QJsonDocument& obj) const -> QJsonArray diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthResourceModels.h b/launcher/ui/pages/modplatform/modrinth/ModrinthResourceModels.h index b0088a73..798a70e6 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthResourceModels.h +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthResourceModels.h @@ -19,6 +19,7 @@ #pragma once #include "ui/pages/modplatform/ModModel.h" +#include "ui/pages/modplatform/modrinth/ModrinthResourcePages.h" namespace ResourceDownload { @@ -28,10 +29,13 @@ class ModrinthModModel : public ModModel { Q_OBJECT public: - ModrinthModModel(ModrinthModPage* parent); + ModrinthModModel(const BaseInstance&); ~ModrinthModModel() override = default; private: + [[nodiscard]] QString debugName() const override { return Modrinth::debugName() + " (Model)"; } + [[nodiscard]] QString metaEntryBase() const override { return Modrinth::metaEntryBase(); } + void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) override; void loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj) override; void loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) override; diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.cpp index 1352e2f6..88621e05 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.cpp @@ -47,7 +47,7 @@ namespace ResourceDownload { ModrinthModPage::ModrinthModPage(ModDownloadDialog* dialog, BaseInstance& instance) : ModPage(dialog, instance) { - m_model = new ModrinthModModel(this); + m_model = new ModrinthModModel(instance); m_ui->packView->setModel(m_model); // index is used to set the sorting with the modrinth api From 0e207aba6c4eb67dccef12750c080a64deba6764 Mon Sep 17 00:00:00 2001 From: flow Date: Sun, 18 Dec 2022 16:55:09 -0300 Subject: [PATCH 091/152] feat(RD): add roleNames and Q_PROPERTY to ResourceModel in preparation for QML interop. Signed-off-by: flow --- launcher/ui/pages/modplatform/ResourceModel.cpp | 15 +++++++++++++++ launcher/ui/pages/modplatform/ResourceModel.h | 3 +++ 2 files changed, 18 insertions(+) diff --git a/launcher/ui/pages/modplatform/ResourceModel.cpp b/launcher/ui/pages/modplatform/ResourceModel.cpp index cf40fef2..eedc5202 100644 --- a/launcher/ui/pages/modplatform/ResourceModel.cpp +++ b/launcher/ui/pages/modplatform/ResourceModel.cpp @@ -79,6 +79,21 @@ auto ResourceModel::data(const QModelIndex& index, int role) const -> QVariant return {}; } +QHash ResourceModel::roleNames() const +{ + QHash roles; + + roles[Qt::ToolTipRole] = "toolTip"; + roles[Qt::DecorationRole] = "decoration"; + roles[Qt::SizeHintRole] = "sizeHint"; + roles[Qt::UserRole] = "pack"; + roles[UserDataTypes::TITLE] = "title"; + roles[UserDataTypes::DESCRIPTION] = "description"; + roles[UserDataTypes::SELECTED] = "selected"; + + return roles; +} + bool ResourceModel::setData(const QModelIndex& index, const QVariant& value, int role) { int pos = index.row(); diff --git a/launcher/ui/pages/modplatform/ResourceModel.h b/launcher/ui/pages/modplatform/ResourceModel.h index af33bf55..45af33a2 100644 --- a/launcher/ui/pages/modplatform/ResourceModel.h +++ b/launcher/ui/pages/modplatform/ResourceModel.h @@ -21,11 +21,14 @@ namespace ResourceDownload { class ResourceModel : public QAbstractListModel { Q_OBJECT + Q_PROPERTY(QString search_term MEMBER m_search_term WRITE setSearchTerm) + public: ResourceModel(BaseInstance const&, ResourceAPI* api); ~ResourceModel() override; [[nodiscard]] auto data(const QModelIndex&, int role) const -> QVariant override; + [[nodiscard]] auto roleNames() const -> QHash override; bool setData(const QModelIndex& index, const QVariant& value, int role) override; [[nodiscard]] virtual auto debugName() const -> QString; From c8eca4fb8508a22b9d4819d57627dd684f8d98c5 Mon Sep 17 00:00:00 2001 From: flow Date: Sun, 18 Dec 2022 17:03:39 -0300 Subject: [PATCH 092/152] fix: build with qt5.12 on Linux and pedantic flag Signed-off-by: flow --- launcher/modplatform/ResourceAPI.h | 1 + launcher/ui/dialogs/ResourceDownloadDialog.h | 1 + launcher/ui/pages/modplatform/ResourceModel.h | 15 ++++++++------- launcher/ui/pages/modplatform/ResourcePage.cpp | 4 +++- launcher/ui/pages/modplatform/ResourcePage.h | 4 +++- .../pages/modplatform/flame/FlameResourcePages.h | 2 +- .../modrinth/ModrinthResourceModels.cpp | 2 +- .../modplatform/modrinth/ModrinthResourcePages.h | 2 +- 8 files changed, 19 insertions(+), 12 deletions(-) diff --git a/launcher/modplatform/ResourceAPI.h b/launcher/modplatform/ResourceAPI.h index 49aac712..78441c34 100644 --- a/launcher/modplatform/ResourceAPI.h +++ b/launcher/modplatform/ResourceAPI.h @@ -39,6 +39,7 @@ #include #include +#include #include "../Version.h" diff --git a/launcher/ui/dialogs/ResourceDownloadDialog.h b/launcher/ui/dialogs/ResourceDownloadDialog.h index 95a5e628..34120350 100644 --- a/launcher/ui/dialogs/ResourceDownloadDialog.h +++ b/launcher/ui/dialogs/ResourceDownloadDialog.h @@ -21,6 +21,7 @@ #include #include +#include #include #include "QObjectPtr.h" diff --git a/launcher/ui/pages/modplatform/ResourceModel.h b/launcher/ui/pages/modplatform/ResourceModel.h index 45af33a2..d0b9234b 100644 --- a/launcher/ui/pages/modplatform/ResourceModel.h +++ b/launcher/ui/pages/modplatform/ResourceModel.h @@ -35,19 +35,16 @@ class ResourceModel : public QAbstractListModel { [[nodiscard]] virtual auto metaEntryBase() const -> QString = 0; [[nodiscard]] inline int rowCount(const QModelIndex& parent) const override { return parent.isValid() ? 0 : m_packs.size(); } - [[nodiscard]] inline int columnCount(const QModelIndex& parent) const override { return parent.isValid() ? 0 : 1; }; - [[nodiscard]] inline auto flags(const QModelIndex& index) const -> Qt::ItemFlags override { return QAbstractListModel::flags(index); }; + [[nodiscard]] inline int columnCount(const QModelIndex& parent) const override { return parent.isValid() ? 0 : 1; } + [[nodiscard]] inline auto flags(const QModelIndex& index) const -> Qt::ItemFlags override { return QAbstractListModel::flags(index); } inline void addActiveJob(Task::Ptr ptr) { m_current_job.addTask(ptr); if (!m_current_job.isRunning()) m_current_job.start(); } inline Task const& activeJob() { return m_current_job; } - signals: - void versionListUpdated(); - void projectInfoUpdated(); - public slots: void fetchMore(const QModelIndex& parent) override; - [[nodiscard]] inline bool canFetchMore(const QModelIndex& parent) const override + // NOTE: Can't use [[nodiscard]] here because of https://bugreports.qt.io/browse/QTBUG-58628 on Qt 5.12 + inline bool canFetchMore(const QModelIndex& parent) const override { return parent.isValid() ? false : m_search_state == SearchState::CanFetchMore; } @@ -105,6 +102,10 @@ class ResourceModel : public QAbstractListModel { /* Default search request callbacks */ void searchRequestFailed(QString reason, int network_error_code); void searchRequestAborted(); + + signals: + void versionListUpdated(); + void projectInfoUpdated(); }; } // namespace ResourceDownload diff --git a/launcher/ui/pages/modplatform/ResourcePage.cpp b/launcher/ui/pages/modplatform/ResourcePage.cpp index e04278af..6e6868c5 100644 --- a/launcher/ui/pages/modplatform/ResourcePage.cpp +++ b/launcher/ui/pages/modplatform/ResourcePage.cpp @@ -302,7 +302,9 @@ void ResourcePage::openUrl(const QUrl& url) QRegularExpressionMatch match; QString page; - for (auto&& [regex, candidate] : urlHandlers().asKeyValueRange()) { + auto handlers = urlHandlers(); + for (auto it = handlers.constKeyValueBegin(); it != handlers.constKeyValueEnd(); it++) { + auto&& [regex, candidate] = *it; if (match = QRegularExpression(regex).match(address); match.hasMatch()) { page = candidate; break; diff --git a/launcher/ui/pages/modplatform/ResourcePage.h b/launcher/ui/pages/modplatform/ResourcePage.h index b51c7ccb..b95c5a40 100644 --- a/launcher/ui/pages/modplatform/ResourcePage.h +++ b/launcher/ui/pages/modplatform/ResourcePage.h @@ -75,8 +75,10 @@ class ResourcePage : public QWidget, public BasePage { void onVersionSelectionChanged(QString data); void onResourceSelected(); + // NOTE: Can't use [[nodiscard]] here because of https://bugreports.qt.io/browse/QTBUG-58628 on Qt 5.12 + /** Associates regex expressions to pages in the order they're given in the map. */ - [[nodiscard]] virtual QMap urlHandlers() const = 0; + virtual QMap urlHandlers() const = 0; virtual void openUrl(const QUrl&); /** Whether the version is opted out or not. Currently only makes sense in CF. */ diff --git a/launcher/ui/pages/modplatform/flame/FlameResourcePages.h b/launcher/ui/pages/modplatform/flame/FlameResourcePages.h index 6c7d0247..12b51aa9 100644 --- a/launcher/ui/pages/modplatform/flame/FlameResourcePages.h +++ b/launcher/ui/pages/modplatform/flame/FlameResourcePages.h @@ -49,7 +49,7 @@ static inline QString displayName() { return "CurseForge"; } static inline QIcon icon() { return APPLICATION->getThemedIcon("flame"); } static inline QString id() { return "curseforge"; } static inline QString debugName() { return "Flame"; } -static inline QString metaEntryBase() { return "FlameMods"; }; +static inline QString metaEntryBase() { return "FlameMods"; } } class FlameModPage : public ModPage { diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthResourceModels.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthResourceModels.cpp index 9d26ae05..895e23fd 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthResourceModels.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthResourceModels.cpp @@ -26,7 +26,7 @@ namespace ResourceDownload { // NOLINTNEXTLINE(modernize-avoid-c-arrays) const char* ModrinthModModel::sorts[5]{ "relevance", "downloads", "follows", "updated", "newest" }; -ModrinthModModel::ModrinthModModel(BaseInstance const& base) : ModModel(base, new ModrinthAPI){}; +ModrinthModModel::ModrinthModModel(BaseInstance const& base) : ModModel(base, new ModrinthAPI) {} void ModrinthModModel::loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) { diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.h b/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.h index 07b32c0c..a263bd44 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.h +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.h @@ -48,7 +48,7 @@ static inline QString displayName() { return "Modrinth"; } static inline QIcon icon() { return APPLICATION->getThemedIcon("modrinth"); } static inline QString id() { return "modrinth"; } static inline QString debugName() { return "Modrinth"; } -static inline QString metaEntryBase() { return "ModrinthPacks"; }; +static inline QString metaEntryBase() { return "ModrinthPacks"; } } class ModrinthModPage : public ModPage { From 36571c5e2237c98e194cff326480ebe3e661c586 Mon Sep 17 00:00:00 2001 From: flow Date: Tue, 20 Dec 2022 12:15:17 -0300 Subject: [PATCH 093/152] refactor(RD): clear up sorting methods This refactors the sorting methods to join every bit of it into a single list, easing maintanance. It also removes the weird index contraint on the list of methods by adding an index field to the DS that holds the method. Lastly, it puts the available methods on their respective API, so other resources on the same API can re-use them later on. Signed-off-by: flow --- launcher/modplatform/ResourceAPI.h | 17 +++++++++- launcher/modplatform/flame/FlameAPI.cpp | 15 +++++++++ launcher/modplatform/flame/FlameAPI.h | 17 ++-------- launcher/modplatform/modrinth/ModrinthAPI.cpp | 12 +++++++ launcher/modplatform/modrinth/ModrinthAPI.h | 4 ++- launcher/ui/pages/modplatform/ModModel.cpp | 33 ++++++++++++------- launcher/ui/pages/modplatform/ModModel.h | 5 +-- launcher/ui/pages/modplatform/ModPage.cpp | 2 +- launcher/ui/pages/modplatform/ResourceModel.h | 5 +++ .../ui/pages/modplatform/ResourcePage.cpp | 11 +++++++ launcher/ui/pages/modplatform/ResourcePage.h | 4 +-- .../modplatform/flame/FlameResourceModels.cpp | 3 -- .../modplatform/flame/FlameResourceModels.h | 4 --- .../modplatform/flame/FlameResourcePages.cpp | 8 +---- .../modrinth/ModrinthResourceModels.cpp | 3 -- .../modrinth/ModrinthResourceModels.h | 4 --- .../modrinth/ModrinthResourcePages.cpp | 7 +--- 17 files changed, 93 insertions(+), 61 deletions(-) diff --git a/launcher/modplatform/ResourceAPI.h b/launcher/modplatform/ResourceAPI.h index 78441c34..a2078b94 100644 --- a/launcher/modplatform/ResourceAPI.h +++ b/launcher/modplatform/ResourceAPI.h @@ -54,12 +54,23 @@ class ResourceAPI { enum ModLoaderType { Forge = 1 << 0, Cauldron = 1 << 1, LiteLoader = 1 << 2, Fabric = 1 << 3, Quilt = 1 << 4 }; Q_DECLARE_FLAGS(ModLoaderTypes, ModLoaderType) + struct SortingMethod { + // The index of the sorting method. Used to allow for arbitrary ordering in the list of methods. + // Used by Flame in the API request. + unsigned int index; + // The real name of the sorting, as used in the respective API specification. + // Used by Modrinth in the API request. + QString name; + // The human-readable name of the sorting, used for display in the UI. + QString readable_name; + }; + struct SearchArgs { ModPlatform::ResourceType type{}; int offset = 0; std::optional search; - std::optional sorting; + std::optional sorting; std::optional loaders; std::optional > versions; }; @@ -95,6 +106,10 @@ class ResourceAPI { std::function on_succeed; }; + public: + /** Gets a list of available sorting methods for this API. */ + [[nodiscard]] virtual auto getSortingMethods() const -> QList = 0; + public slots: [[nodiscard]] virtual NetJob::Ptr searchProjects(SearchArgs&&, SearchCallbacks&&) const { diff --git a/launcher/modplatform/flame/FlameAPI.cpp b/launcher/modplatform/flame/FlameAPI.cpp index 89249c41..32729a14 100644 --- a/launcher/modplatform/flame/FlameAPI.cpp +++ b/launcher/modplatform/flame/FlameAPI.cpp @@ -212,3 +212,18 @@ NetJob::Ptr FlameAPI::getFiles(const QStringList& fileIds, QByteArray* response) return netJob; } + +// https://docs.curseforge.com/?python#tocS_ModsSearchSortField +static QList s_sorts = { { 1, "Featured", QObject::tr("Sort by Featured") }, + { 2, "Popularity", QObject::tr("Sort by Popularity") }, + { 3, "LastUpdated", QObject::tr("Sort by Last Updated") }, + { 4, "Name", QObject::tr("Sort by Name") }, + { 5, "Author", QObject::tr("Sort by Author") }, + { 6, "TotalDownloads", QObject::tr("Sort by Downloads") }, + { 7, "Category", QObject::tr("Sort by Category") }, + { 8, "GameVersion", QObject::tr("Sort by Game Version") } }; + +QList FlameAPI::getSortingMethods() const +{ + return s_sorts; +} diff --git a/launcher/modplatform/flame/FlameAPI.h b/launcher/modplatform/flame/FlameAPI.h index f3cc0bbf..2b288564 100644 --- a/launcher/modplatform/flame/FlameAPI.h +++ b/launcher/modplatform/flame/FlameAPI.h @@ -14,20 +14,9 @@ class FlameAPI : public NetworkResourceAPI { NetJob::Ptr matchFingerprints(const QList& fingerprints, QByteArray* response); NetJob::Ptr getFiles(const QStringList& fileIds, QByteArray* response) const; - private: - static int getSortFieldInt(QString const& sortString) - { - return sortString == "Featured" ? 1 - : sortString == "Popularity" ? 2 - : sortString == "LastUpdated" ? 3 - : sortString == "Name" ? 4 - : sortString == "Author" ? 5 - : sortString == "TotalDownloads" ? 6 - : sortString == "Category" ? 7 - : sortString == "GameVersion" ? 8 - : 1; - } + [[nodiscard]] auto getSortingMethods() const -> QList override; + private: static int getClassId(ModPlatform::ResourceType type) { switch (type) { @@ -62,7 +51,7 @@ class FlameAPI : public NetworkResourceAPI { if (args.search.has_value()) get_arguments.append(QString("searchFilter=%1").arg(args.search.value())); if (args.sorting.has_value()) - get_arguments.append(QString("sortField=%1").arg(getSortFieldInt(args.sorting.value()))); + get_arguments.append(QString("sortField=%1").arg(args.sorting.value().index)); get_arguments.append("sortOrder=desc"); if (args.loaders.has_value()) get_arguments.append(QString("modLoaderType=%1").arg(getMappedModLoader(args.loaders.value()))); diff --git a/launcher/modplatform/modrinth/ModrinthAPI.cpp b/launcher/modplatform/modrinth/ModrinthAPI.cpp index 8e64be09..8d7e3acf 100644 --- a/launcher/modplatform/modrinth/ModrinthAPI.cpp +++ b/launcher/modplatform/modrinth/ModrinthAPI.cpp @@ -112,3 +112,15 @@ NetJob::Ptr ModrinthAPI::getProjects(QStringList addonIds, QByteArray* response) return netJob; } + +// https://docs.modrinth.com/api-spec/#tag/projects/operation/searchProjects +static QList s_sorts = { { 1, "relevance", QObject::tr("Sort by Relevance") }, + { 2, "downloads", QObject::tr("Sort by Downloads") }, + { 3, "follows", QObject::tr("Sort by Follows") }, + { 4, "newest", QObject::tr("Sort by Last Updated") }, + { 5, "updated", QObject::tr("Sort by Newest") } }; + +QList ModrinthAPI::getSortingMethods() const +{ + return s_sorts; +} diff --git a/launcher/modplatform/modrinth/ModrinthAPI.h b/launcher/modplatform/modrinth/ModrinthAPI.h index ec38d9ee..949fc46e 100644 --- a/launcher/modplatform/modrinth/ModrinthAPI.h +++ b/launcher/modplatform/modrinth/ModrinthAPI.h @@ -49,6 +49,8 @@ class ModrinthAPI : public NetworkResourceAPI { NetJob::Ptr getProjects(QStringList addonIds, QByteArray* response) const override; public: + [[nodiscard]] auto getSortingMethods() const -> QList override; + inline auto getAuthorURL(const QString& name) const -> QString { return "https://modrinth.com/user/" + name; }; static auto getModLoaderStrings(const ModLoaderTypes types) -> const QStringList @@ -116,7 +118,7 @@ class ModrinthAPI : public NetworkResourceAPI { if (args.search.has_value()) get_arguments.append(QString("query=%1").arg(args.search.value())); if (args.sorting.has_value()) - get_arguments.append(QString("index=%1").arg(args.sorting.value())); + get_arguments.append(QString("index=%1").arg(args.sorting.value().name)); get_arguments.append(QString("facets=%1").arg(createFacets(args))); return BuildConfig.MODRINTH_PROD_URL + "/search?" + get_arguments.join('&'); diff --git a/launcher/ui/pages/modplatform/ModModel.cpp b/launcher/ui/pages/modplatform/ModModel.cpp index c9dee449..5eeac5d5 100644 --- a/launcher/ui/pages/modplatform/ModModel.cpp +++ b/launcher/ui/pages/modplatform/ModModel.cpp @@ -20,12 +20,23 @@ ResourceAPI::SearchArgs ModModel::createSearchArguments() Q_ASSERT(profile); Q_ASSERT(m_filter); - std::optional> versions {}; - if (!m_filter->versions.empty()) - versions = m_filter->versions; + std::optional> versions{}; + std::optional sort{}; - return { ModPlatform::ResourceType::MOD, m_next_search_offset, m_search_term, - getSorts()[currentSort], profile->getModLoaders(), versions }; + { // Version filter + if (!m_filter->versions.empty()) + versions = m_filter->versions; + } + + { // Sorting method + auto sorting_methods = getSortingMethods(); + auto method = std::find_if(sorting_methods.begin(), sorting_methods.end(), + [this](auto const& e) { return m_current_sort_index == e.index; }); + if (method != sorting_methods.end()) + sort = *method; + } + + return { ModPlatform::ResourceType::MOD, m_next_search_offset, m_search_term, sort, profile->getModLoaders(), versions }; } ResourceAPI::SearchCallbacks ModModel::createSearchCallbacks() { @@ -44,8 +55,8 @@ ResourceAPI::VersionSearchArgs ModModel::createVersionsArguments(QModelIndex& en Q_ASSERT(profile); Q_ASSERT(m_filter); - std::optional> versions {}; - if (!m_filter->versions.empty()) + std::optional> versions{}; + if (!m_filter->versions.empty()) versions = m_filter->versions; return { pack, versions, profile->getModLoaders() }; @@ -73,14 +84,14 @@ ResourceAPI::ProjectInfoCallbacks ModModel::createInfoCallbacks(QModelIndex& ent } }; } -void ModModel::searchWithTerm(const QString& term, const int sort, const bool filter_changed) +void ModModel::searchWithTerm(const QString& term, unsigned int sort, bool filter_changed) { - if (m_search_term == term && m_search_term.isNull() == term.isNull() && currentSort == sort && !filter_changed) { + if (m_search_term == term && m_search_term.isNull() == term.isNull() && m_current_sort_index == sort && !filter_changed) { return; } setSearchTerm(term); - currentSort = sort; + m_current_sort_index = sort; refresh(); } @@ -142,7 +153,7 @@ void ModModel::infoRequestFinished(QJsonDocument& doc, ModPlatform::IndexedPack& qWarning() << "Failed to cache mod info!"; return; } - + emit projectInfoUpdated(); } } diff --git a/launcher/ui/pages/modplatform/ModModel.h b/launcher/ui/pages/modplatform/ModModel.h index 39d062f9..3aeba3ef 100644 --- a/launcher/ui/pages/modplatform/ModModel.h +++ b/launcher/ui/pages/modplatform/ModModel.h @@ -21,7 +21,7 @@ class ModModel : public ResourceModel { ModModel(const BaseInstance&, ResourceAPI* api); /* Ask the API for more information */ - void searchWithTerm(const QString& term, const int sort, const bool filter_changed); + void searchWithTerm(const QString& term, unsigned int sort, bool filter_changed); virtual void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) = 0; virtual void loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj) = 0; @@ -46,11 +46,8 @@ class ModModel : public ResourceModel { protected: virtual auto documentToArray(QJsonDocument& obj) const -> QJsonArray = 0; - virtual auto getSorts() const -> const char** = 0; protected: - int currentSort = 0; - std::shared_ptr m_filter = nullptr; }; diff --git a/launcher/ui/pages/modplatform/ModPage.cpp b/launcher/ui/pages/modplatform/ModPage.cpp index 556bd642..04cbddcb 100644 --- a/launcher/ui/pages/modplatform/ModPage.cpp +++ b/launcher/ui/pages/modplatform/ModPage.cpp @@ -100,7 +100,7 @@ void ModPage::triggerSearch() updateSelectionButton(); } - static_cast(m_model)->searchWithTerm(getSearchTerm(), m_ui->sortByBox->currentIndex(), changed); + static_cast(m_model)->searchWithTerm(getSearchTerm(), m_ui->sortByBox->currentData().toUInt(), changed); m_fetch_progress.watch(&m_model->activeJob()); } diff --git a/launcher/ui/pages/modplatform/ResourceModel.h b/launcher/ui/pages/modplatform/ResourceModel.h index d0b9234b..facff91d 100644 --- a/launcher/ui/pages/modplatform/ResourceModel.h +++ b/launcher/ui/pages/modplatform/ResourceModel.h @@ -6,7 +6,9 @@ #include "QObjectPtr.h" #include "BaseInstance.h" + #include "modplatform/ResourceAPI.h" + #include "tasks/ConcurrentTask.h" class NetJob; @@ -41,6 +43,8 @@ class ResourceModel : public QAbstractListModel { inline void addActiveJob(Task::Ptr ptr) { m_current_job.addTask(ptr); if (!m_current_job.isRunning()) m_current_job.start(); } inline Task const& activeJob() { return m_current_job; } + [[nodiscard]] auto getSortingMethods() const { return m_api->getSortingMethods(); } + 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 @@ -83,6 +87,7 @@ class ResourceModel : public QAbstractListModel { enum class SearchState { None, CanFetchMore, ResetRequested, Finished } m_search_state = SearchState::None; int m_next_search_offset = 0; QString m_search_term; + unsigned int m_current_sort_index = 0; std::unique_ptr m_api; diff --git a/launcher/ui/pages/modplatform/ResourcePage.cpp b/launcher/ui/pages/modplatform/ResourcePage.cpp index 6e6868c5..43b77207 100644 --- a/launcher/ui/pages/modplatform/ResourcePage.cpp +++ b/launcher/ui/pages/modplatform/ResourcePage.cpp @@ -103,6 +103,17 @@ void ResourcePage::setSearchTerm(QString term) m_ui->searchEdit->setText(term); } +void ResourcePage::addSortings() +{ + Q_ASSERT(m_model); + + auto sorts = m_model->getSortingMethods(); + std::sort(sorts.begin(), sorts.end(), [](auto const& l, auto const& r) { return l.index < r.index; }); + + for (auto&& sorting : sorts) + m_ui->sortByBox->addItem(sorting.readable_name, QVariant(sorting.index)); +} + bool ResourcePage::setCurrentPack(ModPlatform::IndexedPack pack) { QVariant v; diff --git a/launcher/ui/pages/modplatform/ResourcePage.h b/launcher/ui/pages/modplatform/ResourcePage.h index b95c5a40..547c4056 100644 --- a/launcher/ui/pages/modplatform/ResourcePage.h +++ b/launcher/ui/pages/modplatform/ResourcePage.h @@ -52,14 +52,14 @@ class ResourcePage : public QWidget, public BasePage { [[nodiscard]] bool setCurrentPack(ModPlatform::IndexedPack); [[nodiscard]] auto getCurrentPack() const -> ModPlatform::IndexedPack; - [[nodiscard]] auto getDialog() const -> const ResourceDownloadDialog* { return m_parent_dialog; } - [[nodiscard]] auto getModel() const -> ResourceModel* { return m_model; } protected: ResourcePage(ResourceDownloadDialog* parent, BaseInstance&); + void addSortings(); + public slots: virtual void updateUi(); virtual void updateSelectionButton(); diff --git a/launcher/ui/pages/modplatform/flame/FlameResourceModels.cpp b/launcher/ui/pages/modplatform/flame/FlameResourceModels.cpp index d0f109de..a1cd1f26 100644 --- a/launcher/ui/pages/modplatform/flame/FlameResourceModels.cpp +++ b/launcher/ui/pages/modplatform/flame/FlameResourceModels.cpp @@ -7,9 +7,6 @@ namespace ResourceDownload { -// NOLINTNEXTLINE(modernize-avoid-c-arrays) -const char* FlameModModel::sorts[6]{ "Featured", "Popularity", "LastUpdated", "Name", "Author", "TotalDownloads" }; - FlameModModel::FlameModModel(BaseInstance const& base) : ModModel(base, new FlameAPI) {} void FlameModModel::loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) diff --git a/launcher/ui/pages/modplatform/flame/FlameResourceModels.h b/launcher/ui/pages/modplatform/flame/FlameResourceModels.h index 7b253dce..47fbbe1a 100644 --- a/launcher/ui/pages/modplatform/flame/FlameResourceModels.h +++ b/launcher/ui/pages/modplatform/flame/FlameResourceModels.h @@ -21,10 +21,6 @@ class FlameModModel : public ModModel { void loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) override; auto documentToArray(QJsonDocument& obj) const -> QJsonArray override; - - // NOLINTNEXTLINE(modernize-avoid-c-arrays) - static const char* sorts[6]; - inline auto getSorts() const -> const char** override { return sorts; }; }; } // namespace ResourceDownload diff --git a/launcher/ui/pages/modplatform/flame/FlameResourcePages.cpp b/launcher/ui/pages/modplatform/flame/FlameResourcePages.cpp index 67737a76..e34be7fd 100644 --- a/launcher/ui/pages/modplatform/flame/FlameResourcePages.cpp +++ b/launcher/ui/pages/modplatform/flame/FlameResourcePages.cpp @@ -48,13 +48,7 @@ FlameModPage::FlameModPage(ModDownloadDialog* dialog, BaseInstance& instance) m_model = new FlameModModel(instance); m_ui->packView->setModel(m_model); - // index is used to set the sorting with the flame api - m_ui->sortByBox->addItem(tr("Sort by Featured")); - m_ui->sortByBox->addItem(tr("Sort by Popularity")); - m_ui->sortByBox->addItem(tr("Sort by Last Updated")); - m_ui->sortByBox->addItem(tr("Sort by Name")); - m_ui->sortByBox->addItem(tr("Sort by Author")); - m_ui->sortByBox->addItem(tr("Sort by Downloads")); + addSortings(); // sometimes Qt just ignores virtual slots and doesn't work as intended it seems, // so it's best not to connect them in the parent's contructor... diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthResourceModels.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthResourceModels.cpp index 895e23fd..06b72fd0 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthResourceModels.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthResourceModels.cpp @@ -23,9 +23,6 @@ namespace ResourceDownload { -// NOLINTNEXTLINE(modernize-avoid-c-arrays) -const char* ModrinthModModel::sorts[5]{ "relevance", "downloads", "follows", "updated", "newest" }; - ModrinthModModel::ModrinthModModel(BaseInstance const& base) : ModModel(base, new ModrinthAPI) {} void ModrinthModModel::loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthResourceModels.h b/launcher/ui/pages/modplatform/modrinth/ModrinthResourceModels.h index 798a70e6..2511f5e5 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthResourceModels.h +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthResourceModels.h @@ -41,10 +41,6 @@ class ModrinthModModel : public ModModel { void loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) override; auto documentToArray(QJsonDocument& obj) const -> QJsonArray override; - - // NOLINTNEXTLINE(modernize-avoid-c-arrays) - static const char* sorts[5]; - inline auto getSorts() const -> const char** override { return sorts; }; }; } // namespace ResourceDownload diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.cpp index 88621e05..45902d16 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.cpp @@ -50,12 +50,7 @@ ModrinthModPage::ModrinthModPage(ModDownloadDialog* dialog, BaseInstance& instan m_model = new ModrinthModModel(instance); m_ui->packView->setModel(m_model); - // index is used to set the sorting with the modrinth api - m_ui->sortByBox->addItem(tr("Sort by Relevance")); - m_ui->sortByBox->addItem(tr("Sort by Downloads")); - m_ui->sortByBox->addItem(tr("Sort by Follows")); - m_ui->sortByBox->addItem(tr("Sort by Last Updated")); - m_ui->sortByBox->addItem(tr("Sort by Newest")); + addSortings(); // sometimes Qt just ignores virtual slots and doesn't work as intended it seems, // so it's best not to connect them in the parent's constructor... From 38e20eb1486928e10f4d3c128f3e9a6c697d872a Mon Sep 17 00:00:00 2001 From: flow Date: Tue, 20 Dec 2022 16:27:15 -0300 Subject: [PATCH 094/152] fix(RD): pass copy of IndexedPack to callbacks instead of ref. This prevents a crash in which the pack list gets updated in a search request meanwhile a versions / extra info request is being processed. Previously, this situation would cause the reference in the latter callbacks to be invalidated by an internal relocation of the pack list. Signed-off-by: flow --- launcher/modplatform/ResourceAPI.h | 8 +-- launcher/ui/pages/modplatform/ModModel.cpp | 58 ++++++++++++---------- 2 files changed, 36 insertions(+), 30 deletions(-) diff --git a/launcher/modplatform/ResourceAPI.h b/launcher/modplatform/ResourceAPI.h index a2078b94..5f4e1832 100644 --- a/launcher/modplatform/ResourceAPI.h +++ b/launcher/modplatform/ResourceAPI.h @@ -81,7 +81,7 @@ class ResourceAPI { }; struct VersionSearchArgs { - ModPlatform::IndexedPack& pack; + ModPlatform::IndexedPack pack; std::optional > mcVersions; std::optional loaders; @@ -94,16 +94,16 @@ class ResourceAPI { } }; struct VersionSearchCallbacks { - std::function on_succeed; + std::function on_succeed; }; struct ProjectInfoArgs { - ModPlatform::IndexedPack& pack; + ModPlatform::IndexedPack pack; void operator=(ProjectInfoArgs other) { pack = other.pack; } }; struct ProjectInfoCallbacks { - std::function on_succeed; + std::function on_succeed; }; public: diff --git a/launcher/ui/pages/modplatform/ModModel.cpp b/launcher/ui/pages/modplatform/ModModel.cpp index 5eeac5d5..29cb2132 100644 --- a/launcher/ui/pages/modplatform/ModModel.cpp +++ b/launcher/ui/pages/modplatform/ModModel.cpp @@ -63,7 +63,7 @@ ResourceAPI::VersionSearchArgs ModModel::createVersionsArguments(QModelIndex& en } ResourceAPI::VersionSearchCallbacks ModModel::createVersionsCallbacks(QModelIndex& entry) { - return { [this, entry](auto& doc, auto& pack) { + return { [this, entry](auto& doc, auto pack) { if (!s_running_models.constFind(this).value()) return; versionRequestSucceeded(doc, pack, entry); @@ -77,7 +77,7 @@ ResourceAPI::ProjectInfoArgs ModModel::createInfoArguments(QModelIndex& entry) } ResourceAPI::ProjectInfoCallbacks ModModel::createInfoCallbacks(QModelIndex& entry) { - return { [this, entry](auto& doc, auto& pack) { + return { [this, entry](auto& doc, auto pack) { if (!s_running_models.constFind(this).value()) return; infoRequestFinished(doc, pack, entry); @@ -136,51 +136,57 @@ void ModModel::infoRequestFinished(QJsonDocument& doc, ModPlatform::IndexedPack& { qDebug() << "Loading mod info"; + auto current_pack = data(index, Qt::UserRole).value(); + + // Check if the index is still valid for this mod or not + if (pack.addonId != current_pack.addonId) + return; + try { auto obj = Json::requireObject(doc); - loadExtraPackInfo(pack, obj); + loadExtraPackInfo(current_pack, obj); } catch (const JSONValidationError& e) { qDebug() << doc; qWarning() << "Error while reading " << debugName() << " mod info: " << e.cause(); } - // Check if the index is still valid for this mod or not - if (pack.addonId == data(index, Qt::UserRole).value().addonId) { - // Cache info :^) - QVariant new_pack; - new_pack.setValue(pack); - if (!setData(index, new_pack, Qt::UserRole)) { - qWarning() << "Failed to cache mod info!"; - return; - } - - emit projectInfoUpdated(); + // Cache info :^) + QVariant new_pack; + new_pack.setValue(current_pack); + if (!setData(index, new_pack, Qt::UserRole)) { + qWarning() << "Failed to cache mod info!"; + return; } + + emit projectInfoUpdated(); } void ModModel::versionRequestSucceeded(QJsonDocument doc, ModPlatform::IndexedPack& pack, const QModelIndex& index) { auto arr = doc.isObject() ? Json::ensureArray(doc.object(), "data") : doc.array(); + auto current_pack = data(index, Qt::UserRole).value(); + + // Check if the index is still valid for this mod or not + if (pack.addonId != current_pack.addonId) + return; + try { - loadIndexedPackVersions(pack, arr); + loadIndexedPackVersions(current_pack, arr); } catch (const JSONValidationError& e) { qDebug() << doc; qWarning() << "Error while reading " << debugName() << " mod version: " << e.cause(); } - // Check if the index is still valid for this mod or not - if (pack.addonId == data(index, Qt::UserRole).value().addonId) { - // Cache info :^) - QVariant new_pack; - new_pack.setValue(pack); - if (!setData(index, new_pack, Qt::UserRole)) { - qWarning() << "Failed to cache mod versions!"; - return; - } - - emit versionListUpdated(); + // Cache info :^) + QVariant new_pack; + new_pack.setValue(current_pack); + if (!setData(index, new_pack, Qt::UserRole)) { + qWarning() << "Failed to cache mod versions!"; + return; } + + emit versionListUpdated(); } } // namespace ResourceDownload From 563fe8d51529bc4c769f5a08bc037fc40cbfe852 Mon Sep 17 00:00:00 2001 From: flow Date: Tue, 20 Dec 2022 17:14:17 -0300 Subject: [PATCH 095/152] fix(RD): separate search and versions/info tasks This allows us to check whether a search request is already on-going, in which case we don't need to make another one (and shouldn't). Signed-off-by: flow --- launcher/ui/pages/modplatform/ModPage.cpp | 2 +- .../ui/pages/modplatform/ResourceModel.cpp | 45 +++++++++++++++---- launcher/ui/pages/modplatform/ResourceModel.h | 13 ++++-- .../ui/pages/modplatform/ResourcePage.cpp | 4 +- 4 files changed, 49 insertions(+), 15 deletions(-) diff --git a/launcher/ui/pages/modplatform/ModPage.cpp b/launcher/ui/pages/modplatform/ModPage.cpp index 04cbddcb..d57e748b 100644 --- a/launcher/ui/pages/modplatform/ModPage.cpp +++ b/launcher/ui/pages/modplatform/ModPage.cpp @@ -101,7 +101,7 @@ void ModPage::triggerSearch() } static_cast(m_model)->searchWithTerm(getSearchTerm(), m_ui->sortByBox->currentData().toUInt(), changed); - m_fetch_progress.watch(&m_model->activeJob()); + m_fetch_progress.watch(m_model->activeSearchJob().get()); } QMap ModPage::urlHandlers() const diff --git a/launcher/ui/pages/modplatform/ResourceModel.cpp b/launcher/ui/pages/modplatform/ResourceModel.cpp index eedc5202..5bbd39d3 100644 --- a/launcher/ui/pages/modplatform/ResourceModel.cpp +++ b/launcher/ui/pages/modplatform/ResourceModel.cpp @@ -123,8 +123,8 @@ void ResourceModel::fetchMore(const QModelIndex& parent) void ResourceModel::search() { - if (!m_current_job.isRunning()) - m_current_job.clear(); + if (hasActiveSearchJob()) + return; auto args{ createSearchArguments() }; @@ -146,22 +146,22 @@ void ResourceModel::search() }; if (auto job = m_api->searchProjects(std::move(args), std::move(callbacks)); job) - addActiveJob(job); + runSearchJob(job); } void ResourceModel::loadEntry(QModelIndex& entry) { auto const& pack = m_packs[entry.row()]; - if (!m_current_job.isRunning()) - m_current_job.clear(); + if (!hasActiveInfoJob()) + m_current_info_job.clear(); if (!pack.versionsLoaded) { auto args{ createVersionsArguments(entry) }; auto callbacks{ createVersionsCallbacks(entry) }; if (auto job = m_api->getProjectVersions(std::move(args), std::move(callbacks)); job) - addActiveJob(job); + runInfoJob(job); } if (!pack.extraDataLoaded) { @@ -169,14 +169,25 @@ void ResourceModel::loadEntry(QModelIndex& entry) auto callbacks{ createInfoCallbacks(entry) }; if (auto job = m_api->getProjectInfo(std::move(args), std::move(callbacks)); job) - addActiveJob(job); + runInfoJob(job); } } void ResourceModel::refresh() { - if (m_current_job.isRunning()) { - m_current_job.abort(); + bool reset_requested = false; + + if (hasActiveInfoJob()) { + m_current_info_job.abort(); + reset_requested = true; + } + + if (hasActiveSearchJob()) { + m_current_search_job->abort(); + reset_requested = true; + } + + if (reset_requested) { m_search_state = SearchState::ResetRequested; return; } @@ -195,6 +206,22 @@ void ResourceModel::clearData() endResetModel(); } +void ResourceModel::runSearchJob(NetJob::Ptr ptr) +{ + m_current_search_job = ptr; + m_current_search_job->start(); +} +void ResourceModel::runInfoJob(Task::Ptr ptr) +{ + if (!m_current_info_job.isRunning()) + m_current_info_job.clear(); + + m_current_info_job.addTask(ptr); + + if (!m_current_info_job.isRunning()) + m_current_info_job.run(); +} + std::optional ResourceModel::getIcon(QModelIndex& index, const QUrl& url) { QPixmap pixmap; diff --git a/launcher/ui/pages/modplatform/ResourceModel.h b/launcher/ui/pages/modplatform/ResourceModel.h index facff91d..5f9ce36d 100644 --- a/launcher/ui/pages/modplatform/ResourceModel.h +++ b/launcher/ui/pages/modplatform/ResourceModel.h @@ -40,8 +40,9 @@ class ResourceModel : public QAbstractListModel { [[nodiscard]] inline int columnCount(const QModelIndex& parent) const override { return parent.isValid() ? 0 : 1; } [[nodiscard]] inline auto flags(const QModelIndex& index) const -> Qt::ItemFlags override { return QAbstractListModel::flags(index); } - inline void addActiveJob(Task::Ptr ptr) { m_current_job.addTask(ptr); if (!m_current_job.isRunning()) m_current_job.start(); } - inline Task const& activeJob() { return m_current_job; } + [[nodiscard]] bool hasActiveSearchJob() const { return m_current_search_job && m_current_search_job->isRunning(); } + [[nodiscard]] bool hasActiveInfoJob() const { return m_current_info_job.isRunning(); } + [[nodiscard]] Task::Ptr activeSearchJob() { return hasActiveSearchJob() ? m_current_search_job : nullptr; } [[nodiscard]] auto getSortingMethods() const { return m_api->getSortingMethods(); } @@ -80,6 +81,9 @@ class ResourceModel : public QAbstractListModel { /** Resets the model's data. */ void clearData(); + void runSearchJob(NetJob::Ptr); + void runInfoJob(Task::Ptr); + protected: const BaseInstance& m_base_instance; @@ -91,7 +95,10 @@ class ResourceModel : public QAbstractListModel { std::unique_ptr m_api; - ConcurrentTask m_current_job; + // Job for searching for new entries + shared_qobject_ptr m_current_search_job; + // Job for fetching versions and extra info on existing entries + ConcurrentTask m_current_info_job; shared_qobject_ptr m_current_icon_job; QSet m_currently_running_icon_actions; diff --git a/launcher/ui/pages/modplatform/ResourcePage.cpp b/launcher/ui/pages/modplatform/ResourcePage.cpp index 43b77207..200943da 100644 --- a/launcher/ui/pages/modplatform/ResourcePage.cpp +++ b/launcher/ui/pages/modplatform/ResourcePage.cpp @@ -353,8 +353,8 @@ void ResourcePage::openUrl(const QUrl& url) searchEdit->setText(slug); newPage->triggerSearch(); - if (model->activeJob().isRunning()) - connect(&model->activeJob(), &Task::finished, jump); + if (model->hasActiveSearchJob()) + connect(model->activeSearchJob().get(), &Task::finished, jump); else jump(); From c3f0139f76b8aacef685c8c97d54f2098bbca5c4 Mon Sep 17 00:00:00 2001 From: flow Date: Fri, 23 Dec 2022 17:28:42 -0300 Subject: [PATCH 096/152] refactor(RD): add helper in ResourceModel to find current sorting Signed-off-by: flow --- launcher/ui/pages/modplatform/ModModel.cpp | 9 +-------- launcher/ui/pages/modplatform/ResourceModel.cpp | 15 +++++++++++++++ launcher/ui/pages/modplatform/ResourceModel.h | 2 ++ 3 files changed, 18 insertions(+), 8 deletions(-) diff --git a/launcher/ui/pages/modplatform/ModModel.cpp b/launcher/ui/pages/modplatform/ModModel.cpp index 29cb2132..beb8aec1 100644 --- a/launcher/ui/pages/modplatform/ModModel.cpp +++ b/launcher/ui/pages/modplatform/ModModel.cpp @@ -21,20 +21,13 @@ ResourceAPI::SearchArgs ModModel::createSearchArguments() Q_ASSERT(m_filter); std::optional> versions{}; - std::optional sort{}; { // Version filter if (!m_filter->versions.empty()) versions = m_filter->versions; } - { // Sorting method - auto sorting_methods = getSortingMethods(); - auto method = std::find_if(sorting_methods.begin(), sorting_methods.end(), - [this](auto const& e) { return m_current_sort_index == e.index; }); - if (method != sorting_methods.end()) - sort = *method; - } + auto sort = getCurrentSortingMethodByIndex(); return { ModPlatform::ResourceType::MOD, m_next_search_offset, m_search_term, sort, profile->getModLoaders(), versions }; } diff --git a/launcher/ui/pages/modplatform/ResourceModel.cpp b/launcher/ui/pages/modplatform/ResourceModel.cpp index 5bbd39d3..d9c30912 100644 --- a/launcher/ui/pages/modplatform/ResourceModel.cpp +++ b/launcher/ui/pages/modplatform/ResourceModel.cpp @@ -222,6 +222,21 @@ void ResourceModel::runInfoJob(Task::Ptr ptr) m_current_info_job.run(); } +std::optional ResourceModel::getCurrentSortingMethodByIndex() const +{ + std::optional sort{}; + + { // Find sorting method by ID + auto sorting_methods = getSortingMethods(); + auto method = std::find_if(sorting_methods.constBegin(), sorting_methods.constEnd(), + [this](auto const& e) { return m_current_sort_index == e.index; }); + if (method != sorting_methods.constEnd()) + sort = *method; + } + + return sort; +} + std::optional ResourceModel::getIcon(QModelIndex& index, const QUrl& url) { QPixmap pixmap; diff --git a/launcher/ui/pages/modplatform/ResourceModel.h b/launcher/ui/pages/modplatform/ResourceModel.h index 5f9ce36d..05aa6a94 100644 --- a/launcher/ui/pages/modplatform/ResourceModel.h +++ b/launcher/ui/pages/modplatform/ResourceModel.h @@ -84,6 +84,8 @@ class ResourceModel : public QAbstractListModel { void runSearchJob(NetJob::Ptr); void runInfoJob(Task::Ptr); + [[nodiscard]] auto getCurrentSortingMethodByIndex() const -> std::optional; + protected: const BaseInstance& m_base_instance; From 3cff23dae24d26f10624d50ac68e9ed2c61fbca1 Mon Sep 17 00:00:00 2001 From: flow Date: Fri, 23 Dec 2022 18:18:20 -0300 Subject: [PATCH 097/152] refactor(RD): move success callbacks from ModModel to ResourceModel While implementing the resource pack downloader in another branch, I noticed that most of the code in the success callback was identical in both cases, safe for a few minute differences in strings. So, this tries to make it easier to share this piece of code. However, it still leaves the possibility of extending the methods in ResourceModel to accomodate for cases where this similarity may not hold. Signed-off-by: flow --- launcher/ui/pages/modplatform/ModModel.cpp | 119 -------------- launcher/ui/pages/modplatform/ModModel.h | 18 +-- .../ui/pages/modplatform/ResourceModel.cpp | 146 +++++++++++++++++- launcher/ui/pages/modplatform/ResourceModel.h | 27 +++- 4 files changed, 166 insertions(+), 144 deletions(-) diff --git a/launcher/ui/pages/modplatform/ModModel.cpp b/launcher/ui/pages/modplatform/ModModel.cpp index beb8aec1..d52a430e 100644 --- a/launcher/ui/pages/modplatform/ModModel.cpp +++ b/launcher/ui/pages/modplatform/ModModel.cpp @@ -1,7 +1,5 @@ #include "ModModel.h" -#include "Json.h" - #include "minecraft/MinecraftInstance.h" #include "minecraft/PackProfile.h" @@ -31,14 +29,6 @@ ResourceAPI::SearchArgs ModModel::createSearchArguments() return { ModPlatform::ResourceType::MOD, m_next_search_offset, m_search_term, sort, profile->getModLoaders(), versions }; } -ResourceAPI::SearchCallbacks ModModel::createSearchCallbacks() -{ - return { [this](auto& doc) { - if (!s_running_models.constFind(this).value()) - return; - searchRequestFinished(doc); - } }; -} ResourceAPI::VersionSearchArgs ModModel::createVersionsArguments(QModelIndex& entry) { @@ -54,28 +44,12 @@ ResourceAPI::VersionSearchArgs ModModel::createVersionsArguments(QModelIndex& en return { pack, versions, profile->getModLoaders() }; } -ResourceAPI::VersionSearchCallbacks ModModel::createVersionsCallbacks(QModelIndex& entry) -{ - return { [this, entry](auto& doc, auto pack) { - if (!s_running_models.constFind(this).value()) - return; - versionRequestSucceeded(doc, pack, entry); - } }; -} ResourceAPI::ProjectInfoArgs ModModel::createInfoArguments(QModelIndex& entry) { auto& pack = m_packs[entry.row()]; return { pack }; } -ResourceAPI::ProjectInfoCallbacks ModModel::createInfoCallbacks(QModelIndex& entry) -{ - return { [this, entry](auto& doc, auto pack) { - if (!s_running_models.constFind(this).value()) - return; - infoRequestFinished(doc, pack, entry); - } }; -} void ModModel::searchWithTerm(const QString& term, unsigned int sort, bool filter_changed) { @@ -89,97 +63,4 @@ void ModModel::searchWithTerm(const QString& term, unsigned int sort, bool filte refresh(); } -/******** Request callbacks ********/ - -void ModModel::searchRequestFinished(QJsonDocument& doc) -{ - QList newList; - auto packs = documentToArray(doc); - - for (auto packRaw : packs) { - auto packObj = packRaw.toObject(); - - ModPlatform::IndexedPack pack; - try { - loadIndexedPack(pack, packObj); - newList.append(pack); - } catch (const JSONValidationError& e) { - qWarning() << "Error while loading mod from " << debugName() << ": " << e.cause(); - continue; - } - } - - if (packs.size() < 25) { - m_search_state = SearchState::Finished; - } else { - m_next_search_offset += 25; - m_search_state = SearchState::CanFetchMore; - } - - // When you have a Qt build with assertions turned on, proceeding here will abort the application - if (newList.size() == 0) - return; - - beginInsertRows(QModelIndex(), m_packs.size(), m_packs.size() + newList.size() - 1); - m_packs.append(newList); - endInsertRows(); -} - -void ModModel::infoRequestFinished(QJsonDocument& doc, ModPlatform::IndexedPack& pack, const QModelIndex& index) -{ - qDebug() << "Loading mod info"; - - auto current_pack = data(index, Qt::UserRole).value(); - - // Check if the index is still valid for this mod or not - if (pack.addonId != current_pack.addonId) - return; - - try { - auto obj = Json::requireObject(doc); - loadExtraPackInfo(current_pack, obj); - } catch (const JSONValidationError& e) { - qDebug() << doc; - qWarning() << "Error while reading " << debugName() << " mod info: " << e.cause(); - } - - // Cache info :^) - QVariant new_pack; - new_pack.setValue(current_pack); - if (!setData(index, new_pack, Qt::UserRole)) { - qWarning() << "Failed to cache mod info!"; - return; - } - - emit projectInfoUpdated(); -} - -void ModModel::versionRequestSucceeded(QJsonDocument doc, ModPlatform::IndexedPack& pack, const QModelIndex& index) -{ - auto arr = doc.isObject() ? Json::ensureArray(doc.object(), "data") : doc.array(); - - auto current_pack = data(index, Qt::UserRole).value(); - - // Check if the index is still valid for this mod or not - if (pack.addonId != current_pack.addonId) - return; - - try { - loadIndexedPackVersions(current_pack, arr); - } catch (const JSONValidationError& e) { - qDebug() << doc; - qWarning() << "Error while reading " << debugName() << " mod version: " << e.cause(); - } - - // Cache info :^) - QVariant new_pack; - new_pack.setValue(current_pack); - if (!setData(index, new_pack, Qt::UserRole)) { - qWarning() << "Failed to cache mod versions!"; - return; - } - - emit versionListUpdated(); -} - } // namespace ResourceDownload diff --git a/launcher/ui/pages/modplatform/ModModel.h b/launcher/ui/pages/modplatform/ModModel.h index 3aeba3ef..c705371a 100644 --- a/launcher/ui/pages/modplatform/ModModel.h +++ b/launcher/ui/pages/modplatform/ModModel.h @@ -23,29 +23,19 @@ class ModModel : public ResourceModel { /* Ask the API for more information */ void searchWithTerm(const QString& term, unsigned int sort, bool filter_changed); - virtual void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) = 0; - virtual void loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj) = 0; - virtual void loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) = 0; + void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) override = 0; + void loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj) override = 0; + void loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) override = 0; void setFilter(std::shared_ptr filter) { m_filter = filter; } - public slots: - void searchRequestFinished(QJsonDocument& doc); - void infoRequestFinished(QJsonDocument& doc, ModPlatform::IndexedPack& pack, const QModelIndex& index); - void versionRequestSucceeded(QJsonDocument doc, ModPlatform::IndexedPack& pack, const QModelIndex& index); - public slots: ResourceAPI::SearchArgs createSearchArguments() override; - ResourceAPI::SearchCallbacks createSearchCallbacks() override; - ResourceAPI::VersionSearchArgs createVersionsArguments(QModelIndex&) override; - ResourceAPI::VersionSearchCallbacks createVersionsCallbacks(QModelIndex&) override; - ResourceAPI::ProjectInfoArgs createInfoArguments(QModelIndex&) override; - ResourceAPI::ProjectInfoCallbacks createInfoCallbacks(QModelIndex&) override; protected: - virtual auto documentToArray(QJsonDocument& obj) const -> QJsonArray = 0; + auto documentToArray(QJsonDocument& obj) const -> QJsonArray override = 0; protected: std::shared_ptr m_filter = nullptr; diff --git a/launcher/ui/pages/modplatform/ResourceModel.cpp b/launcher/ui/pages/modplatform/ResourceModel.cpp index d9c30912..05d44ee2 100644 --- a/launcher/ui/pages/modplatform/ResourceModel.cpp +++ b/launcher/ui/pages/modplatform/ResourceModel.cpp @@ -8,13 +8,11 @@ #include "Application.h" #include "BuildConfig.h" +#include "Json.h" #include "net/Download.h" #include "net/NetJob.h" -#include "minecraft/MinecraftInstance.h" -#include "minecraft/PackProfile.h" - #include "modplatform/ModIndex.h" #include "ui/widgets/ProjectItem.h" @@ -129,9 +127,14 @@ void ResourceModel::search() auto args{ createSearchArguments() }; auto callbacks{ createSearchCallbacks() }; - Q_ASSERT(callbacks.on_succeed); // Use defaults if no callbacks are set + if (!callbacks.on_succeed) + callbacks.on_succeed = [this](auto& doc) { + if (!s_running_models.constFind(this).value()) + return; + searchRequestSucceeded(doc); + }; if (!callbacks.on_fail) callbacks.on_fail = [this](QString reason, int network_error_code) { if (!s_running_models.constFind(this).value()) @@ -160,6 +163,14 @@ void ResourceModel::loadEntry(QModelIndex& entry) auto args{ createVersionsArguments(entry) }; auto callbacks{ createVersionsCallbacks(entry) }; + // Use default if no callbacks are set + if (!callbacks.on_succeed) + callbacks.on_succeed = [this, entry](auto& doc, auto pack) { + if (!s_running_models.constFind(this).value()) + return; + versionRequestSucceeded(doc, pack, entry); + }; + if (auto job = m_api->getProjectVersions(std::move(args), std::move(callbacks)); job) runInfoJob(job); } @@ -168,6 +179,14 @@ void ResourceModel::loadEntry(QModelIndex& entry) auto args{ createInfoArguments(entry) }; auto callbacks{ createInfoCallbacks(entry) }; + // Use default if no callbacks are set + if (!callbacks.on_succeed) + callbacks.on_succeed = [this, entry](auto& doc, auto pack) { + if (!s_running_models.constFind(this).value()) + return; + infoRequestSucceeded(doc, pack, entry); + }; + if (auto job = m_api->getProjectInfo(std::move(args), std::move(callbacks)); job) runInfoJob(job); } @@ -226,10 +245,10 @@ std::optional ResourceModel::getCurrentSortingMethod { std::optional sort{}; - { // Find sorting method by ID + { // Find sorting method by ID auto sorting_methods = getSortingMethods(); auto method = std::find_if(sorting_methods.constBegin(), sorting_methods.constEnd(), - [this](auto const& e) { return m_current_sort_index == e.index; }); + [this](auto const& e) { return m_current_sort_index == e.index; }); if (method != sorting_methods.constEnd()) sort = *method; } @@ -279,6 +298,64 @@ std::optional ResourceModel::getIcon(QModelIndex& index, const QUrl& url) return {}; } +// No 'forgor to implement' shall pass here :blobfox_knife: +#define NEED_FOR_CALLBACK_ASSERT(name) \ + Q_ASSERT_X(0 != 0, #name, "You NEED to re-implement this if you intend on using the default callbacks.") + +QJsonArray ResourceModel::documentToArray(QJsonDocument& doc) const +{ + NEED_FOR_CALLBACK_ASSERT("documentToArray"); + return {}; +} +void ResourceModel::loadIndexedPack(ModPlatform::IndexedPack&, QJsonObject&) +{ + NEED_FOR_CALLBACK_ASSERT("loadIndexedPack"); +} +void ResourceModel::loadExtraPackInfo(ModPlatform::IndexedPack&, QJsonObject&) +{ + NEED_FOR_CALLBACK_ASSERT("loadExtraPackInfo"); +} +void ResourceModel::loadIndexedPackVersions(ModPlatform::IndexedPack&, QJsonArray&) +{ + NEED_FOR_CALLBACK_ASSERT("loadIndexedPackVersions"); +} + +/* Default callbacks */ + +void ResourceModel::searchRequestSucceeded(QJsonDocument& doc) +{ + QList newList; + auto packs = documentToArray(doc); + + for (auto packRaw : packs) { + auto packObj = packRaw.toObject(); + + ModPlatform::IndexedPack pack; + try { + loadIndexedPack(pack, packObj); + newList.append(pack); + } catch (const JSONValidationError& e) { + qWarning() << "Error while loading resource from " << debugName() << ": " << e.cause(); + continue; + } + } + + if (packs.size() < 25) { + m_search_state = SearchState::Finished; + } else { + m_next_search_offset += 25; + m_search_state = SearchState::CanFetchMore; + } + + // When you have a Qt build with assertions turned on, proceeding here will abort the application + if (newList.size() == 0) + return; + + beginInsertRows(QModelIndex(), m_packs.size(), m_packs.size() + newList.size() - 1); + m_packs.append(newList); + endInsertRows(); +} + void ResourceModel::searchRequestFailed(QString reason, int network_error_code) { switch (network_error_code) { @@ -289,8 +366,7 @@ void ResourceModel::searchRequestFailed(QString reason, int network_error_code) case 409: // 409 Gone, notify user to update QMessageBox::critical(nullptr, tr("Error"), - QString("%1") - .arg(tr("API version too old!\nPlease update %1!").arg(BuildConfig.LAUNCHER_DISPLAYNAME))); + QString("%1").arg(tr("API version too old!\nPlease update %1!").arg(BuildConfig.LAUNCHER_DISPLAYNAME))); break; } @@ -309,4 +385,58 @@ void ResourceModel::searchRequestAborted() search(); } +void ResourceModel::versionRequestSucceeded(QJsonDocument& doc, ModPlatform::IndexedPack& pack, const QModelIndex& index) +{ + auto current_pack = data(index, Qt::UserRole).value(); + + // Check if the index is still valid for this resource or not + if (pack.addonId != current_pack.addonId) + return; + + try { + auto arr = doc.isObject() ? Json::ensureArray(doc.object(), "data") : doc.array(); + loadIndexedPackVersions(current_pack, arr); + } catch (const JSONValidationError& e) { + qDebug() << doc; + qWarning() << "Error while reading " << debugName() << " resource version: " << e.cause(); + } + + // Cache info :^) + QVariant new_pack; + new_pack.setValue(current_pack); + if (!setData(index, new_pack, Qt::UserRole)) { + qWarning() << "Failed to cache resource versions!"; + return; + } + + emit versionListUpdated(); +} + +void ResourceModel::infoRequestSucceeded(QJsonDocument& doc, ModPlatform::IndexedPack& pack, const QModelIndex& index) +{ + auto current_pack = data(index, Qt::UserRole).value(); + + // Check if the index is still valid for this resource or not + if (pack.addonId != current_pack.addonId) + return; + + try { + auto obj = Json::requireObject(doc); + loadExtraPackInfo(current_pack, obj); + } catch (const JSONValidationError& e) { + qDebug() << doc; + qWarning() << "Error while reading " << debugName() << " resource info: " << e.cause(); + } + + // Cache info :^) + QVariant new_pack; + new_pack.setValue(current_pack); + if (!setData(index, new_pack, Qt::UserRole)) { + qWarning() << "Failed to cache resource info!"; + return; + } + + emit projectInfoUpdated(); +} + } // namespace ResourceDownload diff --git a/launcher/ui/pages/modplatform/ResourceModel.h b/launcher/ui/pages/modplatform/ResourceModel.h index 05aa6a94..d8be3b6b 100644 --- a/launcher/ui/pages/modplatform/ResourceModel.h +++ b/launcher/ui/pages/modplatform/ResourceModel.h @@ -57,13 +57,13 @@ class ResourceModel : public QAbstractListModel { void setSearchTerm(QString term) { m_search_term = term; } virtual ResourceAPI::SearchArgs createSearchArguments() = 0; - virtual ResourceAPI::SearchCallbacks createSearchCallbacks() = 0; + virtual ResourceAPI::SearchCallbacks createSearchCallbacks() { return {}; } virtual ResourceAPI::VersionSearchArgs createVersionsArguments(QModelIndex&) = 0; - virtual ResourceAPI::VersionSearchCallbacks createVersionsCallbacks(QModelIndex&) = 0; + virtual ResourceAPI::VersionSearchCallbacks createVersionsCallbacks(QModelIndex&) { return {}; } virtual ResourceAPI::ProjectInfoArgs createInfoArguments(QModelIndex&) = 0; - virtual ResourceAPI::ProjectInfoCallbacks createInfoCallbacks(QModelIndex&) = 0; + virtual ResourceAPI::ProjectInfoCallbacks createInfoCallbacks(QModelIndex&) { return {}; } /** Requests the API for more entries. */ virtual void search(); @@ -86,6 +86,22 @@ class ResourceModel : public QAbstractListModel { [[nodiscard]] auto getCurrentSortingMethodByIndex() const -> std::optional; + /** Converts a JSON document to a common array format. + * + * This is needed so that different providers, with different JSON structures, can be parsed + * uniformally. You NEED to re-implement this if you intend on using the default callbacks. + */ + [[nodiscard]] virtual auto documentToArray(QJsonDocument&) const -> QJsonArray; + + /** Functions to load data into a pack. + * + * Those are needed for the same reason as ddocumentToArray, and NEED to be re-implemented in the same way. + */ + + virtual void loadIndexedPack(ModPlatform::IndexedPack&, QJsonObject&); + virtual void loadExtraPackInfo(ModPlatform::IndexedPack&, QJsonObject&); + virtual void loadIndexedPackVersions(ModPlatform::IndexedPack&, QJsonArray&); + protected: const BaseInstance& m_base_instance; @@ -114,9 +130,14 @@ class ResourceModel : public QAbstractListModel { private: /* Default search request callbacks */ + void searchRequestSucceeded(QJsonDocument&); void searchRequestFailed(QString reason, int network_error_code); void searchRequestAborted(); + void versionRequestSucceeded(QJsonDocument&, ModPlatform::IndexedPack&, const QModelIndex&); + + void infoRequestSucceeded(QJsonDocument&, ModPlatform::IndexedPack&, const QModelIndex&); + signals: void versionListUpdated(); void projectInfoUpdated(); From 7d128c79a3cceb5e88157ead72009642ee0e4a07 Mon Sep 17 00:00:00 2001 From: flow Date: Wed, 28 Dec 2022 15:19:20 -0300 Subject: [PATCH 098/152] fix: CodeQL warnings about the rule of two shush Signed-off-by: flow --- launcher/modplatform/ResourceAPI.h | 2 ++ 1 file changed, 2 insertions(+) diff --git a/launcher/modplatform/ResourceAPI.h b/launcher/modplatform/ResourceAPI.h index 5f4e1832..8f794955 100644 --- a/launcher/modplatform/ResourceAPI.h +++ b/launcher/modplatform/ResourceAPI.h @@ -86,6 +86,7 @@ class ResourceAPI { std::optional > mcVersions; std::optional loaders; + VersionSearchArgs(VersionSearchArgs const&) = default; void operator=(VersionSearchArgs other) { pack = other.pack; @@ -100,6 +101,7 @@ class ResourceAPI { struct ProjectInfoArgs { ModPlatform::IndexedPack pack; + ProjectInfoArgs(ProjectInfoArgs const&) = default; void operator=(ProjectInfoArgs other) { pack = other.pack; } }; struct ProjectInfoCallbacks { From b3330cb0da39db6e8add3bbe35cd6d417374146a Mon Sep 17 00:00:00 2001 From: flow Date: Fri, 30 Dec 2022 16:59:35 -0300 Subject: [PATCH 099/152] fix(RD): correctly set the strings for the specific resource names Signed-off-by: flow --- launcher/ui/dialogs/ResourceDownloadDialog.cpp | 3 ++- launcher/ui/pages/modplatform/ModPage.h | 3 +++ launcher/ui/pages/modplatform/ResourcePage.cpp | 4 ++++ launcher/ui/pages/modplatform/ResourcePage.h | 3 +++ launcher/ui/pages/modplatform/ResourcePage.ui | 12 ++---------- 5 files changed, 14 insertions(+), 11 deletions(-) diff --git a/launcher/ui/dialogs/ResourceDownloadDialog.cpp b/launcher/ui/dialogs/ResourceDownloadDialog.cpp index 2eb85928..fa3352b3 100644 --- a/launcher/ui/dialogs/ResourceDownloadDialog.cpp +++ b/launcher/ui/dialogs/ResourceDownloadDialog.cpp @@ -64,7 +64,6 @@ ResourceDownloadDialog::ResourceDownloadDialog(QWidget* parent, const std::share HelpButton->setAutoDefault(false); setWindowModality(Qt::WindowModal); - setWindowTitle(dialogTitle()); } void ResourceDownloadDialog::accept() @@ -206,6 +205,8 @@ void ResourceDownloadDialog::selectedPageChanged(BasePage* previous, BasePage* s ModDownloadDialog::ModDownloadDialog(QWidget* parent, const std::shared_ptr& mods, BaseInstance* instance) : ResourceDownloadDialog(parent, mods), m_instance(instance) { + setWindowTitle(dialogTitle()); + initializeContainer(); connectButtons(); diff --git a/launcher/ui/pages/modplatform/ModPage.h b/launcher/ui/pages/modplatform/ModPage.h index 2fda3b68..a3aab1de 100644 --- a/launcher/ui/pages/modplatform/ModPage.h +++ b/launcher/ui/pages/modplatform/ModPage.h @@ -39,6 +39,9 @@ class ModPage : public ResourcePage { ~ModPage() override = default; + //: The plural version of 'mod' + [[nodiscard]] inline QString resourcesString() const override { return tr("mods"); } + //: The singular version of 'mods' [[nodiscard]] inline QString resourceString() const override { return tr("mod"); } [[nodiscard]] QMap urlHandlers() const override; diff --git a/launcher/ui/pages/modplatform/ResourcePage.cpp b/launcher/ui/pages/modplatform/ResourcePage.cpp index 200943da..bfa7e33d 100644 --- a/launcher/ui/pages/modplatform/ResourcePage.cpp +++ b/launcher/ui/pages/modplatform/ResourcePage.cpp @@ -57,6 +57,10 @@ void ResourcePage::openedImpl() if (!supportsFiltering()) m_ui->resourceFilterButton->setVisible(false); + //: String in the search bar of the mod downloading dialog + m_ui->searchEdit->setPlaceholderText(tr("Search for %1...").arg(resourcesString())); + m_ui->resourceSelectionButton->setText(tr("Select %1 for download").arg(resourceString())); + updateSelectionButton(); triggerSearch(); } diff --git a/launcher/ui/pages/modplatform/ResourcePage.h b/launcher/ui/pages/modplatform/ResourcePage.h index 547c4056..71fc6593 100644 --- a/launcher/ui/pages/modplatform/ResourcePage.h +++ b/launcher/ui/pages/modplatform/ResourcePage.h @@ -36,6 +36,9 @@ class ResourcePage : public QWidget, public BasePage { [[nodiscard]] virtual auto metaEntryBase() const -> QString = 0; [[nodiscard]] virtual auto debugName() const -> QString = 0; + //: The plural version of 'resource' + [[nodiscard]] virtual inline QString resourcesString() const { return tr("resources"); } + //: The singular version of 'resources' [[nodiscard]] virtual inline QString resourceString() const { return tr("resource"); } /* Features this resource's page supports */ diff --git a/launcher/ui/pages/modplatform/ResourcePage.ui b/launcher/ui/pages/modplatform/ResourcePage.ui index 8fe1d613..73a9d3b1 100644 --- a/launcher/ui/pages/modplatform/ResourcePage.ui +++ b/launcher/ui/pages/modplatform/ResourcePage.ui @@ -49,11 +49,7 @@ - - - Search for resources... - - + @@ -74,11 +70,7 @@ - - - Select resource for download - - + From e62e1d9701703d3c8a1c47f6be58c5a5b1b41348 Mon Sep 17 00:00:00 2001 From: flow Date: Tue, 3 Jan 2023 12:48:22 -0300 Subject: [PATCH 100/152] refactor(RD): move BaseInstance dep. to subclasses of ResourceModel Signed-off-by: flow --- launcher/ui/pages/modplatform/ModModel.cpp | 2 +- launcher/ui/pages/modplatform/ModModel.h | 4 ++++ launcher/ui/pages/modplatform/ResourceModel.cpp | 2 +- launcher/ui/pages/modplatform/ResourceModel.h | 5 +---- 4 files changed, 7 insertions(+), 6 deletions(-) diff --git a/launcher/ui/pages/modplatform/ModModel.cpp b/launcher/ui/pages/modplatform/ModModel.cpp index d52a430e..433c7b10 100644 --- a/launcher/ui/pages/modplatform/ModModel.cpp +++ b/launcher/ui/pages/modplatform/ModModel.cpp @@ -7,7 +7,7 @@ namespace ResourceDownload { -ModModel::ModModel(BaseInstance const& base_inst, ResourceAPI* api) : ResourceModel(base_inst, api) {} +ModModel::ModModel(BaseInstance const& base_inst, ResourceAPI* api) : ResourceModel(api), m_base_instance(base_inst) {} /******** Make data requests ********/ diff --git a/launcher/ui/pages/modplatform/ModModel.h b/launcher/ui/pages/modplatform/ModModel.h index c705371a..1fac9040 100644 --- a/launcher/ui/pages/modplatform/ModModel.h +++ b/launcher/ui/pages/modplatform/ModModel.h @@ -2,6 +2,8 @@ #include +#include "BaseInstance.h" + #include "modplatform/ModIndex.h" #include "modplatform/ResourceAPI.h" @@ -38,6 +40,8 @@ class ModModel : public ResourceModel { auto documentToArray(QJsonDocument& obj) const -> QJsonArray override = 0; protected: + const BaseInstance& m_base_instance; + std::shared_ptr m_filter = nullptr; }; diff --git a/launcher/ui/pages/modplatform/ResourceModel.cpp b/launcher/ui/pages/modplatform/ResourceModel.cpp index 05d44ee2..202aa29a 100644 --- a/launcher/ui/pages/modplatform/ResourceModel.cpp +++ b/launcher/ui/pages/modplatform/ResourceModel.cpp @@ -21,7 +21,7 @@ namespace ResourceDownload { QHash ResourceModel::s_running_models; -ResourceModel::ResourceModel(BaseInstance const& base_inst, ResourceAPI* api) : QAbstractListModel(), m_base_instance(base_inst), m_api(api) +ResourceModel::ResourceModel(ResourceAPI* api) : QAbstractListModel(), m_api(api) { s_running_models.insert(this, true); } diff --git a/launcher/ui/pages/modplatform/ResourceModel.h b/launcher/ui/pages/modplatform/ResourceModel.h index d8be3b6b..02014fd6 100644 --- a/launcher/ui/pages/modplatform/ResourceModel.h +++ b/launcher/ui/pages/modplatform/ResourceModel.h @@ -5,7 +5,6 @@ #include #include "QObjectPtr.h" -#include "BaseInstance.h" #include "modplatform/ResourceAPI.h" @@ -26,7 +25,7 @@ class ResourceModel : public QAbstractListModel { Q_PROPERTY(QString search_term MEMBER m_search_term WRITE setSearchTerm) public: - ResourceModel(BaseInstance const&, ResourceAPI* api); + ResourceModel(ResourceAPI* api); ~ResourceModel() override; [[nodiscard]] auto data(const QModelIndex&, int role) const -> QVariant override; @@ -103,8 +102,6 @@ class ResourceModel : public QAbstractListModel { virtual void loadIndexedPackVersions(ModPlatform::IndexedPack&, QJsonArray&); protected: - const BaseInstance& m_base_instance; - /* Basic search parameters */ enum class SearchState { None, CanFetchMore, ResetRequested, Finished } m_search_state = SearchState::None; int m_next_search_offset = 0; From ba677a8cb76dd6cde4a08ff4b6f142f7be1bdb29 Mon Sep 17 00:00:00 2001 From: flow Date: Tue, 3 Jan 2023 13:58:27 -0300 Subject: [PATCH 101/152] refactor: change some ResourceAPI from NetJob to Task This makes it easier to create resource apis that aren't network-based. Signed-off-by: flow --- launcher/QObjectPtr.h | 4 +++ launcher/modplatform/EnsureMetadataTask.cpp | 28 ++++++++--------- launcher/modplatform/EnsureMetadataTask.h | 10 +++--- launcher/modplatform/ResourceAPI.h | 13 ++++---- launcher/modplatform/flame/FlameAPI.cpp | 6 ++-- launcher/modplatform/flame/FlameAPI.h | 6 ++-- .../flame/FlameInstanceCreationTask.cpp | 4 +-- .../flame/FlameInstanceCreationTask.h | 2 +- .../helpers/NetworkResourceAPI.cpp | 8 ++--- .../modplatform/helpers/NetworkResourceAPI.h | 8 ++--- launcher/modplatform/modrinth/ModrinthAPI.cpp | 31 ++++++++++--------- launcher/modplatform/modrinth/ModrinthAPI.h | 10 +++--- .../modrinth/ModrinthCheckUpdate.cpp | 2 +- .../modrinth/ModrinthCheckUpdate.h | 2 +- launcher/ui/pages/instance/ManagedPackPage.h | 2 ++ .../ui/pages/modplatform/ResourceModel.cpp | 2 +- launcher/ui/pages/modplatform/ResourceModel.h | 4 +-- 17 files changed, 75 insertions(+), 67 deletions(-) diff --git a/launcher/QObjectPtr.h b/launcher/QObjectPtr.h index b1ef1c8d..ec466096 100644 --- a/launcher/QObjectPtr.h +++ b/launcher/QObjectPtr.h @@ -28,6 +28,10 @@ class shared_qobject_ptr : public QSharedPointer { constexpr shared_qobject_ptr(const shared_qobject_ptr& other) : QSharedPointer(other) {} + template + constexpr shared_qobject_ptr(const QSharedPointer& other) : QSharedPointer(other) + {} + void reset() { QSharedPointer::reset(); } void reset(const shared_qobject_ptr& other) { diff --git a/launcher/modplatform/EnsureMetadataTask.cpp b/launcher/modplatform/EnsureMetadataTask.cpp index 9bf81338..fb451938 100644 --- a/launcher/modplatform/EnsureMetadataTask.cpp +++ b/launcher/modplatform/EnsureMetadataTask.cpp @@ -13,8 +13,6 @@ #include "modplatform/modrinth/ModrinthAPI.h" #include "modplatform/modrinth/ModrinthPackIndex.h" -#include "net/NetJob.h" - static ModPlatform::ProviderCapabilities ProviderCaps; static ModrinthAPI modrinth_api; @@ -107,7 +105,7 @@ void EnsureMetadataTask::executeTask() } } - NetJob::Ptr version_task; + Task::Ptr version_task; switch (m_provider) { case (ModPlatform::ResourceProvider::MODRINTH): @@ -127,7 +125,7 @@ void EnsureMetadataTask::executeTask() }; connect(version_task.get(), &Task::finished, this, [this, invalidade_leftover] { - NetJob::Ptr project_task; + Task::Ptr project_task; switch (m_provider) { case (ModPlatform::ResourceProvider::MODRINTH): @@ -149,7 +147,7 @@ void EnsureMetadataTask::executeTask() m_current_task = nullptr; }); - m_current_task = project_task.get(); + m_current_task = project_task; project_task->start(); }); @@ -164,7 +162,7 @@ void EnsureMetadataTask::executeTask() setStatus(tr("Requesting metadata information from %1 for '%2'...") .arg(ProviderCaps.readableName(m_provider), m_mods.begin().value()->name())); - m_current_task = version_task.get(); + m_current_task = version_task; version_task->start(); } @@ -210,7 +208,7 @@ void EnsureMetadataTask::emitFail(Mod* m, QString key, RemoveFromList remove) // Modrinth -NetJob::Ptr EnsureMetadataTask::modrinthVersionsTask() +Task::Ptr EnsureMetadataTask::modrinthVersionsTask() { auto hash_type = ProviderCaps.hashType(ModPlatform::ResourceProvider::MODRINTH).first(); @@ -221,7 +219,7 @@ NetJob::Ptr EnsureMetadataTask::modrinthVersionsTask() if (!ver_task) return {}; - connect(ver_task.get(), &NetJob::succeeded, this, [this, response] { + connect(ver_task.get(), &Task::succeeded, this, [this, response] { QJsonParseError parse_error{}; QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); if (parse_error.error != QJsonParseError::NoError) { @@ -260,14 +258,14 @@ NetJob::Ptr EnsureMetadataTask::modrinthVersionsTask() return ver_task; } -NetJob::Ptr EnsureMetadataTask::modrinthProjectsTask() +Task::Ptr EnsureMetadataTask::modrinthProjectsTask() { QHash addonIds; for (auto const& data : m_temp_versions) addonIds.insert(data.addonId.toString(), data.hash); auto response = new QByteArray(); - NetJob::Ptr proj_task; + Task::Ptr proj_task; if (addonIds.isEmpty()) { qWarning() << "No addonId found!"; @@ -281,7 +279,7 @@ NetJob::Ptr EnsureMetadataTask::modrinthProjectsTask() if (!proj_task) return {}; - connect(proj_task.get(), &NetJob::succeeded, this, [this, response, addonIds] { + connect(proj_task.get(), &Task::succeeded, this, [this, response, addonIds] { QJsonParseError parse_error{}; auto doc = QJsonDocument::fromJson(*response, &parse_error); if (parse_error.error != QJsonParseError::NoError) { @@ -335,7 +333,7 @@ NetJob::Ptr EnsureMetadataTask::modrinthProjectsTask() } // Flame -NetJob::Ptr EnsureMetadataTask::flameVersionsTask() +Task::Ptr EnsureMetadataTask::flameVersionsTask() { auto* response = new QByteArray(); @@ -400,7 +398,7 @@ NetJob::Ptr EnsureMetadataTask::flameVersionsTask() return ver_task; } -NetJob::Ptr EnsureMetadataTask::flameProjectsTask() +Task::Ptr EnsureMetadataTask::flameProjectsTask() { QHash addonIds; for (auto const& hash : m_mods.keys()) { @@ -414,7 +412,7 @@ NetJob::Ptr EnsureMetadataTask::flameProjectsTask() } auto response = new QByteArray(); - NetJob::Ptr proj_task; + Task::Ptr proj_task; if (addonIds.isEmpty()) { qWarning() << "No addonId found!"; @@ -428,7 +426,7 @@ NetJob::Ptr EnsureMetadataTask::flameProjectsTask() if (!proj_task) return {}; - connect(proj_task.get(), &NetJob::succeeded, this, [this, response, addonIds] { + connect(proj_task.get(), &Task::succeeded, this, [this, response, addonIds] { QJsonParseError parse_error{}; auto doc = QJsonDocument::fromJson(*response, &parse_error); if (parse_error.error != QJsonParseError::NoError) { diff --git a/launcher/modplatform/EnsureMetadataTask.h b/launcher/modplatform/EnsureMetadataTask.h index a79e5861..635f4a2b 100644 --- a/launcher/modplatform/EnsureMetadataTask.h +++ b/launcher/modplatform/EnsureMetadataTask.h @@ -28,11 +28,11 @@ class EnsureMetadataTask : public Task { private: // FIXME: Move to their own namespace - auto modrinthVersionsTask() -> NetJob::Ptr; - auto modrinthProjectsTask() -> NetJob::Ptr; + auto modrinthVersionsTask() -> Task::Ptr; + auto modrinthProjectsTask() -> Task::Ptr; - auto flameVersionsTask() -> NetJob::Ptr; - auto flameProjectsTask() -> NetJob::Ptr; + auto flameVersionsTask() -> Task::Ptr; + auto flameProjectsTask() -> Task::Ptr; // Helpers enum class RemoveFromList { @@ -61,5 +61,5 @@ class EnsureMetadataTask : public Task { QHash m_temp_versions; ConcurrentTask* m_hashing_task; - NetJob* m_current_task; + Task::Ptr m_current_task; }; diff --git a/launcher/modplatform/ResourceAPI.h b/launcher/modplatform/ResourceAPI.h index 8f794955..dfb3652c 100644 --- a/launcher/modplatform/ResourceAPI.h +++ b/launcher/modplatform/ResourceAPI.h @@ -35,6 +35,7 @@ #pragma once +#include #include #include @@ -44,7 +45,7 @@ #include "../Version.h" #include "modplatform/ModIndex.h" -#include "net/NetJob.h" +#include "tasks/Task.h" /* Simple class with a common interface for interacting with APIs */ class ResourceAPI { @@ -113,28 +114,28 @@ class ResourceAPI { [[nodiscard]] virtual auto getSortingMethods() const -> QList = 0; public slots: - [[nodiscard]] virtual NetJob::Ptr searchProjects(SearchArgs&&, SearchCallbacks&&) const + [[nodiscard]] virtual Task::Ptr searchProjects(SearchArgs&&, SearchCallbacks&&) const { qWarning() << "TODO"; return nullptr; } - [[nodiscard]] virtual NetJob::Ptr getProject(QString addonId, QByteArray* response) const + [[nodiscard]] virtual Task::Ptr getProject(QString addonId, QByteArray* response) const { qWarning() << "TODO"; return nullptr; } - [[nodiscard]] virtual NetJob::Ptr getProjects(QStringList addonIds, QByteArray* response) const + [[nodiscard]] virtual Task::Ptr getProjects(QStringList addonIds, QByteArray* response) const { qWarning() << "TODO"; return nullptr; } - [[nodiscard]] virtual NetJob::Ptr getProjectInfo(ProjectInfoArgs&&, ProjectInfoCallbacks&&) const + [[nodiscard]] virtual Task::Ptr getProjectInfo(ProjectInfoArgs&&, ProjectInfoCallbacks&&) const { qWarning() << "TODO"; return nullptr; } - [[nodiscard]] virtual NetJob::Ptr getProjectVersions(VersionSearchArgs&&, VersionSearchCallbacks&&) const + [[nodiscard]] virtual Task::Ptr getProjectVersions(VersionSearchArgs&&, VersionSearchCallbacks&&) const { qWarning() << "TODO"; return nullptr; diff --git a/launcher/modplatform/flame/FlameAPI.cpp b/launcher/modplatform/flame/FlameAPI.cpp index 32729a14..c8981585 100644 --- a/launcher/modplatform/flame/FlameAPI.cpp +++ b/launcher/modplatform/flame/FlameAPI.cpp @@ -7,7 +7,7 @@ #include "net/Upload.h" -auto FlameAPI::matchFingerprints(const QList& fingerprints, QByteArray* response) -> NetJob::Ptr +Task::Ptr FlameAPI::matchFingerprints(const QList& fingerprints, QByteArray* response) { auto* netJob = new NetJob(QString("Flame::MatchFingerprints"), APPLICATION->network()); @@ -167,7 +167,7 @@ auto FlameAPI::getLatestVersion(VersionSearchArgs&& args) -> ModPlatform::Indexe return ver; } -NetJob::Ptr FlameAPI::getProjects(QStringList addonIds, QByteArray* response) const +Task::Ptr FlameAPI::getProjects(QStringList addonIds, QByteArray* response) const { auto* netJob = new NetJob(QString("Flame::GetProjects"), APPLICATION->network()); @@ -190,7 +190,7 @@ NetJob::Ptr FlameAPI::getProjects(QStringList addonIds, QByteArray* response) co return netJob; } -NetJob::Ptr FlameAPI::getFiles(const QStringList& fileIds, QByteArray* response) const +Task::Ptr FlameAPI::getFiles(const QStringList& fileIds, QByteArray* response) const { auto* netJob = new NetJob(QString("Flame::GetFiles"), APPLICATION->network()); diff --git a/launcher/modplatform/flame/FlameAPI.h b/launcher/modplatform/flame/FlameAPI.h index 2b288564..8e7ed727 100644 --- a/launcher/modplatform/flame/FlameAPI.h +++ b/launcher/modplatform/flame/FlameAPI.h @@ -10,9 +10,9 @@ class FlameAPI : public NetworkResourceAPI { auto getLatestVersion(VersionSearchArgs&& args) -> ModPlatform::IndexedVersion; - NetJob::Ptr getProjects(QStringList addonIds, QByteArray* response) const override; - NetJob::Ptr matchFingerprints(const QList& fingerprints, QByteArray* response); - NetJob::Ptr getFiles(const QStringList& fileIds, QByteArray* response) const; + Task::Ptr getProjects(QStringList addonIds, QByteArray* response) const override; + Task::Ptr matchFingerprints(const QList& fingerprints, QByteArray* response); + Task::Ptr getFiles(const QStringList& fileIds, QByteArray* response) const; [[nodiscard]] auto getSortingMethods() const -> QList override; diff --git a/launcher/modplatform/flame/FlameInstanceCreationTask.cpp b/launcher/modplatform/flame/FlameInstanceCreationTask.cpp index fb6f78e8..890bff48 100644 --- a/launcher/modplatform/flame/FlameInstanceCreationTask.cpp +++ b/launcher/modplatform/flame/FlameInstanceCreationTask.cpp @@ -183,7 +183,7 @@ bool FlameCreationTask::updateInstance() QEventLoop loop; - connect(job.get(), &NetJob::succeeded, this, [this, raw_response, fileIds, old_inst_dir, &old_files, old_minecraft_dir] { + connect(job.get(), &Task::succeeded, this, [this, raw_response, fileIds, old_inst_dir, &old_files, old_minecraft_dir] { // Parse the API response QJsonParseError parse_error{}; auto doc = QJsonDocument::fromJson(*raw_response, &parse_error); @@ -225,7 +225,7 @@ bool FlameCreationTask::updateInstance() m_files_to_remove.append(old_minecraft_dir.absoluteFilePath(relative_path)); } }); - connect(job.get(), &NetJob::finished, &loop, &QEventLoop::quit); + connect(job.get(), &Task::finished, &loop, &QEventLoop::quit); m_process_update_file_info_job = job; job->start(); diff --git a/launcher/modplatform/flame/FlameInstanceCreationTask.h b/launcher/modplatform/flame/FlameInstanceCreationTask.h index 36b62e3e..0ae4735b 100644 --- a/launcher/modplatform/flame/FlameInstanceCreationTask.h +++ b/launcher/modplatform/flame/FlameInstanceCreationTask.h @@ -86,7 +86,7 @@ class FlameCreationTask final : public InstanceCreationTask { Flame::Manifest m_pack; // Handle to allow aborting - NetJob::Ptr m_process_update_file_info_job = nullptr; + Task::Ptr m_process_update_file_info_job = nullptr; NetJob::Ptr m_files_job = nullptr; QString m_managed_id, m_managed_version_id; diff --git a/launcher/modplatform/helpers/NetworkResourceAPI.cpp b/launcher/modplatform/helpers/NetworkResourceAPI.cpp index 77b085c0..88bbc045 100644 --- a/launcher/modplatform/helpers/NetworkResourceAPI.cpp +++ b/launcher/modplatform/helpers/NetworkResourceAPI.cpp @@ -5,7 +5,7 @@ #include "modplatform/ModIndex.h" -NetJob::Ptr NetworkResourceAPI::searchProjects(SearchArgs&& args, SearchCallbacks&& callbacks) const +Task::Ptr NetworkResourceAPI::searchProjects(SearchArgs&& args, SearchCallbacks&& callbacks) const { auto search_url_optional = getSearchURL(args); if (!search_url_optional.has_value()) { @@ -50,7 +50,7 @@ NetJob::Ptr NetworkResourceAPI::searchProjects(SearchArgs&& args, SearchCallback return netJob; } -NetJob::Ptr NetworkResourceAPI::getProjectInfo(ProjectInfoArgs&& args, ProjectInfoCallbacks&& callbacks) const +Task::Ptr NetworkResourceAPI::getProjectInfo(ProjectInfoArgs&& args, ProjectInfoCallbacks&& callbacks) const { auto response = new QByteArray(); auto job = getProject(args.pack.addonId.toString(), response); @@ -71,7 +71,7 @@ NetJob::Ptr NetworkResourceAPI::getProjectInfo(ProjectInfoArgs&& args, ProjectIn return job; } -NetJob::Ptr NetworkResourceAPI::getProjectVersions(VersionSearchArgs&& args, VersionSearchCallbacks&& callbacks) const +Task::Ptr NetworkResourceAPI::getProjectVersions(VersionSearchArgs&& args, VersionSearchCallbacks&& callbacks) const { auto versions_url_optional = getVersionsURL(args); if (!versions_url_optional.has_value()) @@ -104,7 +104,7 @@ NetJob::Ptr NetworkResourceAPI::getProjectVersions(VersionSearchArgs&& args, Ver return netJob; } -NetJob::Ptr NetworkResourceAPI::getProject(QString addonId, QByteArray* response) const +Task::Ptr NetworkResourceAPI::getProject(QString addonId, QByteArray* response) const { auto project_url_optional = getInfoURL(addonId); if (!project_url_optional.has_value()) diff --git a/launcher/modplatform/helpers/NetworkResourceAPI.h b/launcher/modplatform/helpers/NetworkResourceAPI.h index 834f274a..ab5586fd 100644 --- a/launcher/modplatform/helpers/NetworkResourceAPI.h +++ b/launcher/modplatform/helpers/NetworkResourceAPI.h @@ -4,12 +4,12 @@ class NetworkResourceAPI : public ResourceAPI { public: - NetJob::Ptr searchProjects(SearchArgs&&, SearchCallbacks&&) const override; + Task::Ptr searchProjects(SearchArgs&&, SearchCallbacks&&) const override; - NetJob::Ptr getProject(QString addonId, QByteArray* response) const override; + Task::Ptr getProject(QString addonId, QByteArray* response) const override; - NetJob::Ptr getProjectInfo(ProjectInfoArgs&&, ProjectInfoCallbacks&&) const override; - NetJob::Ptr getProjectVersions(VersionSearchArgs&&, VersionSearchCallbacks&&) const override; + Task::Ptr getProjectInfo(ProjectInfoArgs&&, ProjectInfoCallbacks&&) const override; + Task::Ptr getProjectVersions(VersionSearchArgs&&, VersionSearchCallbacks&&) const override; protected: [[nodiscard]] virtual auto getSearchURL(SearchArgs const& args) const -> std::optional = 0; diff --git a/launcher/modplatform/modrinth/ModrinthAPI.cpp b/launcher/modplatform/modrinth/ModrinthAPI.cpp index 8d7e3acf..028480a9 100644 --- a/launcher/modplatform/modrinth/ModrinthAPI.cpp +++ b/launcher/modplatform/modrinth/ModrinthAPI.cpp @@ -4,7 +4,7 @@ #include "Json.h" #include "net/Upload.h" -auto ModrinthAPI::currentVersion(QString hash, QString hash_format, QByteArray* response) -> NetJob::Ptr +Task::Ptr ModrinthAPI::currentVersion(QString hash, QString hash_format, QByteArray* response) { auto* netJob = new NetJob(QString("Modrinth::GetCurrentVersion"), APPLICATION->network()); @@ -16,7 +16,7 @@ auto ModrinthAPI::currentVersion(QString hash, QString hash_format, QByteArray* return netJob; } -auto ModrinthAPI::currentVersions(const QStringList& hashes, QString hash_format, QByteArray* response) -> NetJob::Ptr +Task::Ptr ModrinthAPI::currentVersions(const QStringList& hashes, QString hash_format, QByteArray* response) { auto* netJob = new NetJob(QString("Modrinth::GetCurrentVersions"), APPLICATION->network()); @@ -35,11 +35,11 @@ auto ModrinthAPI::currentVersions(const QStringList& hashes, QString hash_format return netJob; } -auto ModrinthAPI::latestVersion(QString hash, - QString hash_format, - std::optional> mcVersions, - std::optional loaders, - QByteArray* response) -> NetJob::Ptr +Task::Ptr ModrinthAPI::latestVersion(QString hash, + QString hash_format, + std::optional> mcVersions, + std::optional loaders, + QByteArray* response) { auto* netJob = new NetJob(QString("Modrinth::GetLatestVersion"), APPLICATION->network()); @@ -67,11 +67,11 @@ auto ModrinthAPI::latestVersion(QString hash, return netJob; } -auto ModrinthAPI::latestVersions(const QStringList& hashes, - QString hash_format, - std::optional> mcVersions, - std::optional loaders, - QByteArray* response) -> NetJob::Ptr +Task::Ptr ModrinthAPI::latestVersions(const QStringList& hashes, + QString hash_format, + std::optional> mcVersions, + std::optional loaders, + QByteArray* response) { auto* netJob = new NetJob(QString("Modrinth::GetLatestVersions"), APPLICATION->network()); @@ -101,14 +101,17 @@ auto ModrinthAPI::latestVersions(const QStringList& hashes, return netJob; } -NetJob::Ptr ModrinthAPI::getProjects(QStringList addonIds, QByteArray* response) const +Task::Ptr ModrinthAPI::getProjects(QStringList addonIds, QByteArray* response) const { auto netJob = new NetJob(QString("Modrinth::GetProjects"), APPLICATION->network()); auto searchUrl = getMultipleModInfoURL(addonIds); netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchUrl), response)); - QObject::connect(netJob, &NetJob::finished, [response, netJob] { delete response; netJob->deleteLater(); }); + QObject::connect(netJob, &NetJob::finished, [response, netJob] { + delete response; + netJob->deleteLater(); + }); return netJob; } diff --git a/launcher/modplatform/modrinth/ModrinthAPI.h b/launcher/modplatform/modrinth/ModrinthAPI.h index 949fc46e..cba3afc8 100644 --- a/launcher/modplatform/modrinth/ModrinthAPI.h +++ b/launcher/modplatform/modrinth/ModrinthAPI.h @@ -28,25 +28,25 @@ class ModrinthAPI : public NetworkResourceAPI { public: auto currentVersion(QString hash, QString hash_format, - QByteArray* response) -> NetJob::Ptr; + QByteArray* response) -> Task::Ptr; auto currentVersions(const QStringList& hashes, QString hash_format, - QByteArray* response) -> NetJob::Ptr; + QByteArray* response) -> Task::Ptr; auto latestVersion(QString hash, QString hash_format, std::optional> mcVersions, std::optional loaders, - QByteArray* response) -> NetJob::Ptr; + QByteArray* response) -> Task::Ptr; auto latestVersions(const QStringList& hashes, QString hash_format, std::optional> mcVersions, std::optional loaders, - QByteArray* response) -> NetJob::Ptr; + QByteArray* response) -> Task::Ptr; - NetJob::Ptr getProjects(QStringList addonIds, QByteArray* response) const override; + Task::Ptr getProjects(QStringList addonIds, QByteArray* response) const override; public: [[nodiscard]] auto getSortingMethods() const -> QList override; diff --git a/launcher/modplatform/modrinth/ModrinthCheckUpdate.cpp b/launcher/modplatform/modrinth/ModrinthCheckUpdate.cpp index 7826b33d..daca68d7 100644 --- a/launcher/modplatform/modrinth/ModrinthCheckUpdate.cpp +++ b/launcher/modplatform/modrinth/ModrinthCheckUpdate.cpp @@ -175,7 +175,7 @@ void ModrinthCheckUpdate::executeTask() setStatus(tr("Waiting for the API response from Modrinth...")); setProgress(1, 3); - m_net_job = job.get(); + m_net_job = qSharedPointerObjectCast(job); job->start(); lock.exec(); diff --git a/launcher/modplatform/modrinth/ModrinthCheckUpdate.h b/launcher/modplatform/modrinth/ModrinthCheckUpdate.h index 177ce516..88e1a675 100644 --- a/launcher/modplatform/modrinth/ModrinthCheckUpdate.h +++ b/launcher/modplatform/modrinth/ModrinthCheckUpdate.h @@ -19,5 +19,5 @@ class ModrinthCheckUpdate : public CheckUpdateTask { void executeTask() override; private: - NetJob* m_net_job = nullptr; + NetJob::Ptr m_net_job = nullptr; }; diff --git a/launcher/ui/pages/instance/ManagedPackPage.h b/launcher/ui/pages/instance/ManagedPackPage.h index d29a5e88..55782ba7 100644 --- a/launcher/ui/pages/instance/ManagedPackPage.h +++ b/launcher/ui/pages/instance/ManagedPackPage.h @@ -12,6 +12,8 @@ #include "modplatform/flame/FlameAPI.h" #include "modplatform/flame/FlamePackIndex.h" +#include "net/NetJob.h" + #include "ui/pages/BasePage.h" #include diff --git a/launcher/ui/pages/modplatform/ResourceModel.cpp b/launcher/ui/pages/modplatform/ResourceModel.cpp index 202aa29a..be5ead90 100644 --- a/launcher/ui/pages/modplatform/ResourceModel.cpp +++ b/launcher/ui/pages/modplatform/ResourceModel.cpp @@ -225,7 +225,7 @@ void ResourceModel::clearData() endResetModel(); } -void ResourceModel::runSearchJob(NetJob::Ptr ptr) +void ResourceModel::runSearchJob(Task::Ptr ptr) { m_current_search_job = ptr; m_current_search_job->start(); diff --git a/launcher/ui/pages/modplatform/ResourceModel.h b/launcher/ui/pages/modplatform/ResourceModel.h index 02014fd6..7e813373 100644 --- a/launcher/ui/pages/modplatform/ResourceModel.h +++ b/launcher/ui/pages/modplatform/ResourceModel.h @@ -80,7 +80,7 @@ class ResourceModel : public QAbstractListModel { /** Resets the model's data. */ void clearData(); - void runSearchJob(NetJob::Ptr); + void runSearchJob(Task::Ptr); void runInfoJob(Task::Ptr); [[nodiscard]] auto getCurrentSortingMethodByIndex() const -> std::optional; @@ -111,7 +111,7 @@ class ResourceModel : public QAbstractListModel { std::unique_ptr m_api; // Job for searching for new entries - shared_qobject_ptr m_current_search_job; + shared_qobject_ptr m_current_search_job; // Job for fetching versions and extra info on existing entries ConcurrentTask m_current_info_job; From 1919069b12b012a74ff90981a8ec70579909f2d2 Mon Sep 17 00:00:00 2001 From: flow Date: Tue, 3 Jan 2023 16:26:07 -0300 Subject: [PATCH 102/152] fix(RD): don't assert search offset on fetchMore() in ResourceModel This allows the standard QAbstractItemModelTester to work without shenanigans! Signed-off-by: flow --- launcher/ui/pages/modplatform/ResourceModel.cpp | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/launcher/ui/pages/modplatform/ResourceModel.cpp b/launcher/ui/pages/modplatform/ResourceModel.cpp index be5ead90..eb723159 100644 --- a/launcher/ui/pages/modplatform/ResourceModel.cpp +++ b/launcher/ui/pages/modplatform/ResourceModel.cpp @@ -111,11 +111,9 @@ QString ResourceModel::debugName() const void ResourceModel::fetchMore(const QModelIndex& parent) { - if (parent.isValid()) + if (parent.isValid() || m_search_state == SearchState::Finished) return; - Q_ASSERT(m_next_search_offset != 0); - search(); } From 3a168ba6dd3ea0fecce1e88a1d7538647b350c28 Mon Sep 17 00:00:00 2001 From: flow Date: Tue, 3 Jan 2023 16:27:23 -0300 Subject: [PATCH 103/152] feat(tests): add very basic ResourceModel test ______very_____ basic indeed, creating tests is super boring :c Signed-off-by: flow --- tests/CMakeLists.txt | 3 ++ tests/DummyResourceAPI.h | 47 +++++++++++++++++++ tests/ResourceModel_test.cpp | 88 ++++++++++++++++++++++++++++++++++++ 3 files changed, 138 insertions(+) create mode 100644 tests/DummyResourceAPI.h create mode 100644 tests/ResourceModel_test.cpp diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 9f84a9a7..3d0d2dca 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -24,6 +24,9 @@ ecm_add_test(ResourceFolderModel_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_V ecm_add_test(ResourcePackParse_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test TEST_NAME ResourcePackParse) +ecm_add_test(ResourceModel_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test + TEST_NAME ResourceModel) + ecm_add_test(TexturePackParse_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test TEST_NAME TexturePackParse) diff --git a/tests/DummyResourceAPI.h b/tests/DummyResourceAPI.h new file mode 100644 index 00000000..e91be96c --- /dev/null +++ b/tests/DummyResourceAPI.h @@ -0,0 +1,47 @@ +#pragma once + +#include + +#include + +class SearchTask : public Task { + Q_OBJECT + + public: + void executeTask() override { emitSucceeded(); } +}; + +class DummyResourceAPI : public ResourceAPI { + public: + static auto searchRequestResult() + { + static QByteArray json_response = + "{\"hits\":[" + "{" + "\"author\":\"flowln\"," + "\"description\":\"the bestest mod\"," + "\"project_id\":\"something\"," + "\"project_type\":\"mod\"," + "\"slug\":\"bip_bop\"," + "\"title\":\"AAAAAAAA\"," + "\"versions\":[\"2.71\"]" + "}" + "]}"; + + return QJsonDocument::fromJson(json_response); + } + + DummyResourceAPI() : ResourceAPI() {} + [[nodiscard]] auto getSortingMethods() const -> QList override { return {}; }; + + [[nodiscard]] Task::Ptr searchProjects(SearchArgs&&, SearchCallbacks&& callbacks) const override + { + auto task = new SearchTask; + QObject::connect(task, &Task::succeeded, [=] { + auto json = searchRequestResult(); + callbacks.on_succeed(json); + }); + QObject::connect(task, &Task::finished, task, &Task::deleteLater); + return task; + } +}; diff --git a/tests/ResourceModel_test.cpp b/tests/ResourceModel_test.cpp new file mode 100644 index 00000000..716bf853 --- /dev/null +++ b/tests/ResourceModel_test.cpp @@ -0,0 +1,88 @@ +#include +#include +#include + +#include + +#include + +#include "DummyResourceAPI.h" + +using ResourceDownload::ResourceModel; + +#define EXEC_TASK(EXEC) \ + QEventLoop loop; \ + \ + connect(model, &ResourceModel::dataChanged, &loop, &QEventLoop::quit); \ + \ + QTimer expire_timer; \ + expire_timer.callOnTimeout(&loop, &QEventLoop::quit); \ + expire_timer.setSingleShot(true); \ + expire_timer.start(4000); \ + \ + EXEC; \ + if (model->hasActiveSearchJob()) \ + loop.exec(); \ + \ + QVERIFY2(expire_timer.isActive(), "Timer has expired. The search never finished."); \ + expire_timer.stop(); \ + \ + disconnect(model, nullptr, &loop, nullptr) + +class ResourceModelTest; + +class DummyResourceModel : public ResourceModel { + Q_OBJECT + + friend class ResourceModelTest; + + public: + DummyResourceModel() : ResourceModel(new DummyResourceAPI) {} + + [[nodiscard]] auto metaEntryBase() const -> QString override { return ""; }; + + ResourceAPI::SearchArgs createSearchArguments() override { return {}; }; + ResourceAPI::VersionSearchArgs createVersionsArguments(QModelIndex&) override { return {}; }; + ResourceAPI::ProjectInfoArgs createInfoArguments(QModelIndex&) override { return {}; }; + + QJsonArray documentToArray(QJsonDocument& doc) const override { return doc.object().value("hits").toArray(); } + + void loadIndexedPack(ModPlatform::IndexedPack& pack, QJsonObject& obj) override + { + pack.authors.append({ Json::requireString(obj, "author") }); + pack.description = Json::requireString(obj, "description"); + pack.addonId = Json::requireString(obj, "project_id"); + } +}; + +class ResourceModelTest : public QObject { + Q_OBJECT + private slots: + void test_abstract_item_model() { [[maybe_unused]] auto tester = new QAbstractItemModelTester(new DummyResourceModel); } + + void test_search() + { + auto model = new DummyResourceModel; + + QVERIFY(model->m_packs.isEmpty()); + + EXEC_TASK(model->search()); + + QVERIFY(model->m_packs.size() == 1); + QVERIFY(model->m_search_state == DummyResourceModel::SearchState::Finished); + + auto processed_pack = model->m_packs.at(0); + auto search_json = DummyResourceAPI::searchRequestResult(); + auto processed_response = model->documentToArray(search_json).first().toObject(); + + QVERIFY(processed_pack.addonId.toString() == Json::requireString(processed_response, "project_id")); + QVERIFY(processed_pack.description == Json::requireString(processed_response, "description")); + QVERIFY(processed_pack.authors.first().name == Json::requireString(processed_response, "author")); + } +}; + +QTEST_GUILESS_MAIN(ResourceModelTest) + +#include "ResourceModel_test.moc" + +#include "moc_DummyResourceAPI.cpp" From bd36f8e220fb3019b0a9588b21ed1cbce5afbf93 Mon Sep 17 00:00:00 2001 From: flow Date: Sun, 8 Jan 2023 12:28:55 -0300 Subject: [PATCH 104/152] fix(RD): set resource strings for ReviewMessageBox too Signed-off-by: flow --- launcher/ui/dialogs/ResourceDownloadDialog.cpp | 5 +++-- launcher/ui/dialogs/ResourceDownloadDialog.h | 6 +++--- launcher/ui/dialogs/ReviewMessageBox.cpp | 8 ++++++++ launcher/ui/dialogs/ReviewMessageBox.h | 2 ++ launcher/ui/dialogs/ReviewMessageBox.ui | 14 +++++--------- 5 files changed, 21 insertions(+), 14 deletions(-) diff --git a/launcher/ui/dialogs/ResourceDownloadDialog.cpp b/launcher/ui/dialogs/ResourceDownloadDialog.cpp index fa3352b3..147373c9 100644 --- a/launcher/ui/dialogs/ResourceDownloadDialog.cpp +++ b/launcher/ui/dialogs/ResourceDownloadDialog.cpp @@ -99,7 +99,7 @@ void ResourceDownloadDialog::initializeContainer() void ResourceDownloadDialog::connectButtons() { auto OkButton = m_buttons.button(QDialogButtonBox::Ok); - OkButton->setToolTip(tr("Opens a new popup to review your selected %1 and confirm your selection. Shortcut: Ctrl+Return").arg(resourceString())); + OkButton->setToolTip(tr("Opens a new popup to review your selected %1 and confirm your selection. Shortcut: Ctrl+Return").arg(resourcesString())); connect(OkButton, &QPushButton::clicked, this, &ResourceDownloadDialog::confirm); auto CancelButton = m_buttons.button(QDialogButtonBox::Cancel); @@ -114,7 +114,8 @@ void ResourceDownloadDialog::confirm() auto keys = m_selected.keys(); keys.sort(Qt::CaseInsensitive); - auto confirm_dialog = ReviewMessageBox::create(this, tr("Confirm %1 to download").arg(resourceString())); + auto confirm_dialog = ReviewMessageBox::create(this, tr("Confirm %1 to download").arg(resourcesString())); + confirm_dialog->retranslateUi(resourcesString()); for (auto& task : keys) { confirm_dialog->appendResource({ task, m_selected.find(task).value()->getFilename() }); diff --git a/launcher/ui/dialogs/ResourceDownloadDialog.h b/launcher/ui/dialogs/ResourceDownloadDialog.h index 34120350..19843532 100644 --- a/launcher/ui/dialogs/ResourceDownloadDialog.h +++ b/launcher/ui/dialogs/ResourceDownloadDialog.h @@ -52,9 +52,9 @@ class ResourceDownloadDialog : public QDialog, public BasePageProvider { void connectButtons(); //: String that gets appended to the download dialog title ("Download " + resourcesString()) - [[nodiscard]] virtual QString resourceString() const { return tr("resources"); } + [[nodiscard]] virtual QString resourcesString() const { return tr("resources"); } - QString dialogTitle() override { return tr("Download %1").arg(resourceString()); }; + QString dialogTitle() override { return tr("Download %1").arg(resourcesString()); }; bool selectPage(QString pageId); ResourcePage* getSelectedPage(); @@ -99,7 +99,7 @@ class ModDownloadDialog final : public ResourceDownloadDialog { ~ModDownloadDialog() override = default; //: String that gets appended to the mod download dialog title ("Download " + resourcesString()) - [[nodiscard]] QString resourceString() const override { return tr("mods"); } + [[nodiscard]] QString resourcesString() const override { return tr("mods"); } [[nodiscard]] QString geometrySaveKey() const override { return "ModDownloadGeometry"; } QList getPages() override; diff --git a/launcher/ui/dialogs/ReviewMessageBox.cpp b/launcher/ui/dialogs/ReviewMessageBox.cpp index f45a9c4a..9c638d1f 100644 --- a/launcher/ui/dialogs/ReviewMessageBox.cpp +++ b/launcher/ui/dialogs/ReviewMessageBox.cpp @@ -55,3 +55,11 @@ auto ReviewMessageBox::deselectedResources() -> QStringList return list; } + +void ReviewMessageBox::retranslateUi(QString resources_name) +{ + setWindowTitle(tr("Confirm %1 selection").arg(resources_name)); + + ui->explainLabel->setText(tr("You're about to download the following %1:").arg(resources_name)); + ui->onlyCheckedLabel->setText(tr("Only %1 with a check will be downloaded!").arg(resources_name)); +} diff --git a/launcher/ui/dialogs/ReviewMessageBox.h b/launcher/ui/dialogs/ReviewMessageBox.h index e2d0ce37..7ee0d65d 100644 --- a/launcher/ui/dialogs/ReviewMessageBox.h +++ b/launcher/ui/dialogs/ReviewMessageBox.h @@ -20,6 +20,8 @@ class ReviewMessageBox : public QDialog { void appendResource(ResourceInformation&& info); auto deselectedResources() -> QStringList; + void retranslateUi(QString resources_name); + ~ReviewMessageBox() override; protected: diff --git a/launcher/ui/dialogs/ReviewMessageBox.ui b/launcher/ui/dialogs/ReviewMessageBox.ui index ab3bcc2f..bf53ae80 100644 --- a/launcher/ui/dialogs/ReviewMessageBox.ui +++ b/launcher/ui/dialogs/ReviewMessageBox.ui @@ -10,9 +10,6 @@ 350 - - Confirm mod selection - true @@ -39,22 +36,21 @@ + + + + + - - You're about to download the following mods: - - - Only mods with a check will be downloaded! - From c294c2d1df57c3d599fdea65bab9bb97b1fd699f Mon Sep 17 00:00:00 2001 From: flow Date: Sun, 8 Jan 2023 12:33:10 -0300 Subject: [PATCH 105/152] refactor(RD): allow setting custom folder target for downloaded resources Signed-off-by: flow --- launcher/ResourceDownloadTask.cpp | 12 +++++++++++- launcher/ResourceDownloadTask.h | 3 +-- launcher/modplatform/ModIndex.h | 1 + launcher/ui/dialogs/ResourceDownloadDialog.cpp | 3 ++- launcher/ui/dialogs/ReviewMessageBox.cpp | 16 ++++++++++++++++ launcher/ui/dialogs/ReviewMessageBox.h | 3 ++- 6 files changed, 33 insertions(+), 5 deletions(-) diff --git a/launcher/ResourceDownloadTask.cpp b/launcher/ResourceDownloadTask.cpp index 687eaf51..8c9dae6f 100644 --- a/launcher/ResourceDownloadTask.cpp +++ b/launcher/ResourceDownloadTask.cpp @@ -40,7 +40,17 @@ ResourceDownloadTask::ResourceDownloadTask(ModPlatform::IndexedPack pack, m_filesNetJob.reset(new NetJob(tr("Resource download"), APPLICATION->network())); m_filesNetJob->setStatus(tr("Downloading resource:\n%1").arg(m_pack_version.downloadUrl)); - m_filesNetJob->addNetAction(Net::Download::makeFile(m_pack_version.downloadUrl, m_pack_model->dir().absoluteFilePath(getFilename()))); + QDir dir { m_pack_model->dir() }; + { + // FIXME: Make this more generic. May require adding additional info to IndexedVersion, + // or adquiring a reference to the base instance. + if (!m_pack_version.custom_target_folder.isEmpty()) { + dir.cdUp(); + dir.cd(m_pack_version.custom_target_folder); + } + } + + m_filesNetJob->addNetAction(Net::Download::makeFile(m_pack_version.downloadUrl, dir.absoluteFilePath(getFilename()))); connect(m_filesNetJob.get(), &NetJob::succeeded, this, &ResourceDownloadTask::downloadSucceeded); connect(m_filesNetJob.get(), &NetJob::progress, this, &ResourceDownloadTask::downloadProgressChanged); connect(m_filesNetJob.get(), &NetJob::failed, this, &ResourceDownloadTask::downloadFailed); diff --git a/launcher/ResourceDownloadTask.h b/launcher/ResourceDownloadTask.h index 275ddbe1..5ce39d69 100644 --- a/launcher/ResourceDownloadTask.h +++ b/launcher/ResourceDownloadTask.h @@ -32,6 +32,7 @@ class ResourceDownloadTask : public SequentialTask { public: explicit ResourceDownloadTask(ModPlatform::IndexedPack pack, ModPlatform::IndexedVersion version, const std::shared_ptr packs, bool is_indexed = true); const QString& getFilename() const { return m_pack_version.fileName; } + const QString& getCustomPath() const { return m_pack_version.custom_target_folder; } const QVariant& getVersionID() const { return m_pack_version.fileId; } private: @@ -43,9 +44,7 @@ private: LocalModUpdateTask::Ptr m_update_task; void downloadProgressChanged(qint64 current, qint64 total); - void downloadFailed(QString reason); - void downloadSucceeded(); std::tuple to_delete {"", ""}; diff --git a/launcher/modplatform/ModIndex.h b/launcher/modplatform/ModIndex.h index cd40a6ba..b1f8050d 100644 --- a/launcher/modplatform/ModIndex.h +++ b/launcher/modplatform/ModIndex.h @@ -68,6 +68,7 @@ struct IndexedVersion { // For internal use, not provided by APIs bool is_currently_selected = false; + QString custom_target_folder; }; struct ExtraPackData { diff --git a/launcher/ui/dialogs/ResourceDownloadDialog.cpp b/launcher/ui/dialogs/ResourceDownloadDialog.cpp index 147373c9..b9367c16 100644 --- a/launcher/ui/dialogs/ResourceDownloadDialog.cpp +++ b/launcher/ui/dialogs/ResourceDownloadDialog.cpp @@ -118,7 +118,8 @@ void ResourceDownloadDialog::confirm() confirm_dialog->retranslateUi(resourcesString()); for (auto& task : keys) { - confirm_dialog->appendResource({ task, m_selected.find(task).value()->getFilename() }); + auto selected = m_selected.constFind(task).value(); + confirm_dialog->appendResource({ task, selected->getFilename(), selected->getCustomPath() }); } if (confirm_dialog->exec()) { diff --git a/launcher/ui/dialogs/ReviewMessageBox.cpp b/launcher/ui/dialogs/ReviewMessageBox.cpp index 9c638d1f..7b2df278 100644 --- a/launcher/ui/dialogs/ReviewMessageBox.cpp +++ b/launcher/ui/dialogs/ReviewMessageBox.cpp @@ -1,6 +1,8 @@ #include "ReviewMessageBox.h" #include "ui_ReviewMessageBox.h" +#include "Application.h" + #include ReviewMessageBox::ReviewMessageBox(QWidget* parent, QString const& title, QString const& icon) @@ -11,6 +13,10 @@ ReviewMessageBox::ReviewMessageBox(QWidget* parent, QString const& title, QStrin auto back_button = ui->buttonBox->button(QDialogButtonBox::Cancel); back_button->setText(tr("Back")); + ui->modTreeWidget->header()->setSectionResizeMode(0, QHeaderView::Stretch); + ui->modTreeWidget->header()->setStretchLastSection(false); + ui->modTreeWidget->header()->setSectionResizeMode(1, QHeaderView::ResizeToContents); + connect(ui->buttonBox, &QDialogButtonBox::accepted, this, &ReviewMessageBox::accept); connect(ui->buttonBox, &QDialogButtonBox::rejected, this, &ReviewMessageBox::reject); } @@ -36,6 +42,16 @@ void ReviewMessageBox::appendResource(ResourceInformation&& info) itemTop->insertChildren(0, { filenameItem }); + if (!info.custom_file_path.isEmpty()) { + auto customPathItem = new QTreeWidgetItem(itemTop); + customPathItem->setText(0, tr("This download will be placed in: %1").arg(info.custom_file_path)); + + itemTop->insertChildren(1, { customPathItem }); + + itemTop->setIcon(1, QIcon(APPLICATION->getThemedIcon("status-yellow"))); + itemTop->setToolTip(1, tr("This file will be downloaded to a folder location different from the default, possibly due to its loader requiring it.")); + } + ui->modTreeWidget->addTopLevelItem(itemTop); } diff --git a/launcher/ui/dialogs/ReviewMessageBox.h b/launcher/ui/dialogs/ReviewMessageBox.h index 7ee0d65d..5ec2bc23 100644 --- a/launcher/ui/dialogs/ReviewMessageBox.h +++ b/launcher/ui/dialogs/ReviewMessageBox.h @@ -12,9 +12,10 @@ class ReviewMessageBox : public QDialog { public: static auto create(QWidget* parent, QString&& title, QString&& icon = "") -> ReviewMessageBox*; - using ResourceInformation = struct { + using ResourceInformation = struct res_info { QString name; QString filename; + QString custom_file_path {}; }; void appendResource(ResourceInformation&& info); From 9407596b12df8cc45ddc53d3c08e495a2674199c Mon Sep 17 00:00:00 2001 From: flow Date: Fri, 13 Jan 2023 16:49:21 -0300 Subject: [PATCH 106/152] fix(ModUpdater): fail mods individually when there's errors in the JSON Prevents a single problematic mod from invalidating all the API response. Signed-off-by: flow --- launcher/modplatform/EnsureMetadataTask.cpp | 68 ++++++++++++--------- 1 file changed, 39 insertions(+), 29 deletions(-) diff --git a/launcher/modplatform/EnsureMetadataTask.cpp b/launcher/modplatform/EnsureMetadataTask.cpp index fb451938..d9523052 100644 --- a/launcher/modplatform/EnsureMetadataTask.cpp +++ b/launcher/modplatform/EnsureMetadataTask.cpp @@ -289,44 +289,54 @@ Task::Ptr EnsureMetadataTask::modrinthProjectsTask() return; } + QJsonArray entries; + try { - QJsonArray entries; if (addonIds.size() == 1) entries = { doc.object() }; else entries = Json::requireArray(doc); - - for (auto entry : entries) { - auto entry_obj = Json::requireObject(entry); - - ModPlatform::IndexedPack pack; - Modrinth::loadIndexedPack(pack, entry_obj); - - auto hash = addonIds.find(pack.addonId.toString()).value(); - - auto mod_iter = m_mods.find(hash); - if (mod_iter == m_mods.end()) { - qWarning() << "Invalid project id from the API response."; - continue; - } - - auto* mod = mod_iter.value(); - - try { - setStatus(tr("Parsing API response from Modrinth for '%1'...").arg(mod->name())); - - modrinthCallback(pack, m_temp_versions.find(hash).value(), mod); - } catch (Json::JsonException& e) { - qDebug() << e.cause(); - qDebug() << entries; - - emitFail(mod); - } - } } catch (Json::JsonException& e) { qDebug() << e.cause(); qDebug() << doc; } + + for (auto entry : entries) { + ModPlatform::IndexedPack pack; + + try { + auto entry_obj = Json::requireObject(entry); + + Modrinth::loadIndexedPack(pack, entry_obj); + } catch (Json::JsonException& e) { + qDebug() << e.cause(); + qDebug() << doc; + + // Skip this entry, since it has problems + continue; + } + + auto hash = addonIds.find(pack.addonId.toString()).value(); + + auto mod_iter = m_mods.find(hash); + if (mod_iter == m_mods.end()) { + qWarning() << "Invalid project id from the API response."; + continue; + } + + auto* mod = mod_iter.value(); + + try { + setStatus(tr("Parsing API response from Modrinth for '%1'...").arg(mod->name())); + + modrinthCallback(pack, m_temp_versions.find(hash).value(), mod); + } catch (Json::JsonException& e) { + qDebug() << e.cause(); + qDebug() << entries; + + emitFail(mod); + } + } }); return proj_task; From c95c81d42f63a2807889740b89be924fd0b59083 Mon Sep 17 00:00:00 2001 From: flow Date: Fri, 13 Jan 2023 16:59:37 -0300 Subject: [PATCH 107/152] fix(ModUpdater): ensure instead of require icon_url The spec says that this can be null, and indeed some mods have it set to null, and should still be considered as valid. Signed-off-by: flow --- launcher/modplatform/modrinth/ModrinthPackIndex.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/launcher/modplatform/modrinth/ModrinthPackIndex.cpp b/launcher/modplatform/modrinth/ModrinthPackIndex.cpp index f270f470..7ade131e 100644 --- a/launcher/modplatform/modrinth/ModrinthPackIndex.cpp +++ b/launcher/modplatform/modrinth/ModrinthPackIndex.cpp @@ -27,6 +27,7 @@ static ModrinthAPI api; static ModPlatform::ProviderCapabilities ProviderCaps; +// https://docs.modrinth.com/api-spec/#tag/projects/operation/getProject void Modrinth::loadIndexedPack(ModPlatform::IndexedPack& pack, QJsonObject& obj) { pack.addonId = Json::ensureString(obj, "project_id"); @@ -44,7 +45,7 @@ void Modrinth::loadIndexedPack(ModPlatform::IndexedPack& pack, QJsonObject& obj) pack.description = Json::ensureString(obj, "description", ""); - pack.logoUrl = Json::requireString(obj, "icon_url"); + pack.logoUrl = Json::ensureString(obj, "icon_url", ""); pack.logoName = pack.addonId.toString(); ModPlatform::ModpackAuthor modAuthor; From f7b0ba88da5895a48e9d5f1adda223a8fb0f4c32 Mon Sep 17 00:00:00 2001 From: Rachel Powers <508861+Ryex@users.noreply.github.com> Date: Fri, 13 Jan 2023 13:11:20 -0700 Subject: [PATCH 108/152] Apply suggestions from code review Co-authored-by: Sefa Eyeoglu Signed-off-by: Rachel Powers <508861+Ryex@users.noreply.github.com> --- launcher/Application.cpp | 3 ++- launcher/ui/MainWindow.cpp | 6 ++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/launcher/Application.cpp b/launcher/Application.cpp index 19d6d3c2..8d7ff044 100644 --- a/launcher/Application.cpp +++ b/launcher/Application.cpp @@ -266,7 +266,8 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv) m_zipsToImport.append(QUrl::fromLocalFile(QFileInfo(zip_path).absoluteFilePath())); } - for (auto zip_path : parser.positionalArguments()){ // treat unspesified positional arguments as import urls + // treat unspecified positional arguments as import urls + for (auto zip_path : parser.positionalArguments()) { m_zipsToImport.append(QUrl::fromLocalFile(QFileInfo(zip_path).absoluteFilePath())); } diff --git a/launcher/ui/MainWindow.cpp b/launcher/ui/MainWindow.cpp index d5aa4c1a..1a2b1497 100644 --- a/launcher/ui/MainWindow.cpp +++ b/launcher/ui/MainWindow.cpp @@ -1816,7 +1816,7 @@ void MainWindow::processURLs(QList urls) { // NOTE: This loop only processes one dropped file! for (auto& url : urls) { - qDebug() << "Processing :" << url; + qDebug() << "Processing" << url; // The isLocalFile() check below doesn't work as intended without an explicit scheme. if (url.scheme().isEmpty()) @@ -1832,9 +1832,7 @@ void MainWindow::processURLs(QList urls) auto type = ResourceUtils::identify(localFileInfo); - // bool is_resource = type; - - if (!(ResourceUtils::ValidResourceTypes.count(type) > 0)) { // probably instance/modpack + if (ResourceUtils::ValidResourceTypes.count(type) == 0) { // probably instance/modpack addInstance(localFileName); continue; } From ebb0596c1a09a7c14f3c8e9e2cb311e652bd34e0 Mon Sep 17 00:00:00 2001 From: flow Date: Fri, 13 Jan 2023 21:15:10 -0300 Subject: [PATCH 109/152] fix: don't fail mod parsing when encountering invalid modListVersion The spec (admitely a very old one) states that this entry should always have the value "2". However, some mods do not follow this convention, causing issues. One notable example is the 1.6 version of Aether II for 1.7.10, that has this value set at "5" for whatever reason. Signed-off-by: flow --- launcher/minecraft/mod/tasks/LocalModParseTask.cpp | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/launcher/minecraft/mod/tasks/LocalModParseTask.cpp b/launcher/minecraft/mod/tasks/LocalModParseTask.cpp index 8bfe2c84..91cb747f 100644 --- a/launcher/minecraft/mod/tasks/LocalModParseTask.cpp +++ b/launcher/minecraft/mod/tasks/LocalModParseTask.cpp @@ -17,7 +17,7 @@ namespace ModUtils { // NEW format -// https://github.com/MinecraftForge/FML/wiki/FML-mod-information-file/6f62b37cea040daf350dc253eae6326dd9c822c3 +// https://github.com/MinecraftForge/FML/wiki/FML-mod-information-file/c8d8f1929aff9979e322af79a59ce81f3e02db6a // OLD format: // https://github.com/MinecraftForge/FML/wiki/FML-mod-information-file/5bf6a2d05145ec79387acc0d45c958642fb049fc @@ -74,10 +74,11 @@ ModDetails ReadMCModInfo(QByteArray contents) version = Json::ensureString(val, "").toInt(); if (version != 2) { - qCritical() << "BAD stuff happened to mod json:"; - qCritical() << contents; - return {}; + qWarning() << QString(R"(The value of 'modListVersion' is "%1" (expected "2")! The file may be corrupted.)").arg(version); + qWarning() << "The contents of 'mcmod.info' are as follows:"; + qWarning() << contents; } + auto arrVal = jsonDoc.object().value("modlist"); if (arrVal.isUndefined()) { arrVal = jsonDoc.object().value("modList"); From 72a9b98ef065ffc065dd162124ebbd212fe0d862 Mon Sep 17 00:00:00 2001 From: RaptaG <77157639+RaptaG@users.noreply.github.com> Date: Sat, 14 Jan 2023 17:55:56 +0200 Subject: [PATCH 110/152] We're in 2023 :) Signed-off-by: RaptaG <77157639+RaptaG@users.noreply.github.com> --- COPYING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/COPYING.md b/COPYING.md index 79290654..0221d1b0 100644 --- a/COPYING.md +++ b/COPYING.md @@ -1,7 +1,7 @@ ## Prism Launcher Prism Launcher - Minecraft Launcher - Copyright (C) 2022 Prism Launcher Contributors + Copyright (C) 2022-2023 Prism Launcher Contributors 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 From 7992b7eb896f132b0e0aeebc67a491c220b9b031 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 15 Jan 2023 13:40:16 +0000 Subject: [PATCH 111/152] chore(deps): update hendrikmuhs/ccache-action action to v1.2.8 --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5d4004d0..1373815c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -145,7 +145,7 @@ jobs: - name: Setup ccache if: (runner.os != 'Windows' || matrix.msystem == '') && inputs.build_type == 'Debug' - uses: hendrikmuhs/ccache-action@v1.2.7 + uses: hendrikmuhs/ccache-action@v1.2.8 with: key: ${{ matrix.os }}-qt${{ matrix.qt_ver }}-${{ matrix.architecture }} From ad74fedfba45fe0f36ff387e586b21d4ede8ca83 Mon Sep 17 00:00:00 2001 From: flow Date: Tue, 17 Jan 2023 22:51:54 -0300 Subject: [PATCH 112/152] feat(tests): add test for stack overflow in ConcurrentTask Signed-off-by: flow --- tests/Task_test.cpp | 57 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/tests/Task_test.cpp b/tests/Task_test.cpp index 80bba02f..5d906851 100644 --- a/tests/Task_test.cpp +++ b/tests/Task_test.cpp @@ -1,4 +1,6 @@ #include +#include +#include #include #include @@ -11,6 +13,9 @@ class BasicTask : public Task { friend class TaskTest; + public: + BasicTask(bool show_debug_log = true) : Task(nullptr, show_debug_log) {} + private: void executeTask() override { @@ -30,6 +35,41 @@ class BasicTask_MultiStep : public Task { void executeTask() override {}; }; +class BigConcurrentTask : public QThread { + Q_OBJECT + + ConcurrentTask big_task; + + void run() override + { + QTimer deadline; + deadline.setInterval(10000); + connect(&deadline, &QTimer::timeout, this, [this]{ passed_the_deadline = true; }); + deadline.start(); + + static const unsigned s_num_tasks = 1 << 14; + auto sub_tasks = new BasicTask*[s_num_tasks]; + + for (unsigned i = 0; i < s_num_tasks; i++) { + sub_tasks[i] = new BasicTask(false); + big_task.addTask(sub_tasks[i]); + } + + big_task.run(); + + while (!big_task.isFinished() && !passed_the_deadline) + QCoreApplication::processEvents(); + + emit finished(); + } + + public: + bool passed_the_deadline = false; + + signals: + void finished(); +}; + class TaskTest : public QObject { Q_OBJECT @@ -183,6 +223,23 @@ class TaskTest : public QObject { return t.isFinished(); }, 1000), "Task didn't finish as it should."); } + + void test_stackOverflowInConcurrentTask() + { + QEventLoop loop; + + auto thread = new BigConcurrentTask; + thread->setStackSize(32 * 1024); + + connect(thread, &BigConcurrentTask::finished, &loop, &QEventLoop::quit); + + thread->start(); + + loop.exec(); + + QVERIFY(!thread->passed_the_deadline); + thread->deleteLater(); + } }; QTEST_GUILESS_MAIN(TaskTest) From 00d42d296e6519c92716d377496ba48c348c95b3 Mon Sep 17 00:00:00 2001 From: flow Date: Tue, 17 Jan 2023 16:08:50 -0300 Subject: [PATCH 113/152] fix: call processEvents() before adding new tasks to the task queue This allows the ongoing task to go off the stack before the next one is started. Signed-off-by: flow --- launcher/tasks/ConcurrentTask.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/launcher/tasks/ConcurrentTask.cpp b/launcher/tasks/ConcurrentTask.cpp index a890013e..190d48d8 100644 --- a/launcher/tasks/ConcurrentTask.cpp +++ b/launcher/tasks/ConcurrentTask.cpp @@ -110,14 +110,14 @@ void ConcurrentTask::startNext() setStepStatus(next->isMultiStep() ? next->getStepStatus() : next->getStatus()); updateState(); + QCoreApplication::processEvents(); + QMetaObject::invokeMethod(next.get(), &Task::start, Qt::QueuedConnection); // Allow going up the number of concurrent tasks in case of tasks being added in the middle of a running task. int num_starts = m_total_max_size - m_doing.size(); for (int i = 0; i < num_starts; i++) QMetaObject::invokeMethod(this, &ConcurrentTask::startNext, Qt::QueuedConnection); - - QCoreApplication::processEvents(); } void ConcurrentTask::subTaskSucceeded(Task::Ptr task) From cdc9f93f712081c45f661500e9e6a719eed09b6e Mon Sep 17 00:00:00 2001 From: Tayou Date: Fri, 20 Jan 2023 15:13:25 +0100 Subject: [PATCH 114/152] make MainWindow cat update instantly Signed-off-by: Tayou --- launcher/Application.h | 1 + launcher/ui/MainWindow.cpp | 5 +++++ launcher/ui/MainWindow.h | 2 ++ launcher/ui/pages/global/LauncherPage.cpp | 2 ++ 4 files changed, 10 insertions(+) diff --git a/launcher/Application.h b/launcher/Application.h index 4991f4cc..2cd077f8 100644 --- a/launcher/Application.h +++ b/launcher/Application.h @@ -208,6 +208,7 @@ signals: void updateAllowedChanged(bool status); void globalSettingsAboutToOpen(); void globalSettingsClosed(); + int currentCatChanged(int index); #ifdef Q_OS_MACOS void clickedOnDock(); diff --git a/launcher/ui/MainWindow.cpp b/launcher/ui/MainWindow.cpp index 4e830b6c..655e7df0 100644 --- a/launcher/ui/MainWindow.cpp +++ b/launcher/ui/MainWindow.cpp @@ -974,6 +974,7 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new MainWindow ui->actionCAT->setChecked(cat_enable); // NOTE: calling the operator like that is an ugly hack to appease ancient gcc... connect(ui->actionCAT.operator->(), SIGNAL(toggled(bool)), SLOT(onCatToggled(bool))); + connect(APPLICATION, &Application::currentCatChanged, this, &MainWindow::onCatChanged); setCatBackground(cat_enable); } @@ -2076,6 +2077,10 @@ void MainWindow::newsButtonClicked() news_dialog.exec(); } +void MainWindow::onCatChanged(int) { + setCatBackground(APPLICATION->settings()->get("TheCat").toBool()); +} + void MainWindow::on_actionAbout_triggered() { AboutDialog dialog(this); diff --git a/launcher/ui/MainWindow.h b/launcher/ui/MainWindow.h index 6bf5f428..84b5325a 100644 --- a/launcher/ui/MainWindow.h +++ b/launcher/ui/MainWindow.h @@ -90,6 +90,8 @@ protected: private slots: void onCatToggled(bool); + void onCatChanged(int); + void on_actionAbout_triggered(); void on_actionAddInstance_triggered(); diff --git a/launcher/ui/pages/global/LauncherPage.cpp b/launcher/ui/pages/global/LauncherPage.cpp index 69a8e3df..d8b442fd 100644 --- a/launcher/ui/pages/global/LauncherPage.cpp +++ b/launcher/ui/pages/global/LauncherPage.cpp @@ -106,6 +106,8 @@ LauncherPage::LauncherPage(QWidget *parent) : QWidget(parent), ui(new Ui::Launch } connect(ui->fontSizeBox, SIGNAL(valueChanged(int)), SLOT(refreshFontPreview())); connect(ui->consoleFont, SIGNAL(currentFontChanged(QFont)), SLOT(refreshFontPreview())); + + connect(ui->themeCustomizationWidget, &ThemeCustomizationWidget::currentCatChanged, APPLICATION, &Application::currentCatChanged); } LauncherPage::~LauncherPage() From ec1f73c827c127c1dfc2a8cc1760015336cd8845 Mon Sep 17 00:00:00 2001 From: flow Date: Fri, 20 Jan 2023 12:55:38 -0300 Subject: [PATCH 115/152] fix(tests): add some comments on the stack overflow Task test Signed-off-by: flow --- tests/Task_test.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/Task_test.cpp b/tests/Task_test.cpp index 5d906851..6649b724 100644 --- a/tests/Task_test.cpp +++ b/tests/Task_test.cpp @@ -47,6 +47,7 @@ class BigConcurrentTask : public QThread { connect(&deadline, &QTimer::timeout, this, [this]{ passed_the_deadline = true; }); deadline.start(); + // NOTE: Arbitrary value that manages to trigger a problem when there is one. static const unsigned s_num_tasks = 1 << 14; auto sub_tasks = new BasicTask*[s_num_tasks]; @@ -229,6 +230,8 @@ class TaskTest : public QObject { QEventLoop loop; auto thread = new BigConcurrentTask; + // NOTE: This is an arbitrary value, big enough to not cause problems on normal execution, but low enough + // so that the number of tasks that needs to get ran to potentially cause a problem isn't too big. thread->setStackSize(32 * 1024); connect(thread, &BigConcurrentTask::finished, &loop, &QEventLoop::quit); From 3da1d6a464b1f9ce9d058f37b9b7c8841a0f0c85 Mon Sep 17 00:00:00 2001 From: leo78913 Date: Fri, 30 Dec 2022 21:06:14 -0300 Subject: [PATCH 116/152] feat: add Widebar::InsertWidgetBefore method Signed-off-by: leo78913 --- launcher/ui/widgets/WideBar.cpp | 10 ++++++++++ launcher/ui/widgets/WideBar.h | 1 + 2 files changed, 11 insertions(+) diff --git a/launcher/ui/widgets/WideBar.cpp b/launcher/ui/widgets/WideBar.cpp index 428be563..cee2038f 100644 --- a/launcher/ui/widgets/WideBar.cpp +++ b/launcher/ui/widgets/WideBar.cpp @@ -111,6 +111,16 @@ void WideBar::insertActionAfter(QAction* after, QAction* action) m_menu_state = MenuState::Dirty; } +void WideBar::insertWidgetBefore(QAction* before, QWidget* widget) +{ + auto iter = getMatching(before); + if (iter == m_entries.end()) + return; + + BarEntry entry; + entry.bar_action = insertWidget(iter->bar_action, widget); +} + void WideBar::insertSpacer(QAction* action) { auto iter = getMatching(action); diff --git a/launcher/ui/widgets/WideBar.h b/launcher/ui/widgets/WideBar.h index a0a7896c..4004d415 100644 --- a/launcher/ui/widgets/WideBar.h +++ b/launcher/ui/widgets/WideBar.h @@ -22,6 +22,7 @@ class WideBar : public QToolBar { void insertSeparator(QAction* before); void insertActionBefore(QAction* before, QAction* action); void insertActionAfter(QAction* after, QAction* action); + void insertWidgetBefore(QAction* before, QWidget* widget); QMenu* createContextMenu(QWidget* parent = nullptr, const QString& title = QString()); void showVisibilityMenu(const QPoint&); From f3acf35aeac63e63c845368115686393b4bb09ad Mon Sep 17 00:00:00 2001 From: leo78913 Date: Fri, 30 Dec 2022 21:08:10 -0300 Subject: [PATCH 117/152] refactor: Port the main window to a .ui file some stuff still needs to be done in the c++ side because qt designer is dumb >:( the instance toolbar icon and instance name buttons are still added manually inside MainWindow.cpp looks almost identical, with some minor tweaks: - the instance toolbar is now a WideBar, so you can customize what actions you want :D - the instance toolbar buttons are now fullwidth - the close window button is now at the end of the file menu - the help menu has some layout changes this also fixes some stuff: - menus not having tooltips - the top toolbar not connecting to the title bar in kde - the instance toolbar separators looking weird after you move the toolbar Signed-off-by: leo78913 --- launcher/CMakeLists.txt | 1 + launcher/ui/MainWindow.cpp | 971 +++++--------------------------- launcher/ui/MainWindow.h | 24 +- launcher/ui/MainWindow.ui | 697 +++++++++++++++++++++++ launcher/ui/widgets/WideBar.cpp | 7 + 5 files changed, 865 insertions(+), 835 deletions(-) create mode 100644 launcher/ui/MainWindow.ui diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index 65f58667..093f44d3 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -942,6 +942,7 @@ SET(LAUNCHER_SOURCES ) qt_wrap_ui(LAUNCHER_UI + ui/MainWindow.ui ui/setupwizard/PasteWizardPage.ui ui/setupwizard/ThemeWizardPage.ui ui/pages/global/AccountListPage.ui diff --git a/launcher/ui/MainWindow.cpp b/launcher/ui/MainWindow.cpp index 655e7df0..30bbf685 100644 --- a/launcher/ui/MainWindow.cpp +++ b/launcher/ui/MainWindow.cpp @@ -43,6 +43,7 @@ #include "FileSystem.h" #include "MainWindow.h" +#include "ui_MainWindow.h" #include #include @@ -139,785 +140,96 @@ QString profileInUseFilter(const QString & profile, bool used) } } -// WHY: to hold the pre-translation strings together with the T pointer, so it can be retranslated without a lot of ugly code -template -class Translated -{ -public: - Translated(){} - Translated(QWidget *parent) - { - m_contained = new T(parent); - } - void setTooltipId(const char * tooltip) - { - m_tooltip = tooltip; - } - void setTextId(const char * text) - { - m_text = text; - } - operator T*() - { - return m_contained; - } - T * operator->() - { - return m_contained; - } - void retranslate() - { - if(m_text) - { - QString result; - result = QApplication::translate("MainWindow", m_text); - if(result.contains("%1")) { - result = result.arg(BuildConfig.LAUNCHER_DISPLAYNAME); - } - m_contained->setText(result); - } - if(m_tooltip) - { - QString result; - result = QApplication::translate("MainWindow", m_tooltip); - if(result.contains("%1")) { - result = result.arg(BuildConfig.LAUNCHER_DISPLAYNAME); - } - m_contained->setToolTip(result); - } - } -private: - T * m_contained = nullptr; - const char * m_text = nullptr; - const char * m_tooltip = nullptr; -}; -using TranslatedAction = Translated; -using TranslatedToolButton = Translated; - -class TranslatedToolbar -{ -public: - TranslatedToolbar(){} - TranslatedToolbar(QWidget *parent) - { - m_contained = new QToolBar(parent); - } - void setWindowTitleId(const char * title) - { - m_title = title; - } - operator QToolBar*() - { - return m_contained; - } - QToolBar * operator->() - { - return m_contained; - } - void retranslate() - { - if(m_title) - { - m_contained->setWindowTitle(QApplication::translate("MainWindow", m_title)); - } - } -private: - QToolBar * m_contained = nullptr; - const char * m_title = nullptr; -}; - -class MainWindow::Ui -{ -public: - TranslatedAction actionAddInstance; - //TranslatedAction actionRefresh; - TranslatedAction actionCheckUpdate; - TranslatedAction actionSettings; - TranslatedAction actionMoreNews; - TranslatedAction actionManageAccounts; - TranslatedAction actionLaunchInstance; - TranslatedAction actionKillInstance; - TranslatedAction actionRenameInstance; - TranslatedAction actionChangeInstGroup; - TranslatedAction actionChangeInstIcon; - TranslatedAction actionEditInstance; - TranslatedAction actionViewSelectedInstFolder; - TranslatedAction actionDeleteInstance; - TranslatedAction actionCAT; - TranslatedAction actionCopyInstance; - TranslatedAction actionLaunchInstanceOffline; - TranslatedAction actionLaunchInstanceDemo; - TranslatedAction actionExportInstance; - TranslatedAction actionCreateInstanceShortcut; - QVector all_actions; - - LabeledToolButton *renameButton = nullptr; - LabeledToolButton *changeIconButton = nullptr; - - QMenu * foldersMenu = nullptr; - TranslatedToolButton foldersMenuButton; - TranslatedAction actionViewInstanceFolder; - TranslatedAction actionViewCentralModsFolder; - - QMenu * editMenu = nullptr; - TranslatedAction actionUndoTrashInstance; - - QMenu * helpMenu = nullptr; - TranslatedToolButton helpMenuButton; - TranslatedAction actionClearMetadata; - #ifdef Q_OS_MAC - TranslatedAction actionAddToPATH; - #endif - TranslatedAction actionReportBug; - TranslatedAction actionDISCORD; - TranslatedAction actionMATRIX; - TranslatedAction actionREDDIT; - TranslatedAction actionAbout; - - TranslatedAction actionNoAccountsAdded; - TranslatedAction actionNoDefaultAccount; - - TranslatedAction actionLockToolbars; - - TranslatedAction actionChangeTheme; - - QVector all_toolbuttons; - - QWidget *centralWidget = nullptr; - QHBoxLayout *horizontalLayout = nullptr; - QStatusBar *statusBar = nullptr; - - QMenuBar *menuBar = nullptr; - QMenu *fileMenu; - QMenu *viewMenu; - QMenu *profileMenu; - - TranslatedAction actionCloseWindow; - - TranslatedAction actionOpenWiki; - TranslatedAction actionNewsMenuBar; - - TranslatedToolbar mainToolBar; - TranslatedToolbar instanceToolBar; - TranslatedToolbar newsToolBar; - QVector all_toolbars; - - void createMainToolbarActions(MainWindow *MainWindow) - { - actionAddInstance = TranslatedAction(MainWindow); - actionAddInstance->setObjectName(QStringLiteral("actionAddInstance")); - actionAddInstance->setIcon(APPLICATION->getThemedIcon("new")); - actionAddInstance.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Add Instanc&e...")); - actionAddInstance.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Add a new instance.")); - actionAddInstance->setShortcut(QKeySequence::New); - all_actions.append(&actionAddInstance); - - actionViewInstanceFolder = TranslatedAction(MainWindow); - actionViewInstanceFolder->setObjectName(QStringLiteral("actionViewInstanceFolder")); - actionViewInstanceFolder->setIcon(APPLICATION->getThemedIcon("viewfolder")); - actionViewInstanceFolder.setTextId(QT_TRANSLATE_NOOP("MainWindow", "&View Instance Folder")); - actionViewInstanceFolder.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Open the instance folder in a file browser.")); - all_actions.append(&actionViewInstanceFolder); - - actionViewCentralModsFolder = TranslatedAction(MainWindow); - actionViewCentralModsFolder->setObjectName(QStringLiteral("actionViewCentralModsFolder")); - actionViewCentralModsFolder->setIcon(APPLICATION->getThemedIcon("centralmods")); - actionViewCentralModsFolder.setTextId(QT_TRANSLATE_NOOP("MainWindow", "View &Central Mods Folder")); - actionViewCentralModsFolder.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Open the central mods folder in a file browser.")); - all_actions.append(&actionViewCentralModsFolder); - - foldersMenu = new QMenu(MainWindow); - foldersMenu->setTitle(tr("F&olders")); - foldersMenu->setToolTipsVisible(true); - - foldersMenu->addAction(actionViewInstanceFolder); - foldersMenu->addAction(actionViewCentralModsFolder); - - foldersMenuButton = TranslatedToolButton(MainWindow); - foldersMenuButton.setTextId(QT_TRANSLATE_NOOP("MainWindow", "F&olders")); - foldersMenuButton.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Open one of the folders shared between instances.")); - foldersMenuButton->setMenu(foldersMenu); - foldersMenuButton->setPopupMode(QToolButton::InstantPopup); - foldersMenuButton->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); - foldersMenuButton->setIcon(APPLICATION->getThemedIcon("viewfolder")); - foldersMenuButton->setFocusPolicy(Qt::NoFocus); - all_toolbuttons.append(&foldersMenuButton); - - actionSettings = TranslatedAction(MainWindow); - actionSettings->setObjectName(QStringLiteral("actionSettings")); - actionSettings->setIcon(APPLICATION->getThemedIcon("settings")); - actionSettings->setMenuRole(QAction::PreferencesRole); - actionSettings.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Setti&ngs...")); - actionSettings.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Change settings.")); - actionSettings->setShortcut(QKeySequence::Preferences); - all_actions.append(&actionSettings); - - actionUndoTrashInstance = TranslatedAction(MainWindow); - actionUndoTrashInstance->setObjectName(QStringLiteral("actionUndoTrashInstance")); - actionUndoTrashInstance.setTextId(QT_TRANSLATE_NOOP("MainWindow", "&Undo Last Instance Deletion")); - actionUndoTrashInstance->setEnabled(APPLICATION->instances()->trashedSomething()); - actionUndoTrashInstance->setShortcut(QKeySequence::Undo); - all_actions.append(&actionUndoTrashInstance); - - actionClearMetadata = TranslatedAction(MainWindow); - actionClearMetadata->setObjectName(QStringLiteral("actionClearMetadata")); - actionClearMetadata->setIcon(APPLICATION->getThemedIcon("refresh")); - actionClearMetadata.setTextId(QT_TRANSLATE_NOOP("MainWindow", "&Clear Metadata Cache")); - actionClearMetadata.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Clear cached metadata")); - all_actions.append(&actionClearMetadata); - - #ifdef Q_OS_MAC - actionAddToPATH = TranslatedAction(MainWindow); - actionAddToPATH->setObjectName(QStringLiteral("actionAddToPATH")); - actionAddToPATH.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Install to &PATH")); - actionAddToPATH.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Install a prismlauncher symlink to /usr/local/bin")); - all_actions.append(&actionAddToPATH); - #endif - - if (!BuildConfig.BUG_TRACKER_URL.isEmpty()) { - actionReportBug = TranslatedAction(MainWindow); - actionReportBug->setObjectName(QStringLiteral("actionReportBug")); - actionReportBug->setIcon(APPLICATION->getThemedIcon("bug")); - actionReportBug.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Report a &Bug...")); - actionReportBug.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Open the bug tracker to report a bug with %1.")); - all_actions.append(&actionReportBug); - } - - if(!BuildConfig.MATRIX_URL.isEmpty()) { - actionMATRIX = TranslatedAction(MainWindow); - actionMATRIX->setObjectName(QStringLiteral("actionMATRIX")); - actionMATRIX->setIcon(APPLICATION->getThemedIcon("matrix")); - actionMATRIX.setTextId(QT_TRANSLATE_NOOP("MainWindow", "&Matrix Space")); - actionMATRIX.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Open %1 Matrix space")); - all_actions.append(&actionMATRIX); - } - - if (!BuildConfig.DISCORD_URL.isEmpty()) { - actionDISCORD = TranslatedAction(MainWindow); - actionDISCORD->setObjectName(QStringLiteral("actionDISCORD")); - actionDISCORD->setIcon(APPLICATION->getThemedIcon("discord")); - actionDISCORD.setTextId(QT_TRANSLATE_NOOP("MainWindow", "&Discord Guild")); - actionDISCORD.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Open %1 Discord guild.")); - all_actions.append(&actionDISCORD); - } - - if (!BuildConfig.SUBREDDIT_URL.isEmpty()) { - actionREDDIT = TranslatedAction(MainWindow); - actionREDDIT->setObjectName(QStringLiteral("actionREDDIT")); - actionREDDIT->setIcon(APPLICATION->getThemedIcon("reddit-alien")); - actionREDDIT.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Sub&reddit")); - actionREDDIT.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Open %1 subreddit.")); - all_actions.append(&actionREDDIT); - } - - actionAbout = TranslatedAction(MainWindow); - actionAbout->setObjectName(QStringLiteral("actionAbout")); - actionAbout->setIcon(APPLICATION->getThemedIcon("about")); - actionAbout->setMenuRole(QAction::AboutRole); - actionAbout.setTextId(QT_TRANSLATE_NOOP("MainWindow", "&About %1")); - actionAbout.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "View information about %1.")); - all_actions.append(&actionAbout); - - if(BuildConfig.UPDATER_ENABLED) - { - actionCheckUpdate = TranslatedAction(MainWindow); - actionCheckUpdate->setObjectName(QStringLiteral("actionCheckUpdate")); - actionCheckUpdate->setIcon(APPLICATION->getThemedIcon("checkupdate")); - actionCheckUpdate.setTextId(QT_TRANSLATE_NOOP("MainWindow", "&Update...")); - actionCheckUpdate.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Check for new updates for %1.")); - actionCheckUpdate->setMenuRole(QAction::ApplicationSpecificRole); - all_actions.append(&actionCheckUpdate); - } - - actionCAT = TranslatedAction(MainWindow); - actionCAT->setObjectName(QStringLiteral("actionCAT")); - actionCAT->setCheckable(true); - actionCAT->setIcon(APPLICATION->getThemedIcon("cat")); - actionCAT.setTextId(QT_TRANSLATE_NOOP("MainWindow", "&Meow")); - actionCAT.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "It's a fluffy kitty :3")); - actionCAT->setPriority(QAction::LowPriority); - all_actions.append(&actionCAT); - - // profile menu and its actions - actionManageAccounts = TranslatedAction(MainWindow); - actionManageAccounts->setObjectName(QStringLiteral("actionManageAccounts")); - actionManageAccounts.setTextId(QT_TRANSLATE_NOOP("MainWindow", "&Manage Accounts...")); - // FIXME: no tooltip! - actionManageAccounts->setCheckable(false); - actionManageAccounts->setIcon(APPLICATION->getThemedIcon("accounts")); - all_actions.append(&actionManageAccounts); - - actionLockToolbars = TranslatedAction(MainWindow); - actionLockToolbars->setObjectName(QStringLiteral("actionLockToolbars")); - actionLockToolbars.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Lock Toolbars")); - actionLockToolbars->setCheckable(true); - all_actions.append(&actionLockToolbars); - - actionChangeTheme = TranslatedAction(MainWindow); - actionChangeTheme->setObjectName(QStringLiteral("actionChangeTheme")); - actionChangeTheme.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Themes")); - all_actions.append(&actionChangeTheme); - } - - void createMainToolbar(QMainWindow *MainWindow) - { - mainToolBar = TranslatedToolbar(MainWindow); - mainToolBar->setVisible(menuBar->isNativeMenuBar() || !APPLICATION->settings()->get("MenuBarInsteadOfToolBar").toBool()); - mainToolBar->setObjectName(QStringLiteral("mainToolBar")); - mainToolBar->setAllowedAreas(Qt::TopToolBarArea | Qt::BottomToolBarArea); - mainToolBar->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); - mainToolBar->setFloatable(false); - mainToolBar.setWindowTitleId(QT_TRANSLATE_NOOP("MainWindow", "Main Toolbar")); - - mainToolBar->addAction(actionAddInstance); - - mainToolBar->addSeparator(); - - QWidgetAction* foldersButtonAction = new QWidgetAction(MainWindow); - foldersButtonAction->setDefaultWidget(foldersMenuButton); - mainToolBar->addAction(foldersButtonAction); - - mainToolBar->addAction(actionSettings); - - helpMenu = new QMenu(MainWindow); - helpMenu->setToolTipsVisible(true); - - helpMenu->addAction(actionClearMetadata); - - #ifdef Q_OS_MAC - helpMenu->addAction(actionAddToPATH); - #endif - - if (!BuildConfig.BUG_TRACKER_URL.isEmpty()) { - helpMenu->addAction(actionReportBug); - } - - if(!BuildConfig.MATRIX_URL.isEmpty()) { - helpMenu->addAction(actionMATRIX); - } - - if (!BuildConfig.DISCORD_URL.isEmpty()) { - helpMenu->addAction(actionDISCORD); - } - - if (!BuildConfig.SUBREDDIT_URL.isEmpty()) { - helpMenu->addAction(actionREDDIT); - } - - helpMenu->addAction(actionAbout); - - helpMenuButton = TranslatedToolButton(MainWindow); - helpMenuButton.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Help")); - helpMenuButton.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Get help with %1 or Minecraft.")); - helpMenuButton->setMenu(helpMenu); - helpMenuButton->setPopupMode(QToolButton::InstantPopup); - helpMenuButton->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); - helpMenuButton->setIcon(APPLICATION->getThemedIcon("help")); - helpMenuButton->setFocusPolicy(Qt::NoFocus); - all_toolbuttons.append(&helpMenuButton); - QWidgetAction* helpButtonAction = new QWidgetAction(MainWindow); - helpButtonAction->setDefaultWidget(helpMenuButton); - mainToolBar->addAction(helpButtonAction); - - if(BuildConfig.UPDATER_ENABLED) - { - mainToolBar->addAction(actionCheckUpdate); - } - - mainToolBar->addSeparator(); - - mainToolBar->addAction(actionCAT); - - all_toolbars.append(&mainToolBar); - MainWindow->addToolBar(Qt::TopToolBarArea, mainToolBar); - } - - void createMenuBar(QMainWindow *MainWindow) - { - menuBar = new QMenuBar(MainWindow); - menuBar->setVisible(APPLICATION->settings()->get("MenuBarInsteadOfToolBar").toBool()); - - fileMenu = menuBar->addMenu(tr("&File")); - // Workaround for QTBUG-94802 (https://bugreports.qt.io/browse/QTBUG-94802); also present for other menus - fileMenu->setSeparatorsCollapsible(false); - fileMenu->addAction(actionAddInstance); - fileMenu->addAction(actionLaunchInstance); - fileMenu->addAction(actionKillInstance); - fileMenu->addAction(actionCloseWindow); - fileMenu->addSeparator(); - fileMenu->addAction(actionEditInstance); - fileMenu->addAction(actionChangeInstGroup); - fileMenu->addAction(actionViewSelectedInstFolder); - fileMenu->addAction(actionExportInstance); - fileMenu->addAction(actionCopyInstance); - fileMenu->addAction(actionDeleteInstance); - fileMenu->addAction(actionCreateInstanceShortcut); - fileMenu->addSeparator(); - fileMenu->addAction(actionSettings); - - editMenu = menuBar->addMenu(tr("&Edit")); - editMenu->addAction(actionUndoTrashInstance); - - viewMenu = menuBar->addMenu(tr("&View")); - viewMenu->setSeparatorsCollapsible(false); - viewMenu->addAction(actionChangeTheme); - viewMenu->addSeparator(); - viewMenu->addAction(actionCAT); - viewMenu->addSeparator(); - - viewMenu->addAction(actionLockToolbars); - - menuBar->addMenu(foldersMenu); - - profileMenu = menuBar->addMenu(tr("&Accounts")); - profileMenu->setSeparatorsCollapsible(false); - profileMenu->addAction(actionManageAccounts); - - helpMenu = menuBar->addMenu(tr("&Help")); - helpMenu->setSeparatorsCollapsible(false); - helpMenu->addAction(actionClearMetadata); - #ifdef Q_OS_MAC - helpMenu->addAction(actionAddToPATH); - #endif - helpMenu->addSeparator(); - helpMenu->addAction(actionAbout); - helpMenu->addAction(actionOpenWiki); - helpMenu->addAction(actionNewsMenuBar); - helpMenu->addSeparator(); - if (!BuildConfig.BUG_TRACKER_URL.isEmpty()) - helpMenu->addAction(actionReportBug); - if (!BuildConfig.MATRIX_URL.isEmpty()) - helpMenu->addAction(actionMATRIX); - if (!BuildConfig.DISCORD_URL.isEmpty()) - helpMenu->addAction(actionDISCORD); - if (!BuildConfig.SUBREDDIT_URL.isEmpty()) - helpMenu->addAction(actionREDDIT); - if(BuildConfig.UPDATER_ENABLED) - { - helpMenu->addSeparator(); - helpMenu->addAction(actionCheckUpdate); - } - MainWindow->setMenuBar(menuBar); - } - - void createMenuActions(MainWindow *MainWindow) - { - actionCloseWindow = TranslatedAction(MainWindow); - actionCloseWindow->setObjectName(QStringLiteral("actionCloseWindow")); - actionCloseWindow.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Close &Window")); - actionCloseWindow.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Close the current window")); - actionCloseWindow->setShortcut(QKeySequence::Close); - connect(actionCloseWindow, &QAction::triggered, APPLICATION, &Application::closeCurrentWindow); - all_actions.append(&actionCloseWindow); - - actionOpenWiki = TranslatedAction(MainWindow); - actionOpenWiki->setObjectName(QStringLiteral("actionOpenWiki")); - actionOpenWiki.setTextId(QT_TRANSLATE_NOOP("MainWindow", "%1 &Help")); - actionOpenWiki.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Open the %1 wiki")); - actionOpenWiki->setIcon(APPLICATION->getThemedIcon("help")); - connect(actionOpenWiki, &QAction::triggered, MainWindow, &MainWindow::on_actionOpenWiki_triggered); - all_actions.append(&actionOpenWiki); - - actionNewsMenuBar = TranslatedAction(MainWindow); - actionNewsMenuBar->setObjectName(QStringLiteral("actionNewsMenuBar")); - actionNewsMenuBar.setTextId(QT_TRANSLATE_NOOP("MainWindow", "%1 &News")); - actionNewsMenuBar.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Open the %1 wiki")); - actionNewsMenuBar->setIcon(APPLICATION->getThemedIcon("news")); - connect(actionNewsMenuBar, &QAction::triggered, MainWindow, &MainWindow::on_actionMoreNews_triggered); - all_actions.append(&actionNewsMenuBar); - } - - // "Instance actions" are actions that require an instance to be selected (i.e. "new instance" is not here) - // Actions that also require other conditions (e.g. a running instance) won't be changed. - void setInstanceActionsEnabled(bool enabled) - { - actionEditInstance->setEnabled(enabled); - actionChangeInstGroup->setEnabled(enabled); - actionViewSelectedInstFolder->setEnabled(enabled); - actionExportInstance->setEnabled(enabled); - actionDeleteInstance->setEnabled(enabled); - actionCopyInstance->setEnabled(enabled); - actionCreateInstanceShortcut->setEnabled(enabled); - } - - void createStatusBar(QMainWindow *MainWindow) - { - statusBar = new QStatusBar(MainWindow); - statusBar->setObjectName(QStringLiteral("statusBar")); - MainWindow->setStatusBar(statusBar); - } - - void createNewsToolbar(QMainWindow *MainWindow) - { - newsToolBar = TranslatedToolbar(MainWindow); - newsToolBar->setObjectName(QStringLiteral("newsToolBar")); - newsToolBar->setAllowedAreas(Qt::TopToolBarArea | Qt::BottomToolBarArea); - newsToolBar->setIconSize(QSize(16, 16)); - newsToolBar->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); - newsToolBar->setFloatable(false); - newsToolBar->setWindowTitle(QT_TRANSLATE_NOOP("MainWindow", "News Toolbar")); - - actionMoreNews = TranslatedAction(MainWindow); - actionMoreNews->setObjectName(QStringLiteral("actionMoreNews")); - actionMoreNews->setIcon(APPLICATION->getThemedIcon("news")); - actionMoreNews.setTextId(QT_TRANSLATE_NOOP("MainWindow", "More news...")); - actionMoreNews.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Open the development blog to read more news about %1.")); - all_actions.append(&actionMoreNews); - newsToolBar->addAction(actionMoreNews); - - all_toolbars.append(&newsToolBar); - MainWindow->addToolBar(Qt::BottomToolBarArea, newsToolBar); - } - - void createInstanceActions(QMainWindow *MainWindow) - { - // NOTE: not added to toolbar, but used for instance context menu (right click) - actionChangeInstIcon = TranslatedAction(MainWindow); - actionChangeInstIcon->setObjectName(QStringLiteral("actionChangeInstIcon")); - actionChangeInstIcon->setIcon(QIcon(":/icons/instances/grass")); - actionChangeInstIcon->setIconVisibleInMenu(true); - actionChangeInstIcon.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Change Icon")); - actionChangeInstIcon.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Change the selected instance's icon.")); - all_actions.append(&actionChangeInstIcon); - - changeIconButton = new LabeledToolButton(MainWindow); - changeIconButton->setObjectName(QStringLiteral("changeIconButton")); - changeIconButton->setIcon(APPLICATION->getThemedIcon("news")); - changeIconButton->setToolTip(actionChangeInstIcon->toolTip()); - changeIconButton->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); - - // NOTE: not added to toolbar, but used for instance context menu (right click) - actionRenameInstance = TranslatedAction(MainWindow); - actionRenameInstance->setObjectName(QStringLiteral("actionRenameInstance")); - actionRenameInstance.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Rename")); - actionRenameInstance.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Rename the selected instance.")); - actionRenameInstance->setIcon(APPLICATION->getThemedIcon("rename")); - all_actions.append(&actionRenameInstance); - - // the rename label is inside the rename tool button - renameButton = new LabeledToolButton(MainWindow); - renameButton->setObjectName(QStringLiteral("renameButton")); - renameButton->setToolTip(actionRenameInstance->toolTip()); - renameButton->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); - - actionLaunchInstance = TranslatedAction(MainWindow); - actionLaunchInstance->setObjectName(QStringLiteral("actionLaunchInstance")); - actionLaunchInstance.setTextId(QT_TRANSLATE_NOOP("MainWindow", "&Launch")); - actionLaunchInstance.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Launch the selected instance.")); - actionLaunchInstance->setIcon(APPLICATION->getThemedIcon("launch")); - all_actions.append(&actionLaunchInstance); - - actionLaunchInstanceOffline = TranslatedAction(MainWindow); - actionLaunchInstanceOffline->setObjectName(QStringLiteral("actionLaunchInstanceOffline")); - actionLaunchInstanceOffline.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Launch &Offline")); - actionLaunchInstanceOffline.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Launch the selected instance in offline mode.")); - all_actions.append(&actionLaunchInstanceOffline); - - actionLaunchInstanceDemo = TranslatedAction(MainWindow); - actionLaunchInstanceDemo->setObjectName(QStringLiteral("actionLaunchInstanceDemo")); - actionLaunchInstanceDemo.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Launch &Demo")); - actionLaunchInstanceDemo.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Launch the selected instance in demo mode.")); - all_actions.append(&actionLaunchInstanceDemo); - - actionKillInstance = TranslatedAction(MainWindow); - actionKillInstance->setObjectName(QStringLiteral("actionKillInstance")); - actionKillInstance->setDisabled(true); - actionKillInstance.setTextId(QT_TRANSLATE_NOOP("MainWindow", "&Kill")); - actionKillInstance.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Kill the running instance")); - actionKillInstance->setShortcut(QKeySequence(tr("Ctrl+K"))); - actionKillInstance->setIcon(APPLICATION->getThemedIcon("status-bad")); - all_actions.append(&actionKillInstance); - - actionEditInstance = TranslatedAction(MainWindow); - actionEditInstance->setObjectName(QStringLiteral("actionEditInstance")); - actionEditInstance.setTextId(QT_TRANSLATE_NOOP("MainWindow", "&Edit...")); - actionEditInstance.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Change the instance settings, mods and versions.")); - actionEditInstance->setShortcut(QKeySequence(tr("Ctrl+I"))); - actionEditInstance->setIcon(APPLICATION->getThemedIcon("settings-configure")); - all_actions.append(&actionEditInstance); - - actionChangeInstGroup = TranslatedAction(MainWindow); - actionChangeInstGroup->setObjectName(QStringLiteral("actionChangeInstGroup")); - actionChangeInstGroup.setTextId(QT_TRANSLATE_NOOP("MainWindow", "&Change Group...")); - actionChangeInstGroup.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Change the selected instance's group.")); - actionChangeInstGroup->setShortcut(QKeySequence(tr("Ctrl+G"))); - actionChangeInstGroup->setIcon(APPLICATION->getThemedIcon("tag")); - all_actions.append(&actionChangeInstGroup); - - actionViewSelectedInstFolder = TranslatedAction(MainWindow); - actionViewSelectedInstFolder->setObjectName(QStringLiteral("actionViewSelectedInstFolder")); - actionViewSelectedInstFolder.setTextId(QT_TRANSLATE_NOOP("MainWindow", "&Folder")); - actionViewSelectedInstFolder.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Open the selected instance's root folder in a file browser.")); - actionViewSelectedInstFolder->setIcon(APPLICATION->getThemedIcon("viewfolder")); - all_actions.append(&actionViewSelectedInstFolder); - - actionExportInstance = TranslatedAction(MainWindow); - actionExportInstance->setObjectName(QStringLiteral("actionExportInstance")); - actionExportInstance.setTextId(QT_TRANSLATE_NOOP("MainWindow", "E&xport...")); - actionExportInstance.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Export the selected instance as a zip file.")); - actionExportInstance->setShortcut(QKeySequence(tr("Ctrl+E"))); - actionExportInstance->setIcon(APPLICATION->getThemedIcon("export")); - all_actions.append(&actionExportInstance); - - actionDeleteInstance = TranslatedAction(MainWindow); - actionDeleteInstance->setObjectName(QStringLiteral("actionDeleteInstance")); - actionDeleteInstance.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Dele&te")); - actionDeleteInstance.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Delete the selected instance.")); - actionDeleteInstance->setShortcuts({QKeySequence(tr("Backspace")), QKeySequence::Delete}); - actionDeleteInstance->setAutoRepeat(false); - actionDeleteInstance->setIcon(APPLICATION->getThemedIcon("delete")); - all_actions.append(&actionDeleteInstance); - - actionCopyInstance = TranslatedAction(MainWindow); - actionCopyInstance->setObjectName(QStringLiteral("actionCopyInstance")); - actionCopyInstance.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Cop&y...")); - actionCopyInstance.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Copy the selected instance.")); - actionCopyInstance->setShortcut(QKeySequence(tr("Ctrl+D"))); - actionCopyInstance->setIcon(APPLICATION->getThemedIcon("copy")); - all_actions.append(&actionCopyInstance); - - actionCreateInstanceShortcut = TranslatedAction(MainWindow); - actionCreateInstanceShortcut->setObjectName(QStringLiteral("actionCreateInstanceShortcut")); - actionCreateInstanceShortcut.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Create Shortcut")); - actionCreateInstanceShortcut.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Creates a shortcut on your desktop to launch the selected instance.")); - actionCreateInstanceShortcut->setIcon(APPLICATION->getThemedIcon("shortcut")); - all_actions.append(&actionCreateInstanceShortcut); - - setInstanceActionsEnabled(false); - } - - void createInstanceToolbar(QMainWindow *MainWindow) - { - instanceToolBar = TranslatedToolbar(MainWindow); - instanceToolBar->setObjectName(QStringLiteral("instanceToolBar")); - // disabled until we have an instance selected - instanceToolBar->setEnabled(false); - // Qt doesn't like vertical moving toolbars, so we have to force them... - // See https://github.com/PolyMC/PolyMC/issues/493 - connect(instanceToolBar, &QToolBar::orientationChanged, [=](Qt::Orientation){ instanceToolBar->setOrientation(Qt::Vertical); }); - instanceToolBar->setAllowedAreas(Qt::LeftToolBarArea | Qt::RightToolBarArea); - instanceToolBar->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); - instanceToolBar->setIconSize(QSize(16, 16)); - - instanceToolBar->setFloatable(false); - instanceToolBar->setWindowTitle(QT_TRANSLATE_NOOP("MainWindow", "Instance Toolbar")); - - instanceToolBar->addWidget(changeIconButton); - instanceToolBar->addWidget(renameButton); - - instanceToolBar->addSeparator(); - - instanceToolBar->addAction(actionLaunchInstance); - instanceToolBar->addAction(actionKillInstance); - - instanceToolBar->addSeparator(); - - instanceToolBar->addAction(actionEditInstance); - instanceToolBar->addAction(actionChangeInstGroup); - - instanceToolBar->addAction(actionViewSelectedInstFolder); - - instanceToolBar->addAction(actionExportInstance); - instanceToolBar->addAction(actionCopyInstance); - instanceToolBar->addAction(actionDeleteInstance); - - instanceToolBar->addAction(actionCreateInstanceShortcut); // TODO find better position for this - - QLayout * lay = instanceToolBar->layout(); - for(int i = 0; i < lay->count(); i++) - { - QLayoutItem * item = lay->itemAt(i); - if (item->widget()->metaObject()->className() == QString("QToolButton")) - { - item->setAlignment(Qt::AlignLeft); - } - } - - all_toolbars.append(&instanceToolBar); - MainWindow->addToolBar(Qt::RightToolBarArea, instanceToolBar); - } - - void setupUi(MainWindow *MainWindow) - { - if (MainWindow->objectName().isEmpty()) - { - MainWindow->setObjectName(QStringLiteral("MainWindow")); - } - MainWindow->resize(800, 600); - MainWindow->setWindowIcon(APPLICATION->getThemedIcon("logo")); - MainWindow->setWindowTitle(APPLICATION->applicationDisplayName()); -#ifndef QT_NO_ACCESSIBILITY - MainWindow->setAccessibleName(BuildConfig.LAUNCHER_DISPLAYNAME); -#endif - - createMainToolbarActions(MainWindow); - createMenuActions(MainWindow); - createInstanceActions(MainWindow); - - createMenuBar(MainWindow); - - createMainToolbar(MainWindow); - - centralWidget = new QWidget(MainWindow); - centralWidget->setObjectName(QStringLiteral("centralWidget")); - horizontalLayout = new QHBoxLayout(centralWidget); - horizontalLayout->setSpacing(0); - horizontalLayout->setObjectName(QStringLiteral("horizontalLayout")); - horizontalLayout->setSizeConstraint(QLayout::SetDefaultConstraint); - horizontalLayout->setContentsMargins(0, 0, 0, 0); - MainWindow->setCentralWidget(centralWidget); - - createStatusBar(MainWindow); - createNewsToolbar(MainWindow); - createInstanceToolbar(MainWindow); - - MainWindow->updateToolsMenu(); - MainWindow->updateThemeMenu(); - - retranslateUi(MainWindow); - - QMetaObject::connectSlotsByName(MainWindow); - } // setupUi - - void retranslateUi(MainWindow *MainWindow) - { - // all the actions - for(auto * item: all_actions) - { - item->retranslate(); - } - for(auto * item: all_toolbars) - { - item->retranslate(); - } - for(auto * item: all_toolbuttons) - { - item->retranslate(); - } - // submenu buttons - foldersMenuButton->setText(tr("Folders")); - helpMenuButton->setText(tr("Help")); - - // playtime counter - if (MainWindow->m_statusCenter) - { - MainWindow->updateStatusCenter(); - } - } // retranslateUi -}; - -MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new MainWindow::Ui) +MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWindow) { ui->setupUi(this); + // instance toolbar stuff + { + // Qt doesn't like vertical moving toolbars, so we have to force them... + // See https://github.com/PolyMC/PolyMC/issues/493 + connect(ui->instanceToolBar, &QToolBar::orientationChanged, + [=](Qt::Orientation) { ui->instanceToolBar->setOrientation(Qt::Vertical); }); + + // if you try to add a widget to a toolbar in a .ui file + // qt designer will delete it when you save the file >:( + changeIconButton = new LabeledToolButton(this); + changeIconButton->setObjectName(QStringLiteral("changeIconButton")); + changeIconButton->setIcon(APPLICATION->getThemedIcon("news")); + changeIconButton->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); + connect(changeIconButton, &QToolButton::clicked, this, &MainWindow::on_actionChangeInstIcon_triggered); + ui->instanceToolBar->insertWidgetBefore(ui->actionLaunchInstance, changeIconButton); + + renameButton = new LabeledToolButton(this); + renameButton->setObjectName(QStringLiteral("renameButton")); + renameButton->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); + connect(renameButton, &QToolButton::clicked, this, &MainWindow::on_actionRenameInstance_triggered); + ui->instanceToolBar->insertWidgetBefore(ui->actionLaunchInstance, renameButton); + + // restore the instance toolbar settings + auto const setting_name = QString("WideBarVisibility_%1").arg(ui->instanceToolBar->objectName()); + if (!APPLICATION->settings()->contains(setting_name)) + instanceToolbarSetting = APPLICATION->settings()->registerSetting(setting_name); + else + instanceToolbarSetting = APPLICATION->settings()->getSetting(setting_name); + + ui->instanceToolBar->setVisibilityState(instanceToolbarSetting->get().toByteArray()); + } + + // set the menu for the folders and help tool buttons + { + auto foldersMenuButton = dynamic_cast(ui->mainToolBar->widgetForAction(ui->actionFoldersButton)); + foldersMenuButton->setMenu(ui->foldersMenu); + foldersMenuButton->setPopupMode(QToolButton::InstantPopup); + + helpMenuButton = dynamic_cast(ui->mainToolBar->widgetForAction(ui->actionHelpButton)); + helpMenuButton->setMenu(ui->helpMenu); + helpMenuButton->setPopupMode(QToolButton::InstantPopup); + } + + // hide, disable and show stuff + { + ui->actionReportBug->setVisible(!BuildConfig.BUG_TRACKER_URL.isEmpty()); + ui->actionMATRIX->setVisible(!BuildConfig.MATRIX_URL.isEmpty()); + ui->actionDISCORD->setVisible(!BuildConfig.DISCORD_URL.isEmpty()); + ui->actionREDDIT->setVisible(!BuildConfig.SUBREDDIT_URL.isEmpty()); + + ui->actionCheckUpdate->setVisible(BuildConfig.UPDATER_ENABLED); + + ui->actionAddToPATH->setVisible(false); +#ifdef Q_OS_MAC + ui->actionAddToPATH->setVisible(true); +#endif + + // disabled until we have an instance selected + ui->instanceToolBar->setEnabled(false); + ui->actionKillInstance->setEnabled(false); + ui->actionLaunchInstance->setEnabled(false); + setInstanceActionsEnabled(false); + } + + // add the toolbar toggles to the view menu + ui->viewMenu->addAction(ui->instanceToolBar->toggleViewAction()); + ui->viewMenu->addAction(ui->newsToolBar->toggleViewAction()); + + updateThemeMenu(); + updateMainToolBar(); // OSX magic. setUnifiedTitleAndToolBarOnMac(true); // Global shortcuts { + // you can't set QKeySequence::StandardKey shortcuts in qt designer >:( + ui->actionAddInstance->setShortcut(QKeySequence::New); + ui->actionSettings->setShortcut(QKeySequence::Preferences); + ui->actionUndoTrashInstance->setShortcut(QKeySequence::Undo); + ui->actionDeleteInstance->setShortcuts({ QKeySequence(tr("Backspace")), QKeySequence::Delete }); + ui->actionCloseWindow->setShortcut(QKeySequence::Close); + connect(ui->actionCloseWindow, &QAction::triggered, APPLICATION, &Application::closeCurrentWindow); + // FIXME: This is kinda weird. and bad. We need some kind of managed shutdown. auto q = new QShortcut(QKeySequence::Quit, this); - connect(q, SIGNAL(activated()), qApp, SLOT(quit())); + connect(q, &QShortcut::activated, APPLICATION, &Application::quit); } // Konami Code @@ -929,12 +241,13 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new MainWindow // Add the news label to the news toolbar. { m_newsChecker.reset(new NewsChecker(APPLICATION->network(), BuildConfig.NEWS_RSS_URL)); - newsLabel = new QToolButton(); - newsLabel->setIcon(APPLICATION->getThemedIcon("news")); - newsLabel->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); - newsLabel->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); - newsLabel->setFocusPolicy(Qt::NoFocus); - ui->newsToolBar->insertWidget(ui->actionMoreNews, newsLabel); + newsLabel = dynamic_cast(ui->newsToolBar->widgetForAction(ui->actionNewsLabel)); + + //add a spacer before the more news button + QWidget *spacer = new QWidget(); + spacer->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); + ui->newsToolBar->insertWidget(ui->actionMoreNews, spacer); + QObject::connect(newsLabel, &QAbstractButton::clicked, this, &MainWindow::newsButtonClicked); QObject::connect(m_newsChecker.get(), &NewsChecker::newsLoaded, this, &MainWindow::updateNewsLabel); updateNewsLabel(); @@ -970,10 +283,11 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new MainWindow } // The cat background { + // set the cat action priority here so you can still see the action in qt designer + ui->actionCAT->setPriority(QAction::LowPriority); bool cat_enable = APPLICATION->settings()->get("TheCat").toBool(); ui->actionCAT->setChecked(cat_enable); - // NOTE: calling the operator like that is an ugly hack to appease ancient gcc... - connect(ui->actionCAT.operator->(), SIGNAL(toggled(bool)), SLOT(onCatToggled(bool))); + connect(ui->actionCAT, &QAction::toggled, this, &MainWindow::onCatToggled); connect(APPLICATION, &Application::currentCatChanged, this, &MainWindow::onCatChanged); setCatBackground(cat_enable); } @@ -1011,7 +325,7 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new MainWindow // Add "manage accounts" button, right align QWidget *spacer = new QWidget(); spacer->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); - ui->mainToolBar->addWidget(spacer); + ui->mainToolBar->insertWidget(ui->actionAccountsButton, spacer); accountMenu = new QMenu(this); // Use undocumented property... https://stackoverflow.com/questions/7121718/create-a-scrollbar-in-a-submenu-qt @@ -1019,16 +333,9 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new MainWindow repopulateAccountsMenu(); - accountMenuButton = new QToolButton(this); + accountMenuButton = dynamic_cast(ui->mainToolBar->widgetForAction(ui->actionAccountsButton)); accountMenuButton->setMenu(accountMenu); accountMenuButton->setPopupMode(QToolButton::InstantPopup); - accountMenuButton->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); - accountMenuButton->setIcon(APPLICATION->getThemedIcon("noaccount")); - - QWidgetAction *accountMenuButtonAction = new QWidgetAction(this); - accountMenuButtonAction->setDefaultWidget(accountMenuButton); - - ui->mainToolBar->addAction(accountMenuButtonAction); // Update the menu when the active account changes. // Shouldn't have to use lambdas here like this, but if I don't, the compiler throws a fit. @@ -1067,8 +374,7 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new MainWindow bool updatesAllowed = APPLICATION->updatesAreAllowed(); updatesAllowedChanged(updatesAllowed); - // NOTE: calling the operator like that is an ugly hack to appease ancient gcc... - connect(ui->actionCheckUpdate.operator->(), &QAction::triggered, this, &MainWindow::checkForUpdates); + connect(ui->actionCheckUpdate, &QAction::triggered, this, &MainWindow::checkForUpdates); // set up the updater object. auto updater = APPLICATION->updateChecker(); @@ -1089,7 +395,7 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new MainWindow } } - connect(ui->actionUndoTrashInstance.operator->(), &QAction::triggered, this, &MainWindow::undoTrashInstance); + connect(ui->actionUndoTrashInstance, &QAction::triggered, this, &MainWindow::undoTrashInstance); setSelectedInstanceById(APPLICATION->settings()->get("SelectedInstance").toString()); @@ -1130,6 +436,20 @@ void MainWindow::retranslateUi() } ui->retranslateUi(this); + + changeIconButton->setToolTip(ui->actionChangeInstIcon->toolTip()); + renameButton->setToolTip(ui->actionRenameInstance->toolTip()); + + // replace the %1 with the launcher display name in some actions + if (helpMenuButton->toolTip().contains("%1")) + helpMenuButton->setToolTip(helpMenuButton->toolTip().arg(BuildConfig.LAUNCHER_DISPLAYNAME)); + + for (auto action : ui->helpMenu->actions()) { + if (action->text().contains("%1")) + action->setText(action->text().arg(BuildConfig.LAUNCHER_DISPLAYNAME)); + if (action->toolTip().contains("%1")) + action->setToolTip(action->toolTip().arg(BuildConfig.LAUNCHER_DISPLAYNAME)); + } } MainWindow::~MainWindow() @@ -1169,13 +489,16 @@ void MainWindow::showInstanceContextMenu(const QPoint &pos) bool onInstance = view->indexAt(pos).isValid(); if (onInstance) { - actions = ui->instanceToolBar->actions(); + // reuse the file menu actions + actions = ui->fileMenu->actions(); - // replace the change icon widget with an actual action - actions.replace(0, ui->actionChangeInstIcon); + // remove the add instance action, launcher settings action and close action + actions.removeFirst(); + actions.removeLast(); + actions.removeLast(); - // replace the rename widget with an actual action - actions.replace(1, ui->actionRenameInstance); + actions.prepend(ui->actionChangeInstIcon); + actions.prepend(ui->actionRenameInstance); // add header actions.prepend(actionSep); @@ -1231,8 +554,6 @@ void MainWindow::updateMainToolBar() void MainWindow::updateToolsMenu() { - QToolButton *launchButton = dynamic_cast(ui->instanceToolBar->widgetForAction(ui->actionLaunchInstance)); - bool currentInstanceRunning = m_selectedInstance && m_selectedInstance->isRunning(); ui->actionLaunchInstance->setDisabled(!m_selectedInstance || currentInstanceRunning); @@ -1240,7 +561,6 @@ void MainWindow::updateToolsMenu() ui->actionLaunchInstanceDemo->setDisabled(!m_selectedInstance || currentInstanceRunning); QMenu *launchMenu = ui->actionLaunchInstance->menu(); - launchButton->setPopupMode(QToolButton::MenuButtonPopup); if (launchMenu) { launchMenu->clear(); @@ -1249,7 +569,6 @@ void MainWindow::updateToolsMenu() { launchMenu = new QMenu(this); } - QAction *normalLaunch = launchMenu->addAction(tr("Launch")); normalLaunch->setShortcut(QKeySequence::Open); QAction *normalLaunchOffline = launchMenu->addAction(tr("Launch Offline")); @@ -1358,7 +677,7 @@ void MainWindow::updateThemeMenu() void MainWindow::repopulateAccountsMenu() { accountMenu->clear(); - ui->profileMenu->clear(); + ui->accountsMenu->clear(); auto accounts = APPLICATION->accounts(); MinecraftAccountPtr defaultAccount = accounts->defaultAccount(); @@ -1376,14 +695,10 @@ void MainWindow::repopulateAccountsMenu() if (accounts->count() <= 0) { - ui->all_actions.removeAll(&ui->actionNoAccountsAdded); - ui->actionNoAccountsAdded = TranslatedAction(this); - ui->actionNoAccountsAdded->setObjectName(QStringLiteral("actionNoAccountsAdded")); - ui->actionNoAccountsAdded.setTextId(QT_TRANSLATE_NOOP("MainWindow", "No accounts added!")); + ui->actionNoAccountsAdded->setText( "No accounts added!"); ui->actionNoAccountsAdded->setEnabled(false); accountMenu->addAction(ui->actionNoAccountsAdded); - ui->profileMenu->addAction(ui->actionNoAccountsAdded); - ui->all_actions.append(&ui->actionNoAccountsAdded); + ui->accountsMenu->addAction(ui->actionNoAccountsAdded); } else { @@ -1415,18 +730,17 @@ void MainWindow::repopulateAccountsMenu() } accountMenu->addAction(action); - ui->profileMenu->addAction(action); + ui->accountsMenu->addAction(action); connect(action, SIGNAL(triggered(bool)), SLOT(changeActiveAccount())); } } accountMenu->addSeparator(); - ui->profileMenu->addSeparator(); + ui->accountsMenu->addSeparator(); - ui->all_actions.removeAll(&ui->actionNoDefaultAccount); - ui->actionNoDefaultAccount = TranslatedAction(this); + ui->actionNoDefaultAccount = new QAction(this); ui->actionNoDefaultAccount->setObjectName(QStringLiteral("actionNoDefaultAccount")); - ui->actionNoDefaultAccount.setTextId(QT_TRANSLATE_NOOP("MainWindow", "No Default Account")); + ui->actionNoDefaultAccount->setText("No Default Account"); ui->actionNoDefaultAccount->setCheckable(true); ui->actionNoDefaultAccount->setIcon(APPLICATION->getThemedIcon("noaccount")); ui->actionNoDefaultAccount->setData(-1); @@ -1436,15 +750,13 @@ void MainWindow::repopulateAccountsMenu() } accountMenu->addAction(ui->actionNoDefaultAccount); - ui->profileMenu->addAction(ui->actionNoDefaultAccount); + ui->accountsMenu->addAction(ui->actionNoDefaultAccount); connect(ui->actionNoDefaultAccount, SIGNAL(triggered(bool)), SLOT(changeActiveAccount())); - ui->all_actions.append(&ui->actionNoDefaultAccount); - ui->actionNoDefaultAccount.retranslate(); accountMenu->addSeparator(); - ui->profileMenu->addSeparator(); + ui->accountsMenu->addSeparator(); accountMenu->addAction(ui->actionManageAccounts); - ui->profileMenu->addAction(ui->actionManageAccounts); + ui->accountsMenu->addAction(ui->actionManageAccounts); } void MainWindow::updatesAllowedChanged(bool allowed) @@ -1878,7 +1190,7 @@ void MainWindow::on_actionChangeInstIcon_triggered() m_selectedInstance->setIconKey(dlg.selectedIconKey); auto icon = APPLICATION->icons()->getIcon(dlg.selectedIconKey); ui->actionChangeInstIcon->setIcon(icon); - ui->changeIconButton->setIcon(icon); + changeIconButton->setIcon(icon); } } @@ -1888,7 +1200,7 @@ void MainWindow::iconUpdated(QString icon) { auto icon = APPLICATION->icons()->getIcon(m_currentInstIcon); ui->actionChangeInstIcon->setIcon(icon); - ui->changeIconButton->setIcon(icon); + changeIconButton->setIcon(icon); } } @@ -1897,7 +1209,7 @@ void MainWindow::updateInstanceToolIcon(QString new_icon) m_currentInstIcon = new_icon; auto icon = APPLICATION->icons()->getIcon(m_currentInstIcon); ui->actionChangeInstIcon->setIcon(icon); - ui->changeIconButton->setIcon(icon); + changeIconButton->setIcon(icon); } void MainWindow::setSelectedInstanceById(const QString &id) @@ -2145,6 +1457,7 @@ void MainWindow::closeEvent(QCloseEvent *event) // Save the window state and geometry. APPLICATION->settings()->set("MainWindowState", saveState().toBase64()); APPLICATION->settings()->set("MainWindowGeometry", saveGeometry().toBase64()); + instanceToolbarSetting->set(ui->instanceToolBar->getVisibilityState()); event->accept(); emit isClosing(); } @@ -2378,7 +1691,7 @@ void MainWindow::instanceChanged(const QModelIndex ¤t, const QModelIndex & if (m_selectedInstance) { ui->instanceToolBar->setEnabled(true); - ui->setInstanceActionsEnabled(true); + setInstanceActionsEnabled(true); ui->actionLaunchInstance->setEnabled(m_selectedInstance->canLaunch()); ui->actionLaunchInstanceOffline->setEnabled(m_selectedInstance->canLaunch()); ui->actionLaunchInstanceDemo->setEnabled(m_selectedInstance->canLaunch()); @@ -2391,7 +1704,7 @@ void MainWindow::instanceChanged(const QModelIndex ¤t, const QModelIndex & ui->actionKillInstance->setEnabled(m_selectedInstance->isRunning()); ui->actionExportInstance->setEnabled(m_selectedInstance->canExport()); - ui->renameButton->setText(m_selectedInstance->name()); + renameButton->setText(m_selectedInstance->name()); m_statusLeft->setText(m_selectedInstance->getStatusbarDescription()); updateStatusCenter(); updateInstanceToolIcon(m_selectedInstance->iconKey()); @@ -2405,7 +1718,7 @@ void MainWindow::instanceChanged(const QModelIndex ¤t, const QModelIndex & else { ui->instanceToolBar->setEnabled(false); - ui->setInstanceActionsEnabled(false); + setInstanceActionsEnabled(false); ui->actionLaunchInstance->setEnabled(false); ui->actionLaunchInstanceOffline->setEnabled(false); ui->actionLaunchInstanceDemo->setEnabled(false); @@ -2438,9 +1751,9 @@ void MainWindow::selectionBad() statusBar()->clearMessage(); ui->instanceToolBar->setEnabled(false); - ui->setInstanceActionsEnabled(false); + setInstanceActionsEnabled(false); updateToolsMenu(); - ui->renameButton->setText(tr("Rename Instance")); + renameButton->setText(tr("Rename Instance")); updateInstanceToolIcon("grass"); // ...and then see if we can enable the previously selected instance @@ -2495,6 +1808,18 @@ void MainWindow::updateStatusCenter() m_statusCenter->setText(tr("Total playtime: %1").arg(Time::prettifyDuration(timePlayed))); } } +// "Instance actions" are actions that require an instance to be selected (i.e. "new instance" is not here) +// Actions that also require other conditions (e.g. a running instance) won't be changed. +void MainWindow::setInstanceActionsEnabled(bool enabled) +{ + ui->actionEditInstance->setEnabled(enabled); + ui->actionChangeInstGroup->setEnabled(enabled); + ui->actionViewSelectedInstFolder->setEnabled(enabled); + ui->actionExportInstance->setEnabled(enabled); + ui->actionDeleteInstance->setEnabled(enabled); + ui->actionCopyInstance->setEnabled(enabled); + ui->actionCreateInstanceShortcut->setEnabled(enabled); +} void MainWindow::refreshCurrentInstance(bool running) { diff --git a/launcher/ui/MainWindow.h b/launcher/ui/MainWindow.h index 84b5325a..fab21a8f 100644 --- a/launcher/ui/MainWindow.h +++ b/launcher/ui/MainWindow.h @@ -61,13 +61,16 @@ class BaseProfilerFactory; class InstanceView; class KonamiCode; class InstanceTask; +class LabeledToolButton; +namespace Ui +{ +class MainWindow; +} class MainWindow : public QMainWindow { Q_OBJECT - class Ui; - public: explicit MainWindow(QWidget *parent = 0); ~MainWindow(); @@ -107,10 +110,6 @@ private slots: void on_actionChangeInstGroup_triggered(); void on_actionChangeInstIcon_triggered(); - void on_changeIconButton_clicked(bool) - { - on_actionChangeInstIcon_triggered(); - } void on_actionViewInstanceFolder_triggered(); @@ -156,10 +155,6 @@ private slots: void on_actionExportInstance_triggered(); void on_actionRenameInstance_triggered(); - void on_renameButton_clicked(bool) - { - on_actionRenameInstance_triggered(); - } void on_actionEditInstance_triggered(); @@ -230,14 +225,14 @@ private: void updateInstanceToolIcon(QString new_icon); void setSelectedInstanceById(const QString &id); void updateStatusCenter(); + void setInstanceActionsEnabled(bool enabled); void runModalTask(Task *task); void instanceFromInstanceTask(InstanceTask *task); void finalizeInstance(InstancePtr inst); private: - std::unique_ptr ui; - + Ui::MainWindow *ui; // these are managed by Qt's memory management model! InstanceView *view = nullptr; InstanceProxyModel *proxymodel = nullptr; @@ -245,9 +240,14 @@ private: QLabel *m_statusLeft = nullptr; QLabel *m_statusCenter = nullptr; QMenu *accountMenu = nullptr; + LabeledToolButton *changeIconButton = nullptr; + LabeledToolButton *renameButton = nullptr; QToolButton *accountMenuButton = nullptr; + QToolButton *helpMenuButton = nullptr; KonamiCode * secretEventFilter = nullptr; + std::shared_ptr instanceToolbarSetting = nullptr; + unique_qobject_ptr m_newsChecker; InstancePtr m_selectedInstance; diff --git a/launcher/ui/MainWindow.ui b/launcher/ui/MainWindow.ui new file mode 100644 index 00000000..6078ecbf --- /dev/null +++ b/launcher/ui/MainWindow.ui @@ -0,0 +1,697 @@ + + + MainWindow + + + + 0 + 0 + 800 + 600 + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + Main Toolbar + + + Qt::BottomToolBarArea|Qt::TopToolBarArea + + + Qt::ToolButtonTextBesideIcon + + + false + + + TopToolBarArea + + + false + + + + + + + + + + + + + + News Toolbar + + + Qt::BottomToolBarArea|Qt::TopToolBarArea + + + + 16 + 16 + + + + Qt::ToolButtonTextBesideIcon + + + false + + + BottomToolBarArea + + + false + + + + + + + Instance Toolbar + + + Qt::LeftToolBarArea|Qt::RightToolBarArea + + + + 16 + 16 + + + + Qt::ToolButtonTextBesideIcon + + + false + + + RightToolBarArea + + + false + + + + + + + + + + + + + + + + 0 + 0 + 800 + 20 + + + + + &File + + + true + + + + + + + + + + + + + + + + + + + + &Edit + + + true + + + + + + &View + + + true + + + + + + + + + + F&olders + + + true + + + + + + + &Accounts + + + + + &Help + + + true + + + + + + + + + + + + + + + + + + + + + + + + + .. + + + News + + + + + + .. + + + More news... + + + Open the development blog to read more news about %1. + + + + + true + + + + .. + + + &Meow + + + It's a fluffy kitty :3 + + + + + true + + + Lock Toolbars + + + + + false + + + &Undo Last Instance Deletion + + + + + + .. + + + Add Instanc&e... + + + Add a new instance. + + + + + + .. + + + &Update... + + + Check for new updates for %1. + + + QAction::ApplicationSpecificRole + + + + + + .. + + + Setti&ngs... + + + Change settings. + + + QAction::PreferencesRole + + + + + + .. + + + &Manage Accounts... + + + + + + .. + + + &Launch + + + Launch the selected instance. + + + + + + .. + + + &Kill + + + Kill the running instance + + + Ctrl+K + + + + + + .. + + + Rename + + + Rename the selected instance. + + + + + + .. + + + &Change Group... + + + Change the selected instance's group. + + + Ctrl+G + + + + + Change Icon + + + Change the selected instance's icon. + + + + + + .. + + + &Edit... + + + Change the instance settings, mods and versions. + + + Ctrl+I + + + + + + .. + + + &Folder + + + Open the selected instance's root folder in a file browser. + + + + + + .. + + + Dele&te + + + Delete the selected instance. + + + false + + + + + + .. + + + Cop&y... + + + Copy the selected instance. + + + Ctrl+D + + + + + Launch &Offline + + + Launch the selected instance in offline mode. + + + + + Launch &Demo + + + Launch the selected instance in demo mode. + + + + + + .. + + + E&xport... + + + Export the selected instance as a zip file. + + + Ctrl+E + + + + + + .. + + + Create Shortcut + + + Creates a shortcut on your desktop to launch the selected instance. + + + + + + .. + + + No accounts added! + + + + + + .. + + + No Default Account + + + + + + .. + + + Close &Window + + + Close the current window + + + QAction::QuitRole + + + + + + .. + + + &View Instance Folder + + + Open the instance folder in a file browser. + + + + + + .. + + + View &Central Mods Folder + + + Open the central mods folder in a file browser. + + + + + Themes + + + + + + .. + + + Report a &Bug... + + + Open the bug tracker to report a bug with %1. + + + + + + .. + + + &Discord Guild + + + Open %1 Discord guild. + + + + + + .. + + + &Matrix Space + + + Open %1 Matrix space + + + + + + .. + + + Sub&reddit + + + Open %1 subreddit. + + + + + + .. + + + &About %1 + + + View information about %1. + + + QAction::AboutRole + + + + + + .. + + + &Clear Metadata Cache + + + Clear cached metadata + + + + + false + + + + .. + + + Install to &PATH + + + Install a %1 symlink to /usr/local/bin + + + false + + + + + + .. + + + Folders + + + Open one of the folders shared between instances. + + + + + + .. + + + Help + + + Get help with %1 or Minecraft. + + + + + + .. + + + Accounts + + + + + + .. + + + %1 &Help + + + Open the %1 wiki + + + + + + WideBar + QToolBar +
    ui/widgets/WideBar.h
    +
    +
    + + +
    diff --git a/launcher/ui/widgets/WideBar.cpp b/launcher/ui/widgets/WideBar.cpp index cee2038f..a029b0a8 100644 --- a/launcher/ui/widgets/WideBar.cpp +++ b/launcher/ui/widgets/WideBar.cpp @@ -10,6 +10,9 @@ class ActionButton : public QToolButton { ActionButton(QAction* action, QWidget* parent = nullptr) : QToolButton(parent), m_action(action) { setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); + setToolButtonStyle(Qt::ToolButtonTextBesideIcon); + // workaround for breeze and breeze forks + setProperty("_kde_toolButton_alignment", Qt::AlignLeft); connect(action, &QAction::changed, this, &ActionButton::actionChanged); connect(this, &ActionButton::clicked, action, &QAction::trigger); @@ -21,6 +24,10 @@ class ActionButton : public QToolButton { { setEnabled(m_action->isEnabled()); setChecked(m_action->isChecked()); + setMenu(m_action->menu()); + if (menu()) { + setPopupMode(QToolButton::MenuButtonPopup); + } setCheckable(m_action->isCheckable()); setText(m_action->text()); setIcon(m_action->icon()); From b2de01b0760d6cb814fe570bc150ee6d891f2e9d Mon Sep 17 00:00:00 2001 From: leo78913 Date: Sun, 8 Jan 2023 22:47:38 -0300 Subject: [PATCH 118/152] feat(WideBar): Allow disabling alt shortcuts Signed-off-by: leo78913 --- launcher/ui/MainWindow.cpp | 1 + launcher/ui/MainWindow.ui | 3 +++ launcher/ui/widgets/WideBar.cpp | 34 ++++++++++++++++++++------------- launcher/ui/widgets/WideBar.h | 5 +++++ 4 files changed, 30 insertions(+), 13 deletions(-) diff --git a/launcher/ui/MainWindow.cpp b/launcher/ui/MainWindow.cpp index 30bbf685..69ef3016 100644 --- a/launcher/ui/MainWindow.cpp +++ b/launcher/ui/MainWindow.cpp @@ -174,6 +174,7 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWi instanceToolbarSetting = APPLICATION->settings()->getSetting(setting_name); ui->instanceToolBar->setVisibilityState(instanceToolbarSetting->get().toByteArray()); + } // set the menu for the folders and help tool buttons diff --git a/launcher/ui/MainWindow.ui b/launcher/ui/MainWindow.ui index 6078ecbf..218f0a2a 100644 --- a/launcher/ui/MainWindow.ui +++ b/launcher/ui/MainWindow.ui @@ -106,6 +106,9 @@ false + + true + RightToolBarArea diff --git a/launcher/ui/widgets/WideBar.cpp b/launcher/ui/widgets/WideBar.cpp index a029b0a8..717958fd 100644 --- a/launcher/ui/widgets/WideBar.cpp +++ b/launcher/ui/widgets/WideBar.cpp @@ -7,15 +7,20 @@ class ActionButton : public QToolButton { Q_OBJECT public: - ActionButton(QAction* action, QWidget* parent = nullptr) : QToolButton(parent), m_action(action) + ActionButton(QAction* action, QWidget* parent = nullptr, bool use_default_action = false) : QToolButton(parent), + m_action(action), m_use_default_action(use_default_action) { setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); setToolButtonStyle(Qt::ToolButtonTextBesideIcon); // workaround for breeze and breeze forks setProperty("_kde_toolButton_alignment", Qt::AlignLeft); + if (m_use_default_action) { + setDefaultAction(action); + } else { + connect(this, &ActionButton::clicked, action, &QAction::trigger); + } connect(action, &QAction::changed, this, &ActionButton::actionChanged); - connect(this, &ActionButton::clicked, action, &QAction::trigger); actionChanged(); }; @@ -23,21 +28,24 @@ class ActionButton : public QToolButton { void actionChanged() { setEnabled(m_action->isEnabled()); - setChecked(m_action->isChecked()); - setMenu(m_action->menu()); - if (menu()) { + // better pop up mode + if (m_action->menu()) { setPopupMode(QToolButton::MenuButtonPopup); } - setCheckable(m_action->isCheckable()); - setText(m_action->text()); - setIcon(m_action->icon()); - setToolTip(m_action->toolTip()); - setHidden(!m_action->isVisible()); + if (!m_use_default_action) { + setChecked(m_action->isChecked()); + setCheckable(m_action->isCheckable()); + setText(m_action->text()); + setIcon(m_action->icon()); + setToolTip(m_action->toolTip()); + setHidden(!m_action->isVisible()); + } setFocusPolicy(Qt::NoFocus); } private: QAction* m_action; + bool m_use_default_action; }; WideBar::WideBar(const QString& title, QWidget* parent) : QToolBar(title, parent) @@ -61,7 +69,7 @@ WideBar::WideBar(QWidget* parent) : QToolBar(parent) void WideBar::addAction(QAction* action) { BarEntry entry; - entry.bar_action = addWidget(new ActionButton(action, this)); + entry.bar_action = addWidget(new ActionButton(action, this, m_use_default_action)); entry.menu_action = action; entry.type = BarEntry::Type::Action; @@ -93,7 +101,7 @@ void WideBar::insertActionBefore(QAction* before, QAction* action) return; BarEntry entry; - entry.bar_action = insertWidget(iter->bar_action, new ActionButton(action, this)); + entry.bar_action = insertWidget(iter->bar_action, new ActionButton(action, this, m_use_default_action)); entry.menu_action = action; entry.type = BarEntry::Type::Action; @@ -109,7 +117,7 @@ void WideBar::insertActionAfter(QAction* after, QAction* action) return; BarEntry entry; - entry.bar_action = insertWidget((iter + 1)->bar_action, new ActionButton(action, this)); + entry.bar_action = insertWidget((iter + 1)->bar_action, new ActionButton(action, this, m_use_default_action)); entry.menu_action = action; entry.type = BarEntry::Type::Action; diff --git a/launcher/ui/widgets/WideBar.h b/launcher/ui/widgets/WideBar.h index 4004d415..59bda514 100644 --- a/launcher/ui/widgets/WideBar.h +++ b/launcher/ui/widgets/WideBar.h @@ -9,6 +9,9 @@ class WideBar : public QToolBar { Q_OBJECT + // Why: so we can enable / disable alt shortcuts in toolbuttons + // with toolbuttons using setDefaultAction, theres no alt shortcuts + Q_PROPERTY(bool useDefaultAction MEMBER m_use_default_action) public: explicit WideBar(const QString& title, QWidget* parent = nullptr); @@ -49,6 +52,8 @@ class WideBar : public QToolBar { private: QList m_entries; + bool m_use_default_action = false; + // Menu to toggle visibility from buttons in the bar std::unique_ptr m_bar_menu = nullptr; enum class MenuState { Fresh, Dirty } m_menu_state = MenuState::Dirty; From ada595663da02e951145690cd29d99454aae829b Mon Sep 17 00:00:00 2001 From: leo78913 Date: Mon, 9 Jan 2023 00:51:46 -0300 Subject: [PATCH 119/152] fix(widebar): fix insertSeparator WideBar::insertSeparator was adding the separator to the end of the toolbar Signed-off-by: leo78913 --- launcher/ui/MainWindow.cpp | 2 ++ launcher/ui/widgets/WideBar.cpp | 4 +++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/launcher/ui/MainWindow.cpp b/launcher/ui/MainWindow.cpp index 69ef3016..ca6827e0 100644 --- a/launcher/ui/MainWindow.cpp +++ b/launcher/ui/MainWindow.cpp @@ -166,6 +166,8 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWi connect(renameButton, &QToolButton::clicked, this, &MainWindow::on_actionRenameInstance_triggered); ui->instanceToolBar->insertWidgetBefore(ui->actionLaunchInstance, renameButton); + ui->instanceToolBar->insertSeparator(ui->actionLaunchInstance); + // restore the instance toolbar settings auto const setting_name = QString("WideBarVisibility_%1").arg(ui->instanceToolBar->objectName()); if (!APPLICATION->settings()->contains(setting_name)) diff --git a/launcher/ui/widgets/WideBar.cpp b/launcher/ui/widgets/WideBar.cpp index 717958fd..4f81f444 100644 --- a/launcher/ui/widgets/WideBar.cpp +++ b/launcher/ui/widgets/WideBar.cpp @@ -158,7 +158,9 @@ void WideBar::insertSeparator(QAction* before) return; BarEntry entry; - entry.bar_action = QToolBar::insertSeparator(before); + entry.bar_action = new QAction("", this); + entry.bar_action->setSeparator(true); + insertAction(iter->bar_action, entry.bar_action); entry.type = BarEntry::Type::Separator; m_entries.insert(iter, entry); From 3b38a4c690426bd368a4d0c9821d3cef3a157bcb Mon Sep 17 00:00:00 2001 From: leo78913 Date: Mon, 9 Jan 2023 19:28:36 -0300 Subject: [PATCH 120/152] Fix: translate NoAccountsAdded text Co-authored-by: flow Signed-off-by: leo78913 --- launcher/ui/MainWindow.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/launcher/ui/MainWindow.cpp b/launcher/ui/MainWindow.cpp index ca6827e0..d89ab97c 100644 --- a/launcher/ui/MainWindow.cpp +++ b/launcher/ui/MainWindow.cpp @@ -698,7 +698,7 @@ void MainWindow::repopulateAccountsMenu() if (accounts->count() <= 0) { - ui->actionNoAccountsAdded->setText( "No accounts added!"); + ui->actionNoAccountsAdded->setText(tr("No accounts added!")); ui->actionNoAccountsAdded->setEnabled(false); accountMenu->addAction(ui->actionNoAccountsAdded); ui->accountsMenu->addAction(ui->actionNoAccountsAdded); From 55d406433519f7542b389143e1fb1d0ab03105b1 Mon Sep 17 00:00:00 2001 From: leo78913 Date: Mon, 9 Jan 2023 19:29:09 -0300 Subject: [PATCH 121/152] Fix: translate actionNoDefaultAcount text Co-authored-by: flow Signed-off-by: leo78913 --- launcher/ui/MainWindow.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/launcher/ui/MainWindow.cpp b/launcher/ui/MainWindow.cpp index d89ab97c..1fc94807 100644 --- a/launcher/ui/MainWindow.cpp +++ b/launcher/ui/MainWindow.cpp @@ -743,7 +743,7 @@ void MainWindow::repopulateAccountsMenu() ui->actionNoDefaultAccount = new QAction(this); ui->actionNoDefaultAccount->setObjectName(QStringLiteral("actionNoDefaultAccount")); - ui->actionNoDefaultAccount->setText("No Default Account"); + ui->actionNoDefaultAccount->setText(tr("No Default Account")); ui->actionNoDefaultAccount->setCheckable(true); ui->actionNoDefaultAccount->setIcon(APPLICATION->getThemedIcon("noaccount")); ui->actionNoDefaultAccount->setData(-1); From f16989bea94163ade99c2c24fc88d53aaff30a8d Mon Sep 17 00:00:00 2001 From: leo78913 Date: Tue, 10 Jan 2023 12:02:02 -0300 Subject: [PATCH 122/152] feat(WideBar): custom context menu actions Signed-off-by: leo78913 --- launcher/ui/MainWindow.cpp | 4 ++++ launcher/ui/widgets/WideBar.cpp | 8 ++++++++ launcher/ui/widgets/WideBar.h | 4 ++++ 3 files changed, 16 insertions(+) diff --git a/launcher/ui/MainWindow.cpp b/launcher/ui/MainWindow.cpp index 1fc94807..ae458b38 100644 --- a/launcher/ui/MainWindow.cpp +++ b/launcher/ui/MainWindow.cpp @@ -177,6 +177,10 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWi ui->instanceToolBar->setVisibilityState(instanceToolbarSetting->get().toByteArray()); + ui->instanceToolBar->addContextMenuAction(ui->newsToolBar->toggleViewAction()); + ui->instanceToolBar->addContextMenuAction(ui->instanceToolBar->toggleViewAction()); + ui->instanceToolBar->addContextMenuAction(ui->actionLockToolbars); + } // set the menu for the folders and help tool buttons diff --git a/launcher/ui/widgets/WideBar.cpp b/launcher/ui/widgets/WideBar.cpp index 4f81f444..540d599d 100644 --- a/launcher/ui/widgets/WideBar.cpp +++ b/launcher/ui/widgets/WideBar.cpp @@ -207,6 +207,10 @@ void WideBar::showVisibilityMenu(QPoint const& position) m_bar_menu->clear(); + m_bar_menu->addActions(m_context_menu_actions); + + m_bar_menu->addSeparator()->setText(tr("Customize toolbar actions")); + for (auto& entry : m_entries) { if (entry.type != BarEntry::Type::Action) continue; @@ -233,6 +237,10 @@ void WideBar::showVisibilityMenu(QPoint const& position) m_bar_menu->popup(mapToGlobal(position)); } +void WideBar::addContextMenuAction(QAction* action) { + m_context_menu_actions.append(action); +} + [[nodiscard]] QByteArray WideBar::getVisibilityState() const { QByteArray state; diff --git a/launcher/ui/widgets/WideBar.h b/launcher/ui/widgets/WideBar.h index 59bda514..c47f3a59 100644 --- a/launcher/ui/widgets/WideBar.h +++ b/launcher/ui/widgets/WideBar.h @@ -30,6 +30,8 @@ class WideBar : public QToolBar { QMenu* createContextMenu(QWidget* parent = nullptr, const QString& title = QString()); void showVisibilityMenu(const QPoint&); + void addContextMenuAction(QAction* action); + // Ideally we would use a QBitArray for this, but it doesn't support string conversion, // so using it in settings is very messy. @@ -52,6 +54,8 @@ class WideBar : public QToolBar { private: QList m_entries; + QList m_context_menu_actions; + bool m_use_default_action = false; // Menu to toggle visibility from buttons in the bar From 4ed4fb2314213dc50635bb098ce690211a9188e9 Mon Sep 17 00:00:00 2001 From: leo78913 Date: Tue, 10 Jan 2023 12:03:21 -0300 Subject: [PATCH 123/152] remove useless setEnabled calls Signed-off-by: leo78913 --- launcher/ui/MainWindow.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/launcher/ui/MainWindow.cpp b/launcher/ui/MainWindow.cpp index ae458b38..dbbaa2b0 100644 --- a/launcher/ui/MainWindow.cpp +++ b/launcher/ui/MainWindow.cpp @@ -210,8 +210,6 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWi // disabled until we have an instance selected ui->instanceToolBar->setEnabled(false); - ui->actionKillInstance->setEnabled(false); - ui->actionLaunchInstance->setEnabled(false); setInstanceActionsEnabled(false); } From 6c5f6e890006d601de2a872d31910252948f4221 Mon Sep 17 00:00:00 2001 From: leo78913 Date: Tue, 10 Jan 2023 19:26:26 -0300 Subject: [PATCH 124/152] Fix status bar name Signed-off-by: leo78913 --- launcher/ui/MainWindow.ui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/launcher/ui/MainWindow.ui b/launcher/ui/MainWindow.ui index 218f0a2a..8437cb2e 100644 --- a/launcher/ui/MainWindow.ui +++ b/launcher/ui/MainWindow.ui @@ -29,7 +29,7 @@
    - + Main Toolbar From 670cf8ee07387a4b8c11854117b2a6d4d8517a1a Mon Sep 17 00:00:00 2001 From: leo78913 Date: Fri, 13 Jan 2023 16:51:19 -0300 Subject: [PATCH 125/152] Fix: make the newsLabel toolbutton fullwidth again this reverts it to how it was before the MainWindow .ui port Signed-off-by: leo78913 --- launcher/ui/MainWindow.cpp | 12 ++++++------ launcher/ui/MainWindow.ui | 10 ---------- 2 files changed, 6 insertions(+), 16 deletions(-) diff --git a/launcher/ui/MainWindow.cpp b/launcher/ui/MainWindow.cpp index dbbaa2b0..79e01d91 100644 --- a/launcher/ui/MainWindow.cpp +++ b/launcher/ui/MainWindow.cpp @@ -246,12 +246,12 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWi // Add the news label to the news toolbar. { m_newsChecker.reset(new NewsChecker(APPLICATION->network(), BuildConfig.NEWS_RSS_URL)); - newsLabel = dynamic_cast(ui->newsToolBar->widgetForAction(ui->actionNewsLabel)); - - //add a spacer before the more news button - QWidget *spacer = new QWidget(); - spacer->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); - ui->newsToolBar->insertWidget(ui->actionMoreNews, spacer); + newsLabel = new QToolButton(); + newsLabel->setIcon(APPLICATION->getThemedIcon("news")); + newsLabel->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); + newsLabel->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); + newsLabel->setFocusPolicy(Qt::NoFocus); + ui->newsToolBar->insertWidget(ui->actionMoreNews, newsLabel); QObject::connect(newsLabel, &QAbstractButton::clicked, this, &MainWindow::newsButtonClicked); QObject::connect(m_newsChecker.get(), &NewsChecker::newsLoaded, this, &MainWindow::updateNewsLabel); diff --git a/launcher/ui/MainWindow.ui b/launcher/ui/MainWindow.ui index 8437cb2e..42f70996 100644 --- a/launcher/ui/MainWindow.ui +++ b/launcher/ui/MainWindow.ui @@ -84,7 +84,6 @@ false - @@ -222,15 +221,6 @@ - - - - .. - - - News - - From 5a25ce8c1bb2aad54eb558297a11f6b614003cd1 Mon Sep 17 00:00:00 2001 From: leo78913 Date: Tue, 17 Jan 2023 19:51:56 -0300 Subject: [PATCH 126/152] Fix main window icon and stuff MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit i forgor 💀 Signed-off-by: leo78913 --- launcher/ui/MainWindow.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/launcher/ui/MainWindow.cpp b/launcher/ui/MainWindow.cpp index 79e01d91..a51cd55f 100644 --- a/launcher/ui/MainWindow.cpp +++ b/launcher/ui/MainWindow.cpp @@ -144,6 +144,12 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWi { ui->setupUi(this); + setWindowIcon(APPLICATION->getThemedIcon("logo")); + setWindowTitle(APPLICATION->applicationDisplayName()); +#ifndef QT_NO_ACCESSIBILITY + setAccessibleName(BuildConfig.LAUNCHER_DISPLAYNAME); +#endif + // instance toolbar stuff { // Qt doesn't like vertical moving toolbars, so we have to force them... From 2a949fcb867ca86594481780101edb37409e8198 Mon Sep 17 00:00:00 2001 From: TheLastRar Date: Sun, 8 Jan 2023 18:12:14 +0000 Subject: [PATCH 127/152] fix: zlib fallback Signed-off-by: TheLastRar --- CMakeLists.txt | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 2194317b..f32a73d1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -208,9 +208,15 @@ set(Launcher_BUILD_TIMESTAMP "${TODAY}") ################################ 3rd Party Libs ################################ -if(NOT Launcher_FORCE_BUNDLED_LIBS) +# Successive configurations of cmake without cleaning the build dir will cause zlib fallback to fail due to cached values +# Record when fallback triggered and skip this find_package +if(NOT Launcher_FORCE_BUNDLED_LIBS AND NOT FORCE_BUNDLED_ZLIB) find_package(ZLIB QUIET) endif() +if(NOT ZLIB_FOUND) + set(FORCE_BUNDLED_ZLIB TRUE CACHE BOOL "") + mark_as_advanced(FORCE_BUNDLED_ZLIB) +endif() # Find the required Qt parts include(QtVersionlessBackport) @@ -379,13 +385,14 @@ add_subdirectory(libraries/libnbtplusplus) add_subdirectory(libraries/systeminfo) # system information library add_subdirectory(libraries/launcher) # java based launcher part for Minecraft add_subdirectory(libraries/javacheck) # java compatibility checker -if(NOT ZLIB_FOUND) +if(FORCE_BUNDLED_ZLIB) message(STATUS "Using bundled zlib") + set(CMAKE_POLICY_DEFAULT_CMP0069 NEW) # Suppress cmake warnings and allow INTERPROCEDURAL_OPTIMIZATION for zlib set(SKIP_INSTALL_ALL ON) add_subdirectory(libraries/zlib EXCLUDE_FROM_ALL) - set(ZLIB_INCLUDE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/libraries/zlib" "${CMAKE_CURRENT_BINARY_DIR}/libraries/zlib" CACHE STRING "") + set(ZLIB_INCLUDE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/libraries/zlib" "${CMAKE_CURRENT_BINARY_DIR}/libraries/zlib" CACHE STRING "" FORCE) set_target_properties(zlibstatic PROPERTIES INTERFACE_INCLUDE_DIRECTORIES "${ZLIB_INCLUDE_DIR}") add_library(ZLIB::ZLIB ALIAS zlibstatic) set(ZLIB_LIBRARY ZLIB::ZLIB CACHE STRING "zlib library name") From ea5020e188d7cb6d4c8dcf7f953161759ed17899 Mon Sep 17 00:00:00 2001 From: flow Date: Mon, 23 Jan 2023 11:03:55 -0300 Subject: [PATCH 128/152] fix(license): add/fix my copyright/license headers *sobbing in messy legal stuff i know nothing about* Signed-off-by: flow --- launcher/ResourceDownloadTask.cpp | 4 +- launcher/ResourceDownloadTask.h | 4 +- launcher/modplatform/ResourceAPI.h | 4 +- launcher/modplatform/flame/FlameAPI.cpp | 4 ++ launcher/modplatform/flame/FlameAPI.h | 4 ++ .../helpers/NetworkResourceAPI.cpp | 4 ++ .../modplatform/helpers/NetworkResourceAPI.h | 4 ++ launcher/modplatform/modrinth/ModrinthAPI.cpp | 4 ++ launcher/modplatform/modrinth/ModrinthAPI.h | 18 +-------- .../ui/pages/instance/ManagedPackPage.cpp | 2 +- launcher/ui/pages/instance/ManagedPackPage.h | 2 +- launcher/ui/pages/modplatform/ModModel.cpp | 4 ++ launcher/ui/pages/modplatform/ModModel.h | 4 ++ launcher/ui/pages/modplatform/ModPage.cpp | 4 +- launcher/ui/pages/modplatform/ModPage.h | 4 ++ .../ui/pages/modplatform/ResourceModel.cpp | 4 ++ launcher/ui/pages/modplatform/ResourceModel.h | 4 ++ .../ui/pages/modplatform/ResourcePage.cpp | 38 +++++++++++++++++++ launcher/ui/pages/modplatform/ResourcePage.h | 4 ++ .../modplatform/flame/FlameResourceModels.cpp | 4 ++ .../modplatform/flame/FlameResourceModels.h | 4 ++ .../modplatform/flame/FlameResourcePages.cpp | 4 +- .../modplatform/flame/FlameResourcePages.h | 4 +- .../modrinth/ModrinthResourceModels.cpp | 2 + .../modrinth/ModrinthResourceModels.h | 2 + .../modrinth/ModrinthResourcePages.cpp | 2 + .../modrinth/ModrinthResourcePages.h | 4 +- 27 files changed, 119 insertions(+), 27 deletions(-) diff --git a/launcher/ResourceDownloadTask.cpp b/launcher/ResourceDownloadTask.cpp index 8c9dae6f..98bcf259 100644 --- a/launcher/ResourceDownloadTask.cpp +++ b/launcher/ResourceDownloadTask.cpp @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-only /* -* PolyMC - Minecraft Launcher -* Copyright (c) 2022 flowln +* Prism Launcher - Minecraft Launcher +* Copyright (c) 2022-2023 flowln * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify diff --git a/launcher/ResourceDownloadTask.h b/launcher/ResourceDownloadTask.h index 5ce39d69..73ad2d07 100644 --- a/launcher/ResourceDownloadTask.h +++ b/launcher/ResourceDownloadTask.h @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-only /* -* PolyMC - Minecraft Launcher -* Copyright (c) 2022 flowln +* Prism Launcher - Minecraft Launcher +* Copyright (c) 2022-2023 flowln * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify diff --git a/launcher/modplatform/ResourceAPI.h b/launcher/modplatform/ResourceAPI.h index dfb3652c..34f33779 100644 --- a/launcher/modplatform/ResourceAPI.h +++ b/launcher/modplatform/ResourceAPI.h @@ -1,4 +1,6 @@ -// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2023 flowln +// +// SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0 /* * PolyMC - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu diff --git a/launcher/modplatform/flame/FlameAPI.cpp b/launcher/modplatform/flame/FlameAPI.cpp index c8981585..57f70047 100644 --- a/launcher/modplatform/flame/FlameAPI.cpp +++ b/launcher/modplatform/flame/FlameAPI.cpp @@ -1,3 +1,7 @@ +// SPDX-FileCopyrightText: 2023 flowln +// +// SPDX-License-Identifier: GPL-3.0-only + #include "FlameAPI.h" #include "FlameModIndex.h" diff --git a/launcher/modplatform/flame/FlameAPI.h b/launcher/modplatform/flame/FlameAPI.h index 8e7ed727..06d749e6 100644 --- a/launcher/modplatform/flame/FlameAPI.h +++ b/launcher/modplatform/flame/FlameAPI.h @@ -1,3 +1,7 @@ +// SPDX-FileCopyrightText: 2023 flowln +// +// SPDX-License-Identifier: GPL-3.0-only + #pragma once #include "modplatform/ModIndex.h" diff --git a/launcher/modplatform/helpers/NetworkResourceAPI.cpp b/launcher/modplatform/helpers/NetworkResourceAPI.cpp index 88bbc045..ac994c31 100644 --- a/launcher/modplatform/helpers/NetworkResourceAPI.cpp +++ b/launcher/modplatform/helpers/NetworkResourceAPI.cpp @@ -1,3 +1,7 @@ +// SPDX-FileCopyrightText: 2023 flowln +// +// SPDX-License-Identifier: GPL-3.0-only + #include "NetworkResourceAPI.h" #include "Application.h" diff --git a/launcher/modplatform/helpers/NetworkResourceAPI.h b/launcher/modplatform/helpers/NetworkResourceAPI.h index ab5586fd..94813bec 100644 --- a/launcher/modplatform/helpers/NetworkResourceAPI.h +++ b/launcher/modplatform/helpers/NetworkResourceAPI.h @@ -1,3 +1,7 @@ +// SPDX-FileCopyrightText: 2023 flowln +// +// SPDX-License-Identifier: GPL-3.0-only + #pragma once #include "modplatform/ResourceAPI.h" diff --git a/launcher/modplatform/modrinth/ModrinthAPI.cpp b/launcher/modplatform/modrinth/ModrinthAPI.cpp index 028480a9..0c601d22 100644 --- a/launcher/modplatform/modrinth/ModrinthAPI.cpp +++ b/launcher/modplatform/modrinth/ModrinthAPI.cpp @@ -1,3 +1,7 @@ +// SPDX-FileCopyrightText: 2023 flowln +// +// SPDX-License-Identifier: GPL-3.0-only + #include "ModrinthAPI.h" #include "Application.h" diff --git a/launcher/modplatform/modrinth/ModrinthAPI.h b/launcher/modplatform/modrinth/ModrinthAPI.h index cba3afc8..dda27303 100644 --- a/launcher/modplatform/modrinth/ModrinthAPI.h +++ b/launcher/modplatform/modrinth/ModrinthAPI.h @@ -1,20 +1,6 @@ +// SPDX-FileCopyrightText: 2022-2023 flowln +// // SPDX-License-Identifier: GPL-3.0-only -/* - * PolyMC - Minecraft Launcher - * Copyright (c) 2022 flowln - * - * 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 diff --git a/launcher/ui/pages/instance/ManagedPackPage.cpp b/launcher/ui/pages/instance/ManagedPackPage.cpp index 8d56d894..dc983d9a 100644 --- a/launcher/ui/pages/instance/ManagedPackPage.cpp +++ b/launcher/ui/pages/instance/ManagedPackPage.cpp @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2022 flow +// SPDX-FileCopyrightText: 2022 flowln // // SPDX-License-Identifier: GPL-3.0-only diff --git a/launcher/ui/pages/instance/ManagedPackPage.h b/launcher/ui/pages/instance/ManagedPackPage.h index 55782ba7..1ac6fc03 100644 --- a/launcher/ui/pages/instance/ManagedPackPage.h +++ b/launcher/ui/pages/instance/ManagedPackPage.h @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2022 flow +// SPDX-FileCopyrightText: 2022 flowln // // SPDX-License-Identifier: GPL-3.0-only diff --git a/launcher/ui/pages/modplatform/ModModel.cpp b/launcher/ui/pages/modplatform/ModModel.cpp index 433c7b10..3ffe6cb0 100644 --- a/launcher/ui/pages/modplatform/ModModel.cpp +++ b/launcher/ui/pages/modplatform/ModModel.cpp @@ -1,3 +1,7 @@ +// SPDX-FileCopyrightText: 2023 flowln +// +// SPDX-License-Identifier: GPL-3.0-only + #include "ModModel.h" #include "minecraft/MinecraftInstance.h" diff --git a/launcher/ui/pages/modplatform/ModModel.h b/launcher/ui/pages/modplatform/ModModel.h index 1fac9040..5d4a7785 100644 --- a/launcher/ui/pages/modplatform/ModModel.h +++ b/launcher/ui/pages/modplatform/ModModel.h @@ -1,3 +1,7 @@ +// SPDX-FileCopyrightText: 2023 flowln +// +// SPDX-License-Identifier: GPL-3.0-only + #pragma once #include diff --git a/launcher/ui/pages/modplatform/ModPage.cpp b/launcher/ui/pages/modplatform/ModPage.cpp index d57e748b..04be43ad 100644 --- a/launcher/ui/pages/modplatform/ModPage.cpp +++ b/launcher/ui/pages/modplatform/ModPage.cpp @@ -1,4 +1,6 @@ -// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2023 flowln +// +// SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0 /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu diff --git a/launcher/ui/pages/modplatform/ModPage.h b/launcher/ui/pages/modplatform/ModPage.h index a3aab1de..c3b58cd6 100644 --- a/launcher/ui/pages/modplatform/ModPage.h +++ b/launcher/ui/pages/modplatform/ModPage.h @@ -1,3 +1,7 @@ +// SPDX-FileCopyrightText: 2023 flowln +// +// SPDX-License-Identifier: GPL-3.0-only + #pragma once #include diff --git a/launcher/ui/pages/modplatform/ResourceModel.cpp b/launcher/ui/pages/modplatform/ResourceModel.cpp index eb723159..8af70104 100644 --- a/launcher/ui/pages/modplatform/ResourceModel.cpp +++ b/launcher/ui/pages/modplatform/ResourceModel.cpp @@ -1,3 +1,7 @@ +// SPDX-FileCopyrightText: 2023 flowln +// +// SPDX-License-Identifier: GPL-3.0-only + #include "ResourceModel.h" #include diff --git a/launcher/ui/pages/modplatform/ResourceModel.h b/launcher/ui/pages/modplatform/ResourceModel.h index 7e813373..610b631c 100644 --- a/launcher/ui/pages/modplatform/ResourceModel.h +++ b/launcher/ui/pages/modplatform/ResourceModel.h @@ -1,3 +1,7 @@ +// SPDX-FileCopyrightText: 2023 flowln +// +// SPDX-License-Identifier: GPL-3.0-only + #pragma once #include diff --git a/launcher/ui/pages/modplatform/ResourcePage.cpp b/launcher/ui/pages/modplatform/ResourcePage.cpp index bfa7e33d..bbd465bc 100644 --- a/launcher/ui/pages/modplatform/ResourcePage.cpp +++ b/launcher/ui/pages/modplatform/ResourcePage.cpp @@ -1,3 +1,41 @@ +// SPDX-FileCopyrightText: 2023 flowln +// +// SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0 +/* + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu + * Copyright (C) 2022 TheKodeToad + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * 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 "ResourcePage.h" #include "ui_ResourcePage.h" diff --git a/launcher/ui/pages/modplatform/ResourcePage.h b/launcher/ui/pages/modplatform/ResourcePage.h index 71fc6593..1896d53e 100644 --- a/launcher/ui/pages/modplatform/ResourcePage.h +++ b/launcher/ui/pages/modplatform/ResourcePage.h @@ -1,3 +1,7 @@ +// SPDX-FileCopyrightText: 2023 flowln +// +// SPDX-License-Identifier: GPL-3.0-only + #pragma once #include diff --git a/launcher/ui/pages/modplatform/flame/FlameResourceModels.cpp b/launcher/ui/pages/modplatform/flame/FlameResourceModels.cpp index a1cd1f26..de1f2122 100644 --- a/launcher/ui/pages/modplatform/flame/FlameResourceModels.cpp +++ b/launcher/ui/pages/modplatform/flame/FlameResourceModels.cpp @@ -1,3 +1,7 @@ +// SPDX-FileCopyrightText: 2023 flowln +// +// SPDX-License-Identifier: GPL-3.0-only + #include "FlameResourceModels.h" #include "Json.h" diff --git a/launcher/ui/pages/modplatform/flame/FlameResourceModels.h b/launcher/ui/pages/modplatform/flame/FlameResourceModels.h index 47fbbe1a..625a2a7d 100644 --- a/launcher/ui/pages/modplatform/flame/FlameResourceModels.h +++ b/launcher/ui/pages/modplatform/flame/FlameResourceModels.h @@ -1,3 +1,7 @@ +// SPDX-FileCopyrightText: 2023 flowln +// +// SPDX-License-Identifier: GPL-3.0-only + #pragma once #include "ui/pages/modplatform/ModModel.h" diff --git a/launcher/ui/pages/modplatform/flame/FlameResourcePages.cpp b/launcher/ui/pages/modplatform/flame/FlameResourcePages.cpp index e34be7fd..485431a7 100644 --- a/launcher/ui/pages/modplatform/flame/FlameResourcePages.cpp +++ b/launcher/ui/pages/modplatform/flame/FlameResourcePages.cpp @@ -1,4 +1,6 @@ -// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2023 flowln +// +// SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0 /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu diff --git a/launcher/ui/pages/modplatform/flame/FlameResourcePages.h b/launcher/ui/pages/modplatform/flame/FlameResourcePages.h index 12b51aa9..b21a53ad 100644 --- a/launcher/ui/pages/modplatform/flame/FlameResourcePages.h +++ b/launcher/ui/pages/modplatform/flame/FlameResourcePages.h @@ -1,4 +1,6 @@ -// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2023 flowln +// +// SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0 /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthResourceModels.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthResourceModels.cpp index 06b72fd0..73d55133 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthResourceModels.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthResourceModels.cpp @@ -1,3 +1,5 @@ +// SPDX-FileCopyrightText: 2023 flowln +// // SPDX-License-Identifier: GPL-3.0-only /* * PolyMC - Minecraft Launcher diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthResourceModels.h b/launcher/ui/pages/modplatform/modrinth/ModrinthResourceModels.h index 2511f5e5..56cab146 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthResourceModels.h +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthResourceModels.h @@ -1,3 +1,5 @@ +// SPDX-FileCopyrightText: 2023 flowln +// // SPDX-License-Identifier: GPL-3.0-only /* * PolyMC - Minecraft Launcher diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.cpp index 45902d16..b82f800e 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.cpp @@ -1,3 +1,5 @@ +// SPDX-FileCopyrightText: 2023 flowln +// // SPDX-License-Identifier: GPL-3.0-only /* * PolyMC - Minecraft Launcher diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.h b/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.h index a263bd44..be38eff1 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.h +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.h @@ -1,4 +1,6 @@ -// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2023 flowln +// +// SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0 /* * PolyMC - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu From 322f317a5b4908348b92f65a611f474382f83069 Mon Sep 17 00:00:00 2001 From: TheLastRar Date: Mon, 23 Jan 2023 19:03:12 +0000 Subject: [PATCH 129/152] fix: Undo zlibs file rename when using bundled zlib Signed-off-by: TheLastRar --- CMakeLists.txt | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index f32a73d1..37bb49ba 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -391,8 +391,18 @@ if(FORCE_BUNDLED_ZLIB) set(CMAKE_POLICY_DEFAULT_CMP0069 NEW) # Suppress cmake warnings and allow INTERPROCEDURAL_OPTIMIZATION for zlib set(SKIP_INSTALL_ALL ON) add_subdirectory(libraries/zlib EXCLUDE_FROM_ALL) - - set(ZLIB_INCLUDE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/libraries/zlib" "${CMAKE_CURRENT_BINARY_DIR}/libraries/zlib" CACHE STRING "" FORCE) + + # On OS where unistd.h exists, zlib's generated header defines `Z_HAVE_UNISTD_H`, while the included header does not. + # We cannot safely undo the rename on those systems, and they generally have packages for zlib anyway. + check_include_file(unistd.h NEED_GENERATED_ZCONF) + if (EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/libraries/zlib/zconf.h.included" AND NOT NEED_GENERATED_ZCONF) + # zlib's cmake script renames a file, dirtying the submodule, see https://github.com/madler/zlib/issues/162 + message(STATUS "Undoing Rename") + message(STATUS " ${CMAKE_CURRENT_SOURCE_DIR}/libraries/zlib/zconf.h") + file(RENAME "${CMAKE_CURRENT_SOURCE_DIR}/libraries/zlib/zconf.h.included" "${CMAKE_CURRENT_SOURCE_DIR}/libraries/zlib/zconf.h") + endif() + + set(ZLIB_INCLUDE_DIR "${CMAKE_CURRENT_BINARY_DIR}/libraries/zlib" "${CMAKE_CURRENT_SOURCE_DIR}/libraries/zlib" CACHE STRING "" FORCE) set_target_properties(zlibstatic PROPERTIES INTERFACE_INCLUDE_DIRECTORIES "${ZLIB_INCLUDE_DIR}") add_library(ZLIB::ZLIB ALIAS zlibstatic) set(ZLIB_LIBRARY ZLIB::ZLIB CACHE STRING "zlib library name") From c45fa016c0f0e5c8a03f029488de29bde8dadcc4 Mon Sep 17 00:00:00 2001 From: Rachel Powers <508861+Ryex@users.noreply.github.com> Date: Mon, 23 Jan 2023 18:36:58 -0700 Subject: [PATCH 130/152] fix: let jars be found from inside build dir for debug builds debug bug builds run form inside the build dir before they are bundled can't find the jars Signed-off-by: Rachel Powers <508861+Ryex@users.noreply.github.com> --- launcher/Application.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/launcher/Application.cpp b/launcher/Application.cpp index 5f70ab94..537ffb68 100644 --- a/launcher/Application.cpp +++ b/launcher/Application.cpp @@ -1547,7 +1547,10 @@ QString Application::getJarPath(QString jarFile) FS::PathCombine(m_rootPath, "share/" + BuildConfig.LAUNCHER_APP_BINARY_NAME), #endif FS::PathCombine(m_rootPath, "jars"), - FS::PathCombine(applicationDirPath(), "jars") + FS::PathCombine(applicationDirPath(), "jars"), +#if !defined(NDEBUG) + FS::PathCombine(applicationDirPath(), "..", "jars") // from inside build dir , for debuging +#endif }; for(QString p : potentialPaths) { From 085e067fc1c34c08db369dbf9136faca50ed048c Mon Sep 17 00:00:00 2001 From: Rachel Powers <508861+Ryex@users.noreply.github.com> Date: Tue, 24 Jan 2023 02:26:21 -0700 Subject: [PATCH 131/152] remove NDEBUG check per Scrumplex's orders Signed-off-by: Rachel Powers <508861+Ryex@users.noreply.github.com> --- launcher/Application.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/launcher/Application.cpp b/launcher/Application.cpp index 537ffb68..608fc618 100644 --- a/launcher/Application.cpp +++ b/launcher/Application.cpp @@ -1548,9 +1548,7 @@ QString Application::getJarPath(QString jarFile) #endif FS::PathCombine(m_rootPath, "jars"), FS::PathCombine(applicationDirPath(), "jars"), -#if !defined(NDEBUG) FS::PathCombine(applicationDirPath(), "..", "jars") // from inside build dir , for debuging -#endif }; for(QString p : potentialPaths) { From 58239ff98f383682aedd2184d50cfc8638cba3cb Mon Sep 17 00:00:00 2001 From: DioEgizio <83089242+DioEgizio@users.noreply.github.com> Date: Tue, 24 Jan 2023 15:05:13 +0100 Subject: [PATCH 132/152] fix: update cmark to fix a CVE Signed-off-by: DioEgizio <83089242+DioEgizio@users.noreply.github.com> --- libraries/cmark | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/cmark b/libraries/cmark index a8da5a2f..5ba25ff4 160000 --- a/libraries/cmark +++ b/libraries/cmark @@ -1 +1 @@ -Subproject commit a8da5a2f252b96eca60ae8bada1a9ba059a38401 +Subproject commit 5ba25ff40eba44c811f79ab6a792baf945b8307c From 3ddf41333230cd8d04c18bac27df75941d14ce6e Mon Sep 17 00:00:00 2001 From: Rachel Powers <508861+Ryex@users.noreply.github.com> Date: Tue, 24 Jan 2023 09:24:12 -0700 Subject: [PATCH 133/152] Update launcher/Application.cpp Co-authored-by: Sefa Eyeoglu Signed-off-by: Rachel Powers <508861+Ryex@users.noreply.github.com> --- launcher/Application.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/launcher/Application.cpp b/launcher/Application.cpp index 608fc618..6a798822 100644 --- a/launcher/Application.cpp +++ b/launcher/Application.cpp @@ -1548,7 +1548,7 @@ QString Application::getJarPath(QString jarFile) #endif FS::PathCombine(m_rootPath, "jars"), FS::PathCombine(applicationDirPath(), "jars"), - FS::PathCombine(applicationDirPath(), "..", "jars") // from inside build dir , for debuging + FS::PathCombine(applicationDirPath(), "..", "jars") // from inside build dir, for debuging }; for(QString p : potentialPaths) { From 6d27ef5eeada43853b55a591921c8d5a78d537c9 Mon Sep 17 00:00:00 2001 From: flow Date: Tue, 24 Jan 2023 15:43:21 -0300 Subject: [PATCH 134/152] fix(ResourceFolder): don't create two smart ptrs for the same raw ptr Signed-off-by: flow --- launcher/minecraft/mod/ResourceFolderModel.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/launcher/minecraft/mod/ResourceFolderModel.cpp b/launcher/minecraft/mod/ResourceFolderModel.cpp index a52c5db3..fdfb434b 100644 --- a/launcher/minecraft/mod/ResourceFolderModel.cpp +++ b/launcher/minecraft/mod/ResourceFolderModel.cpp @@ -260,7 +260,7 @@ void ResourceFolderModel::resolveResource(Resource* res) return; } - auto task = createParseTask(*res); + Task::Ptr task{ createParseTask(*res) }; if (!task) return; @@ -270,11 +270,11 @@ void ResourceFolderModel::resolveResource(Resource* res) m_active_parse_tasks.insert(ticket, task); connect( - task, &Task::succeeded, this, [=] { onParseSucceeded(ticket, res->internal_id()); }, Qt::ConnectionType::QueuedConnection); + task.get(), &Task::succeeded, this, [=] { onParseSucceeded(ticket, res->internal_id()); }, Qt::ConnectionType::QueuedConnection); connect( - task, &Task::failed, this, [=] { onParseFailed(ticket, res->internal_id()); }, Qt::ConnectionType::QueuedConnection); + task.get(), &Task::failed, this, [=] { onParseFailed(ticket, res->internal_id()); }, Qt::ConnectionType::QueuedConnection); connect( - task, &Task::finished, this, [=] { m_active_parse_tasks.remove(ticket); }, Qt::ConnectionType::QueuedConnection); + task.get(), &Task::finished, this, [=] { m_active_parse_tasks.remove(ticket); }, Qt::ConnectionType::QueuedConnection); m_helper_thread_task.addTask(task); From 90feaaf2df2e0a6e38bc21b6f96a3f53b443e1f4 Mon Sep 17 00:00:00 2001 From: flow Date: Tue, 24 Jan 2023 15:44:12 -0300 Subject: [PATCH 135/152] fix(Tasks): don't try to start more tasks than necessary Signed-off-by: flow --- launcher/tasks/ConcurrentTask.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/launcher/tasks/ConcurrentTask.cpp b/launcher/tasks/ConcurrentTask.cpp index 190d48d8..3cc37b2a 100644 --- a/launcher/tasks/ConcurrentTask.cpp +++ b/launcher/tasks/ConcurrentTask.cpp @@ -115,7 +115,7 @@ void ConcurrentTask::startNext() QMetaObject::invokeMethod(next.get(), &Task::start, Qt::QueuedConnection); // Allow going up the number of concurrent tasks in case of tasks being added in the middle of a running task. - int num_starts = m_total_max_size - m_doing.size(); + int num_starts = qMin(m_queue.size(), m_total_max_size - m_doing.size()); for (int i = 0; i < num_starts; i++) QMetaObject::invokeMethod(this, &ConcurrentTask::startNext, Qt::QueuedConnection); } From 29f7ea752fd34bdea64a7c7f2c505982ac39ce0d Mon Sep 17 00:00:00 2001 From: flow Date: Tue, 24 Jan 2023 16:52:09 -0300 Subject: [PATCH 136/152] refactor: make shared_qobject_ptr ctor explicit This turns issues like creating two shared ptrs from a single raw ptr from popping up at runtime, instead making them a compile error. Signed-off-by: flow --- launcher/Application.cpp | 2 +- launcher/InstanceImportTask.cpp | 4 +- launcher/LaunchController.cpp | 6 +- launcher/QObjectPtr.h | 16 ++- launcher/java/JavaInstallList.cpp | 4 +- launcher/launch/steps/CheckJava.cpp | 2 +- launcher/meta/BaseEntity.cpp | 2 +- launcher/minecraft/AssetsUtils.cpp | 2 +- launcher/minecraft/ComponentUpdateTask.cpp | 2 +- launcher/minecraft/MinecraftInstance.cpp | 39 ++++--- launcher/minecraft/MinecraftUpdate.cpp | 8 +- launcher/minecraft/PackProfile.cpp | 20 ++-- launcher/minecraft/PackProfile.h | 4 +- launcher/minecraft/auth/MinecraftAccount.cpp | 4 +- launcher/minecraft/auth/flows/MSA.cpp | 36 +++--- launcher/minecraft/auth/flows/Mojang.cpp | 16 +-- launcher/minecraft/auth/flows/Offline.cpp | 4 +- .../minecraft/mod/ResourcePackFolderModel.cpp | 2 +- .../minecraft/mod/TexturePackFolderModel.cpp | 2 +- .../minecraft/mod/tasks/BasicFolderLoadTask.h | 8 +- .../minecraft/mod/tasks/ModFolderLoadTask.cpp | 8 +- launcher/minecraft/update/AssetUpdateTask.cpp | 2 +- .../minecraft/update/FMLLibrariesTask.cpp | 10 +- launcher/minecraft/update/LibrariesTask.cpp | 2 +- launcher/modplatform/CheckUpdateTask.h | 4 +- launcher/modplatform/EnsureMetadataTask.cpp | 8 +- launcher/modplatform/EnsureMetadataTask.h | 2 +- .../atlauncher/ATLPackInstallTask.cpp | 19 ++-- .../modplatform/flame/FileResolvingTask.cpp | 4 +- launcher/modplatform/flame/FlameAPI.cpp | 16 +-- .../modplatform/flame/FlameCheckUpdate.cpp | 2 +- .../flame/FlameInstanceCreationTask.cpp | 4 +- launcher/modplatform/helpers/HashUtils.cpp | 8 +- .../helpers/NetworkResourceAPI.cpp | 18 +-- .../modplatform/legacy_ftb/PackFetchTask.cpp | 2 +- .../legacy_ftb/PackInstallTask.cpp | 2 +- .../modpacksch/FTBPackInstallTask.cpp | 22 ++-- launcher/modplatform/modrinth/ModrinthAPI.cpp | 21 ++-- .../modrinth/ModrinthCheckUpdate.cpp | 2 +- .../modrinth/ModrinthInstanceCreationTask.cpp | 2 +- .../technic/SingleZipPackInstallTask.cpp | 4 +- .../technic/SolderPackInstallTask.cpp | 6 +- launcher/net/Download.cpp | 11 +- launcher/net/Download.h | 3 - launcher/net/Upload.cpp | 2 +- launcher/net/Upload.h | 2 + launcher/news/NewsChecker.cpp | 6 +- launcher/tasks/ConcurrentTask.h | 2 + launcher/translations/TranslationsModel.cpp | 4 +- launcher/ui/dialogs/ModUpdateDialog.cpp | 30 ++--- launcher/ui/dialogs/ModUpdateDialog.h | 10 +- .../ui/dialogs/ResourceDownloadDialog.cpp | 2 +- launcher/ui/pages/instance/VersionPage.cpp | 2 +- launcher/ui/pages/instance/VersionPage.h | 2 +- .../ui/pages/modplatform/ResourceModel.cpp | 2 +- .../modplatform/atlauncher/AtlListModel.cpp | 6 +- .../ui/pages/modplatform/flame/FlameModel.cpp | 6 +- .../ui/pages/modplatform/ftb/FtbListModel.cpp | 18 +-- .../modplatform/modrinth/ModrinthModel.cpp | 6 +- .../modplatform/technic/TechnicModel.cpp | 6 +- .../pages/modplatform/technic/TechnicPage.cpp | 8 +- tests/DummyResourceAPI.h | 5 +- tests/Task_test.cpp | 104 ++++++++++-------- 63 files changed, 301 insertions(+), 287 deletions(-) diff --git a/launcher/Application.cpp b/launcher/Application.cpp index d4a1284f..387f735c 100644 --- a/launcher/Application.cpp +++ b/launcher/Application.cpp @@ -679,7 +679,7 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv) // initialize network access and proxy setup { - m_network = new QNetworkAccessManager(); + m_network.reset(new QNetworkAccessManager()); QString proxyTypeStr = settings()->get("ProxyType").toString(); QString addr = settings()->get("ProxyAddr").toString(); int port = settings()->get("ProxyPort").value(); diff --git a/launcher/InstanceImportTask.cpp b/launcher/InstanceImportTask.cpp index 6b3fd296..70bf5784 100644 --- a/launcher/InstanceImportTask.cpp +++ b/launcher/InstanceImportTask.cpp @@ -88,7 +88,7 @@ void InstanceImportTask::executeTask() entry->setStale(true); m_archivePath = entry->getFullPath(); - m_filesNetJob = new NetJob(tr("Modpack download"), APPLICATION->network()); + m_filesNetJob.reset(new NetJob(tr("Modpack download"), APPLICATION->network())); m_filesNetJob->addNetAction(Net::Download::makeCached(m_sourceUrl, entry)); connect(m_filesNetJob.get(), &NetJob::succeeded, this, &InstanceImportTask::downloadSucceeded); @@ -301,7 +301,7 @@ void InstanceImportTask::processFlame() void InstanceImportTask::processTechnic() { - shared_qobject_ptr packProcessor = new Technic::TechnicPackProcessor(); + shared_qobject_ptr packProcessor{ new Technic::TechnicPackProcessor }; connect(packProcessor.get(), &Technic::TechnicPackProcessor::succeeded, this, &InstanceImportTask::emitSucceeded); connect(packProcessor.get(), &Technic::TechnicPackProcessor::failed, this, &InstanceImportTask::emitFailed); packProcessor->run(m_globalSettings, name(), m_instIcon, m_stagingPath); diff --git a/launcher/LaunchController.cpp b/launcher/LaunchController.cpp index 9741fd95..070ee283 100644 --- a/launcher/LaunchController.cpp +++ b/launcher/LaunchController.cpp @@ -382,15 +382,15 @@ void LaunchController::launchInstance() } resolved_servers = resolved_servers + "]\n\n"; } - m_launcher->prependStep(new TextPrint(m_launcher.get(), resolved_servers, MessageLevel::Launcher)); + m_launcher->prependStep(makeShared(m_launcher.get(), resolved_servers, MessageLevel::Launcher)); } else { online_mode = m_demo ? "demo" : "offline"; } - m_launcher->prependStep(new TextPrint(m_launcher.get(), "Launched instance in " + online_mode + " mode\n", MessageLevel::Launcher)); + m_launcher->prependStep(makeShared(m_launcher.get(), "Launched instance in " + online_mode + " mode\n", MessageLevel::Launcher)); // Prepend Version - m_launcher->prependStep(new TextPrint(m_launcher.get(), BuildConfig.LAUNCHER_DISPLAYNAME + " version: " + BuildConfig.printableVersionString() + "\n\n", MessageLevel::Launcher)); + m_launcher->prependStep(makeShared(m_launcher.get(), BuildConfig.LAUNCHER_DISPLAYNAME + " version: " + BuildConfig.printableVersionString() + "\n\n", MessageLevel::Launcher)); m_launcher->start(); } diff --git a/launcher/QObjectPtr.h b/launcher/QObjectPtr.h index ec466096..a1c64b43 100644 --- a/launcher/QObjectPtr.h +++ b/launcher/QObjectPtr.h @@ -20,8 +20,8 @@ using unique_qobject_ptr = QScopedPointer; template class shared_qobject_ptr : public QSharedPointer { public: - constexpr shared_qobject_ptr() : QSharedPointer() {} - constexpr shared_qobject_ptr(T* ptr) : QSharedPointer(ptr, &QObject::deleteLater) {} + constexpr explicit shared_qobject_ptr() : QSharedPointer() {} + constexpr explicit shared_qobject_ptr(T* ptr) : QSharedPointer(ptr, &QObject::deleteLater) {} constexpr shared_qobject_ptr(std::nullptr_t null_ptr) : QSharedPointer(null_ptr, &QObject::deleteLater) {} template @@ -33,9 +33,21 @@ class shared_qobject_ptr : public QSharedPointer { {} void reset() { QSharedPointer::reset(); } + void reset(T*&& other) + { + shared_qobject_ptr t(other); + this->swap(t); + } void reset(const shared_qobject_ptr& other) { shared_qobject_ptr t(other); this->swap(t); } }; + +template +shared_qobject_ptr makeShared(Args... args) +{ + auto obj = new T(args...); + return shared_qobject_ptr(obj); +} diff --git a/launcher/java/JavaInstallList.cpp b/launcher/java/JavaInstallList.cpp index e2f0aa00..b29af857 100644 --- a/launcher/java/JavaInstallList.cpp +++ b/launcher/java/JavaInstallList.cpp @@ -67,7 +67,7 @@ void JavaInstallList::load() if(m_status != Status::InProgress) { m_status = Status::InProgress; - m_loadTask = new JavaListLoadTask(this); + m_loadTask.reset(new JavaListLoadTask(this)); m_loadTask->start(); } } @@ -167,7 +167,7 @@ void JavaListLoadTask::executeTask() JavaUtils ju; QList candidate_paths = ju.FindJavaPaths(); - m_job = new JavaCheckerJob("Java detection"); + m_job.reset(new JavaCheckerJob("Java detection")); connect(m_job.get(), &Task::finished, this, &JavaListLoadTask::javaCheckerFinished); connect(m_job.get(), &Task::progress, this, &Task::setProgress); diff --git a/launcher/launch/steps/CheckJava.cpp b/launcher/launch/steps/CheckJava.cpp index 7aeb61bf..f0187586 100644 --- a/launcher/launch/steps/CheckJava.cpp +++ b/launcher/launch/steps/CheckJava.cpp @@ -93,7 +93,7 @@ void CheckJava::executeTask() || storedArchitecture.size() == 0 || storedRealArchitecture.size() == 0 || storedVendor.size() == 0) { - m_JavaChecker = new JavaChecker(); + m_JavaChecker.reset(new JavaChecker); emit logLine(QString("Checking Java version..."), MessageLevel::Launcher); connect(m_JavaChecker.get(), &JavaChecker::checkFinished, this, &CheckJava::checkJavaFinished); m_JavaChecker->m_path = realJavaPath; diff --git a/launcher/meta/BaseEntity.cpp b/launcher/meta/BaseEntity.cpp index de4e1012..97815eba 100644 --- a/launcher/meta/BaseEntity.cpp +++ b/launcher/meta/BaseEntity.cpp @@ -126,7 +126,7 @@ void Meta::BaseEntity::load(Net::Mode loadType) { return; } - m_updateTask = new NetJob(QObject::tr("Download of meta file %1").arg(localFilename()), APPLICATION->network()); + m_updateTask.reset(new NetJob(QObject::tr("Download of meta file %1").arg(localFilename()), APPLICATION->network())); auto url = this->url(); auto entry = APPLICATION->metacache()->resolveEntry("meta", localFilename()); entry->setStale(true); diff --git a/launcher/minecraft/AssetsUtils.cpp b/launcher/minecraft/AssetsUtils.cpp index 15062c2b..16fdfdb1 100644 --- a/launcher/minecraft/AssetsUtils.cpp +++ b/launcher/minecraft/AssetsUtils.cpp @@ -340,7 +340,7 @@ QString AssetObject::getRelPath() NetJob::Ptr AssetsIndex::getDownloadJob() { - auto job = new NetJob(QObject::tr("Assets for %1").arg(id), APPLICATION->network()); + auto job = makeShared(QObject::tr("Assets for %1").arg(id), APPLICATION->network()); for (auto &object : objects.values()) { auto dl = object.getDownloadAction(); diff --git a/launcher/minecraft/ComponentUpdateTask.cpp b/launcher/minecraft/ComponentUpdateTask.cpp index 6db21622..d55bc17f 100644 --- a/launcher/minecraft/ComponentUpdateTask.cpp +++ b/launcher/minecraft/ComponentUpdateTask.cpp @@ -572,7 +572,7 @@ void ComponentUpdateTask::resolveDependencies(bool checkOnly) // add stuff... for(auto &add: toAdd) { - ComponentPtr component = new Component(d->m_list, add.uid); + auto component = makeShared(d->m_list, add.uid); if(!add.equalsVersion.isEmpty()) { // exact version diff --git a/launcher/minecraft/MinecraftInstance.cpp b/launcher/minecraft/MinecraftInstance.cpp index d0a5ed31..8a814cbf 100644 --- a/launcher/minecraft/MinecraftInstance.cpp +++ b/launcher/minecraft/MinecraftInstance.cpp @@ -962,12 +962,12 @@ shared_qobject_ptr MinecraftInstance::createLaunchTask(AuthSessionPt // print a header { - process->appendStep(new TextPrint(pptr, "Minecraft folder is:\n" + gameRoot() + "\n\n", MessageLevel::Launcher)); + process->appendStep(makeShared(pptr, "Minecraft folder is:\n" + gameRoot() + "\n\n", MessageLevel::Launcher)); } // check java { - process->appendStep(new CheckJava(pptr)); + process->appendStep(makeShared(pptr)); } // check launch method @@ -975,13 +975,13 @@ shared_qobject_ptr MinecraftInstance::createLaunchTask(AuthSessionPt QString method = launchMethod(); if(!validMethods.contains(method)) { - process->appendStep(new TextPrint(pptr, "Selected launch method \"" + method + "\" is not valid.\n", MessageLevel::Fatal)); + process->appendStep(makeShared(pptr, "Selected launch method \"" + method + "\" is not valid.\n", MessageLevel::Fatal)); return process; } // create the .minecraft folder and server-resource-packs (workaround for Minecraft bug MCL-3732) { - process->appendStep(new CreateGameFolders(pptr)); + process->appendStep(makeShared(pptr)); } if (!serverToJoin && settings()->get("JoinServerOnLaunch").toBool()) @@ -993,7 +993,7 @@ shared_qobject_ptr MinecraftInstance::createLaunchTask(AuthSessionPt if(serverToJoin && serverToJoin->port == 25565) { // Resolve server address to join on launch - auto *step = new LookupServerAddress(pptr); + auto step = makeShared(pptr); step->setLookupAddress(serverToJoin->address); step->setOutputAddressPtr(serverToJoin); process->appendStep(step); @@ -1002,7 +1002,7 @@ shared_qobject_ptr MinecraftInstance::createLaunchTask(AuthSessionPt // run pre-launch command if that's needed if(getPreLaunchCommand().size()) { - auto step = new PreLaunchCommand(pptr); + auto step = makeShared(pptr); step->setWorkingDirectory(gameRoot()); process->appendStep(step); } @@ -1011,43 +1011,43 @@ shared_qobject_ptr MinecraftInstance::createLaunchTask(AuthSessionPt if(session->status != AuthSession::PlayableOffline) { if(!session->demo) { - process->appendStep(new ClaimAccount(pptr, session)); + process->appendStep(makeShared(pptr, session)); } - process->appendStep(new Update(pptr, Net::Mode::Online)); + process->appendStep(makeShared(pptr, Net::Mode::Online)); } else { - process->appendStep(new Update(pptr, Net::Mode::Offline)); + process->appendStep(makeShared(pptr, Net::Mode::Offline)); } // if there are any jar mods { - process->appendStep(new ModMinecraftJar(pptr)); + process->appendStep(makeShared(pptr)); } // Scan mods folders for mods { - process->appendStep(new ScanModFolders(pptr)); + process->appendStep(makeShared(pptr)); } // print some instance info here... { - process->appendStep(new PrintInstanceInfo(pptr, session, serverToJoin)); + process->appendStep(makeShared(pptr, session, serverToJoin)); } // extract native jars if needed { - process->appendStep(new ExtractNatives(pptr)); + process->appendStep(makeShared(pptr)); } // reconstruct assets if needed { - process->appendStep(new ReconstructAssets(pptr)); + process->appendStep(makeShared(pptr)); } // verify that minimum Java requirements are met { - process->appendStep(new VerifyJavaInstall(pptr)); + process->appendStep(makeShared(pptr)); } { @@ -1055,7 +1055,7 @@ shared_qobject_ptr MinecraftInstance::createLaunchTask(AuthSessionPt auto method = launchMethod(); if(method == "LauncherPart") { - auto step = new LauncherPartLaunch(pptr); + auto step = makeShared(pptr); step->setWorkingDirectory(gameRoot()); step->setAuthSession(session); step->setServerToJoin(serverToJoin); @@ -1063,7 +1063,7 @@ shared_qobject_ptr MinecraftInstance::createLaunchTask(AuthSessionPt } else if (method == "DirectJava") { - auto step = new DirectJavaLaunch(pptr); + auto step = makeShared(pptr); step->setWorkingDirectory(gameRoot()); step->setAuthSession(session); step->setServerToJoin(serverToJoin); @@ -1074,7 +1074,7 @@ shared_qobject_ptr MinecraftInstance::createLaunchTask(AuthSessionPt // run post-exit command if that's needed if(getPostExitCommand().size()) { - auto step = new PostLaunchCommand(pptr); + auto step = makeShared(pptr); step->setWorkingDirectory(gameRoot()); process->appendStep(step); } @@ -1084,8 +1084,7 @@ shared_qobject_ptr MinecraftInstance::createLaunchTask(AuthSessionPt } if(m_settings->get("QuitAfterGameStop").toBool()) { - auto step = new QuitAfterGameStop(pptr); - process->appendStep(step); + process->appendStep(makeShared(pptr)); } m_launchProcess = process; emit launchTaskChanged(m_launchProcess); diff --git a/launcher/minecraft/MinecraftUpdate.cpp b/launcher/minecraft/MinecraftUpdate.cpp index 3a3aa864..07ad4882 100644 --- a/launcher/minecraft/MinecraftUpdate.cpp +++ b/launcher/minecraft/MinecraftUpdate.cpp @@ -43,7 +43,7 @@ void MinecraftUpdate::executeTask() m_tasks.clear(); // create folders { - m_tasks.append(new FoldersTask(m_inst)); + m_tasks.append(makeShared(m_inst)); } // add metadata update task if necessary @@ -59,17 +59,17 @@ void MinecraftUpdate::executeTask() // libraries download { - m_tasks.append(new LibrariesTask(m_inst)); + m_tasks.append(makeShared(m_inst)); } // FML libraries download and copy into the instance { - m_tasks.append(new FMLLibrariesTask(m_inst)); + m_tasks.append(makeShared(m_inst)); } // assets update { - m_tasks.append(new AssetUpdateTask(m_inst)); + m_tasks.append(makeShared(m_inst)); } if(!m_preFailure.isEmpty()) diff --git a/launcher/minecraft/PackProfile.cpp b/launcher/minecraft/PackProfile.cpp index 42021b3c..da7c1d84 100644 --- a/launcher/minecraft/PackProfile.cpp +++ b/launcher/minecraft/PackProfile.cpp @@ -130,7 +130,7 @@ static ComponentPtr componentFromJsonV1(PackProfile * parent, const QString & co // critical auto uid = Json::requireString(obj.value("uid")); auto filePath = componentJsonPattern.arg(uid); - auto component = new Component(parent, uid); + auto component = makeShared(parent, uid); component->m_version = Json::ensureString(obj.value("version")); component->m_dependencyOnly = Json::ensureBoolean(obj.value("dependencyOnly"), false); component->m_important = Json::ensureBoolean(obj.value("important"), false); @@ -518,23 +518,23 @@ bool PackProfile::revertToBase(int index) return true; } -Component * PackProfile::getComponent(const QString &id) +ComponentPtr PackProfile::getComponent(const QString &id) { auto iter = d->componentIndex.find(id); if (iter == d->componentIndex.end()) { return nullptr; } - return (*iter).get(); + return (*iter); } -Component * PackProfile::getComponent(int index) +ComponentPtr PackProfile::getComponent(int index) { if(index < 0 || index >= d->components.size()) { return nullptr; } - return d->components[index].get(); + return d->components[index]; } QVariant PackProfile::data(const QModelIndex &index, int role) const @@ -765,7 +765,7 @@ bool PackProfile::installEmpty(const QString& uid, const QString& name) file.write(OneSixVersionFormat::versionFileToJson(f).toJson()); file.close(); - appendComponent(new Component(this, f->uid, f)); + appendComponent(makeShared(this, f->uid, f)); scheduleSave(); invalidateLaunchProfile(); return true; @@ -872,7 +872,7 @@ bool PackProfile::installJarMods_internal(QStringList filepaths) file.write(OneSixVersionFormat::versionFileToJson(f).toJson()); file.close(); - appendComponent(new Component(this, f->uid, f)); + appendComponent(makeShared(this, f->uid, f)); } scheduleSave(); invalidateLaunchProfile(); @@ -933,7 +933,7 @@ bool PackProfile::installCustomJar_internal(QString filepath) file.write(OneSixVersionFormat::versionFileToJson(f).toJson()); file.close(); - appendComponent(new Component(this, f->uid, f)); + appendComponent(makeShared(this, f->uid, f)); scheduleSave(); invalidateLaunchProfile(); @@ -989,7 +989,7 @@ bool PackProfile::installAgents_internal(QStringList filepaths) patchFile.write(OneSixVersionFormat::versionFileToJson(versionFile).toJson()); patchFile.close(); - appendComponent(new Component(this, versionFile->uid, versionFile)); + appendComponent(makeShared(this, versionFile->uid, versionFile)); } scheduleSave(); @@ -1038,7 +1038,7 @@ bool PackProfile::setComponentVersion(const QString& uid, const QString& version else { // add new - auto component = new Component(this, uid); + auto component = makeShared(this, uid); component->m_version = version; component->m_important = important; appendComponent(component); diff --git a/launcher/minecraft/PackProfile.h b/launcher/minecraft/PackProfile.h index 67b418f4..731cd0ba 100644 --- a/launcher/minecraft/PackProfile.h +++ b/launcher/minecraft/PackProfile.h @@ -136,10 +136,10 @@ signals: public: /// get the profile component by id - Component * getComponent(const QString &id); + ComponentPtr getComponent(const QString &id); /// get the profile component by index - Component * getComponent(int index); + ComponentPtr getComponent(int index); /// Add the component to the internal list of patches // todo(merged): is this the best approach diff --git a/launcher/minecraft/auth/MinecraftAccount.cpp b/launcher/minecraft/auth/MinecraftAccount.cpp index 73d570f1..48cf5d42 100644 --- a/launcher/minecraft/auth/MinecraftAccount.cpp +++ b/launcher/minecraft/auth/MinecraftAccount.cpp @@ -75,7 +75,7 @@ MinecraftAccountPtr MinecraftAccount::loadFromJsonV3(const QJsonObject& json) { MinecraftAccountPtr MinecraftAccount::createFromUsername(const QString &username) { - MinecraftAccountPtr account = new MinecraftAccount(); + auto account = makeShared(); account->data.type = AccountType::Mojang; account->data.yggdrasilToken.extra["userName"] = username; account->data.yggdrasilToken.extra["clientToken"] = QUuid::createUuid().toString().remove(QRegularExpression("[{}-]")); @@ -91,7 +91,7 @@ MinecraftAccountPtr MinecraftAccount::createBlankMSA() MinecraftAccountPtr MinecraftAccount::createOffline(const QString &username) { - MinecraftAccountPtr account = new MinecraftAccount(); + auto account = makeShared(); account->data.type = AccountType::Offline; account->data.yggdrasilToken.token = "offline"; account->data.yggdrasilToken.validity = Katabasis::Validity::Certain; diff --git a/launcher/minecraft/auth/flows/MSA.cpp b/launcher/minecraft/auth/flows/MSA.cpp index 416b8f2c..f1987e0c 100644 --- a/launcher/minecraft/auth/flows/MSA.cpp +++ b/launcher/minecraft/auth/flows/MSA.cpp @@ -10,28 +10,28 @@ #include "minecraft/auth/steps/GetSkinStep.h" MSASilent::MSASilent(AccountData* data, QObject* parent) : AuthFlow(data, parent) { - m_steps.append(new MSAStep(m_data, MSAStep::Action::Refresh)); - m_steps.append(new XboxUserStep(m_data)); - m_steps.append(new XboxAuthorizationStep(m_data, &m_data->xboxApiToken, "http://xboxlive.com", "Xbox")); - m_steps.append(new XboxAuthorizationStep(m_data, &m_data->mojangservicesToken, "rp://api.minecraftservices.com/", "Mojang")); - m_steps.append(new LauncherLoginStep(m_data)); - m_steps.append(new XboxProfileStep(m_data)); - m_steps.append(new EntitlementsStep(m_data)); - m_steps.append(new MinecraftProfileStep(m_data)); - m_steps.append(new GetSkinStep(m_data)); + m_steps.append(makeShared(m_data, MSAStep::Action::Refresh)); + m_steps.append(makeShared(m_data)); + m_steps.append(makeShared(m_data, &m_data->xboxApiToken, "http://xboxlive.com", "Xbox")); + m_steps.append(makeShared(m_data, &m_data->mojangservicesToken, "rp://api.minecraftservices.com/", "Mojang")); + m_steps.append(makeShared(m_data)); + m_steps.append(makeShared(m_data)); + m_steps.append(makeShared(m_data)); + m_steps.append(makeShared(m_data)); + m_steps.append(makeShared(m_data)); } MSAInteractive::MSAInteractive( AccountData* data, QObject* parent ) : AuthFlow(data, parent) { - m_steps.append(new MSAStep(m_data, MSAStep::Action::Login)); - m_steps.append(new XboxUserStep(m_data)); - m_steps.append(new XboxAuthorizationStep(m_data, &m_data->xboxApiToken, "http://xboxlive.com", "Xbox")); - m_steps.append(new XboxAuthorizationStep(m_data, &m_data->mojangservicesToken, "rp://api.minecraftservices.com/", "Mojang")); - m_steps.append(new LauncherLoginStep(m_data)); - m_steps.append(new XboxProfileStep(m_data)); - m_steps.append(new EntitlementsStep(m_data)); - m_steps.append(new MinecraftProfileStep(m_data)); - m_steps.append(new GetSkinStep(m_data)); + m_steps.append(makeShared(m_data, MSAStep::Action::Login)); + m_steps.append(makeShared(m_data)); + m_steps.append(makeShared(m_data, &m_data->xboxApiToken, "http://xboxlive.com", "Xbox")); + m_steps.append(makeShared(m_data, &m_data->mojangservicesToken, "rp://api.minecraftservices.com/", "Mojang")); + m_steps.append(makeShared(m_data)); + m_steps.append(makeShared(m_data)); + m_steps.append(makeShared(m_data)); + m_steps.append(makeShared(m_data)); + m_steps.append(makeShared(m_data)); } diff --git a/launcher/minecraft/auth/flows/Mojang.cpp b/launcher/minecraft/auth/flows/Mojang.cpp index b86b0936..5900ea98 100644 --- a/launcher/minecraft/auth/flows/Mojang.cpp +++ b/launcher/minecraft/auth/flows/Mojang.cpp @@ -9,10 +9,10 @@ MojangRefresh::MojangRefresh( AccountData *data, QObject *parent ) : AuthFlow(data, parent) { - m_steps.append(new YggdrasilStep(m_data, QString())); - m_steps.append(new MinecraftProfileStepMojang(m_data)); - m_steps.append(new MigrationEligibilityStep(m_data)); - m_steps.append(new GetSkinStep(m_data)); + m_steps.append(makeShared(m_data, QString())); + m_steps.append(makeShared(m_data)); + m_steps.append(makeShared(m_data)); + m_steps.append(makeShared(m_data)); } MojangLogin::MojangLogin( @@ -20,8 +20,8 @@ MojangLogin::MojangLogin( QString password, QObject *parent ): AuthFlow(data, parent), m_password(password) { - m_steps.append(new YggdrasilStep(m_data, m_password)); - m_steps.append(new MinecraftProfileStepMojang(m_data)); - m_steps.append(new MigrationEligibilityStep(m_data)); - m_steps.append(new GetSkinStep(m_data)); + m_steps.append(makeShared(m_data, m_password)); + m_steps.append(makeShared(m_data)); + m_steps.append(makeShared(m_data)); + m_steps.append(makeShared(m_data)); } diff --git a/launcher/minecraft/auth/flows/Offline.cpp b/launcher/minecraft/auth/flows/Offline.cpp index fc614a8c..d5c63271 100644 --- a/launcher/minecraft/auth/flows/Offline.cpp +++ b/launcher/minecraft/auth/flows/Offline.cpp @@ -6,12 +6,12 @@ OfflineRefresh::OfflineRefresh( AccountData *data, QObject *parent ) : AuthFlow(data, parent) { - m_steps.append(new OfflineStep(m_data)); + m_steps.append(makeShared(m_data)); } OfflineLogin::OfflineLogin( AccountData *data, QObject *parent ) : AuthFlow(data, parent) { - m_steps.append(new OfflineStep(m_data)); + m_steps.append(makeShared(m_data)); } diff --git a/launcher/minecraft/mod/ResourcePackFolderModel.cpp b/launcher/minecraft/mod/ResourcePackFolderModel.cpp index ebac707d..da4bd091 100644 --- a/launcher/minecraft/mod/ResourcePackFolderModel.cpp +++ b/launcher/minecraft/mod/ResourcePackFolderModel.cpp @@ -142,7 +142,7 @@ int ResourcePackFolderModel::columnCount(const QModelIndex& parent) const Task* ResourcePackFolderModel::createUpdateTask() { - return new BasicFolderLoadTask(m_dir, [](QFileInfo const& entry) { return new ResourcePack(entry); }); + return new BasicFolderLoadTask(m_dir, [](QFileInfo const& entry) { return makeShared(entry); }); } Task* ResourcePackFolderModel::createParseTask(Resource& resource) diff --git a/launcher/minecraft/mod/TexturePackFolderModel.cpp b/launcher/minecraft/mod/TexturePackFolderModel.cpp index 561f6202..5a32cfaf 100644 --- a/launcher/minecraft/mod/TexturePackFolderModel.cpp +++ b/launcher/minecraft/mod/TexturePackFolderModel.cpp @@ -43,7 +43,7 @@ TexturePackFolderModel::TexturePackFolderModel(const QString &dir) : ResourceFol Task* TexturePackFolderModel::createUpdateTask() { - return new BasicFolderLoadTask(m_dir, [](QFileInfo const& entry) { return new TexturePack(entry); }); + return new BasicFolderLoadTask(m_dir, [](QFileInfo const& entry) { return makeShared(entry); }); } Task* TexturePackFolderModel::createParseTask(Resource& resource) diff --git a/launcher/minecraft/mod/tasks/BasicFolderLoadTask.h b/launcher/minecraft/mod/tasks/BasicFolderLoadTask.h index 2fce2942..3ee7e2e0 100644 --- a/launcher/minecraft/mod/tasks/BasicFolderLoadTask.h +++ b/launcher/minecraft/mod/tasks/BasicFolderLoadTask.h @@ -26,11 +26,11 @@ class BasicFolderLoadTask : public Task { public: BasicFolderLoadTask(QDir dir) : Task(nullptr, false), m_dir(dir), m_result(new Result), m_thread_to_spawn_into(thread()) { - m_create_func = [](QFileInfo const& entry) -> Resource* { - return new Resource(entry); + m_create_func = [](QFileInfo const& entry) -> Resource::Ptr { + return makeShared(entry); }; } - BasicFolderLoadTask(QDir dir, std::function create_function) + BasicFolderLoadTask(QDir dir, std::function create_function) : Task(nullptr, false), m_dir(dir), m_result(new Result), m_create_func(std::move(create_function)), m_thread_to_spawn_into(thread()) {} @@ -65,7 +65,7 @@ private: std::atomic m_aborted = false; - std::function m_create_func; + std::function m_create_func; /** This is the thread in which we should put new mod objects */ QThread* m_thread_to_spawn_into; diff --git a/launcher/minecraft/mod/tasks/ModFolderLoadTask.cpp b/launcher/minecraft/mod/tasks/ModFolderLoadTask.cpp index 78ef4386..3677a1dc 100644 --- a/launcher/minecraft/mod/tasks/ModFolderLoadTask.cpp +++ b/launcher/minecraft/mod/tasks/ModFolderLoadTask.cpp @@ -72,14 +72,14 @@ void ModFolderLoadTask::executeTask() delete mod; } else { - m_result->mods[mod->internal_id()] = mod; + m_result->mods[mod->internal_id()].reset(std::move(mod)); m_result->mods[mod->internal_id()]->setStatus(ModStatus::NoMetadata); } } else { QString chopped_id = mod->internal_id().chopped(9); if (m_result->mods.contains(chopped_id)) { - m_result->mods[mod->internal_id()] = mod; + m_result->mods[mod->internal_id()].reset(std::move(mod)); auto metadata = m_result->mods[chopped_id]->metadata(); if (metadata) { @@ -90,7 +90,7 @@ void ModFolderLoadTask::executeTask() } } else { - m_result->mods[mod->internal_id()] = mod; + m_result->mods[mod->internal_id()].reset(std::move(mod)); m_result->mods[mod->internal_id()]->setStatus(ModStatus::NoMetadata); } } @@ -130,6 +130,6 @@ void ModFolderLoadTask::getFromMetadata() auto* mod = new Mod(m_mods_dir, metadata); mod->setStatus(ModStatus::NotInstalled); - m_result->mods[mod->internal_id()] = mod; + m_result->mods[mod->internal_id()].reset(std::move(mod)); } } diff --git a/launcher/minecraft/update/AssetUpdateTask.cpp b/launcher/minecraft/update/AssetUpdateTask.cpp index dd246665..8ccb0e1d 100644 --- a/launcher/minecraft/update/AssetUpdateTask.cpp +++ b/launcher/minecraft/update/AssetUpdateTask.cpp @@ -24,7 +24,7 @@ void AssetUpdateTask::executeTask() auto assets = profile->getMinecraftAssets(); QUrl indexUrl = assets->url; QString localPath = assets->id + ".json"; - auto job = new NetJob( + auto job = makeShared( tr("Asset index for %1").arg(m_inst->name()), APPLICATION->network() ); diff --git a/launcher/minecraft/update/FMLLibrariesTask.cpp b/launcher/minecraft/update/FMLLibrariesTask.cpp index 7a0bd2f3..96fd3ba3 100644 --- a/launcher/minecraft/update/FMLLibrariesTask.cpp +++ b/launcher/minecraft/update/FMLLibrariesTask.cpp @@ -61,7 +61,7 @@ void FMLLibrariesTask::executeTask() // download missing libs to our place setStatus(tr("Downloading FML libraries...")); - auto dljob = new NetJob("FML libraries", APPLICATION->network()); + NetJob::Ptr dljob{ new NetJob("FML libraries", APPLICATION->network()) }; auto metacache = APPLICATION->metacache(); Net::Download::Options options = Net::Download::Option::MakeEternal; for (auto &lib : fmlLibsToProcess) @@ -71,10 +71,10 @@ void FMLLibrariesTask::executeTask() dljob->addNetAction(Net::Download::makeCached(QUrl(urlString), entry, options)); } - connect(dljob, &NetJob::succeeded, this, &FMLLibrariesTask::fmllibsFinished); - connect(dljob, &NetJob::failed, this, &FMLLibrariesTask::fmllibsFailed); - connect(dljob, &NetJob::aborted, this, [this]{ emitFailed(tr("Aborted")); }); - connect(dljob, &NetJob::progress, this, &FMLLibrariesTask::progress); + connect(dljob.get(), &NetJob::succeeded, this, &FMLLibrariesTask::fmllibsFinished); + connect(dljob.get(), &NetJob::failed, this, &FMLLibrariesTask::fmllibsFailed); + connect(dljob.get(), &NetJob::aborted, this, [this]{ emitFailed(tr("Aborted")); }); + connect(dljob.get(), &NetJob::progress, this, &FMLLibrariesTask::progress); downloadJob.reset(dljob); downloadJob->start(); } diff --git a/launcher/minecraft/update/LibrariesTask.cpp b/launcher/minecraft/update/LibrariesTask.cpp index 33a575c2..b9410111 100644 --- a/launcher/minecraft/update/LibrariesTask.cpp +++ b/launcher/minecraft/update/LibrariesTask.cpp @@ -20,7 +20,7 @@ void LibrariesTask::executeTask() auto components = inst->getPackProfile(); auto profile = components->getProfile(); - auto job = new NetJob(tr("Libraries for instance %1").arg(inst->name()), APPLICATION->network()); + NetJob::Ptr job{ new NetJob(tr("Libraries for instance %1").arg(inst->name()), APPLICATION->network()) }; downloadJob.reset(job); auto metacache = APPLICATION->metacache(); diff --git a/launcher/modplatform/CheckUpdateTask.h b/launcher/modplatform/CheckUpdateTask.h index 932a62d9..f7582b8f 100644 --- a/launcher/modplatform/CheckUpdateTask.h +++ b/launcher/modplatform/CheckUpdateTask.h @@ -22,10 +22,10 @@ class CheckUpdateTask : public Task { QString new_version; QString changelog; ModPlatform::ResourceProvider provider; - ResourceDownloadTask* download; + shared_qobject_ptr download; public: - UpdatableMod(QString name, QString old_h, QString old_v, QString new_v, QString changelog, ModPlatform::ResourceProvider p, ResourceDownloadTask* t) + UpdatableMod(QString name, QString old_h, QString old_v, QString new_v, QString changelog, ModPlatform::ResourceProvider p, shared_qobject_ptr t) : name(name), old_hash(old_h), old_version(old_v), new_version(new_v), changelog(changelog), provider(p), download(t) {} }; diff --git a/launcher/modplatform/EnsureMetadataTask.cpp b/launcher/modplatform/EnsureMetadataTask.cpp index d9523052..34d969f0 100644 --- a/launcher/modplatform/EnsureMetadataTask.cpp +++ b/launcher/modplatform/EnsureMetadataTask.cpp @@ -32,7 +32,7 @@ EnsureMetadataTask::EnsureMetadataTask(Mod* mod, QDir dir, ModPlatform::Resource EnsureMetadataTask::EnsureMetadataTask(QList& mods, QDir dir, ModPlatform::ResourceProvider prov) : Task(nullptr), m_index_dir(dir), m_provider(prov), m_current_task(nullptr) { - m_hashing_task = new ConcurrentTask(this, "MakeHashesTask", 10); + m_hashing_task.reset(new ConcurrentTask(this, "MakeHashesTask", 10)); for (auto* mod : mods) { auto hash_task = createNewHash(mod); if (!hash_task) @@ -217,7 +217,7 @@ Task::Ptr EnsureMetadataTask::modrinthVersionsTask() // Prevents unfortunate timings when aborting the task if (!ver_task) - return {}; + return Task::Ptr{nullptr}; connect(ver_task.get(), &Task::succeeded, this, [this, response] { QJsonParseError parse_error{}; @@ -277,7 +277,7 @@ Task::Ptr EnsureMetadataTask::modrinthProjectsTask() // Prevents unfortunate timings when aborting the task if (!proj_task) - return {}; + return Task::Ptr{nullptr}; connect(proj_task.get(), &Task::succeeded, this, [this, response, addonIds] { QJsonParseError parse_error{}; @@ -434,7 +434,7 @@ Task::Ptr EnsureMetadataTask::flameProjectsTask() // Prevents unfortunate timings when aborting the task if (!proj_task) - return {}; + return Task::Ptr{nullptr}; connect(proj_task.get(), &Task::succeeded, this, [this, response, addonIds] { QJsonParseError parse_error{}; diff --git a/launcher/modplatform/EnsureMetadataTask.h b/launcher/modplatform/EnsureMetadataTask.h index 635f4a2b..03cae4e4 100644 --- a/launcher/modplatform/EnsureMetadataTask.h +++ b/launcher/modplatform/EnsureMetadataTask.h @@ -60,6 +60,6 @@ class EnsureMetadataTask : public Task { ModPlatform::ResourceProvider m_provider; QHash m_temp_versions; - ConcurrentTask* m_hashing_task; + ConcurrentTask::Ptr m_hashing_task; Task::Ptr m_current_task; }; diff --git a/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp b/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp index 291ad916..4bd8b7f2 100644 --- a/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp +++ b/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp @@ -81,16 +81,17 @@ bool PackInstallTask::abort() void PackInstallTask::executeTask() { qDebug() << "PackInstallTask::executeTask: " << QThread::currentThreadId(); - auto *netJob = new NetJob("ATLauncher::VersionFetch", APPLICATION->network()); + NetJob::Ptr netJob{ new NetJob("ATLauncher::VersionFetch", APPLICATION->network()) }; auto searchUrl = QString(BuildConfig.ATL_DOWNLOAD_SERVER_URL + "packs/%1/versions/%2/Configs.json") .arg(m_pack_safe_name).arg(m_version_name); netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchUrl), &response)); + + QObject::connect(netJob.get(), &NetJob::succeeded, this, &PackInstallTask::onDownloadSucceeded); + QObject::connect(netJob.get(), &NetJob::failed, this, &PackInstallTask::onDownloadFailed); + QObject::connect(netJob.get(), &NetJob::aborted, this, &PackInstallTask::onDownloadAborted); + jobPtr = netJob; jobPtr->start(); - - QObject::connect(netJob, &NetJob::succeeded, this, &PackInstallTask::onDownloadSucceeded); - QObject::connect(netJob, &NetJob::failed, this, &PackInstallTask::onDownloadFailed); - QObject::connect(netJob, &NetJob::aborted, this, &PackInstallTask::onDownloadAborted); } void PackInstallTask::onDownloadSucceeded() @@ -552,7 +553,7 @@ bool PackInstallTask::createLibrariesComponent(QString instanceRoot, std::shared file.write(OneSixVersionFormat::versionFileToJson(f).toJson()); file.close(); - profile->appendComponent(new Component(profile.get(), target_id, f)); + profile->appendComponent(ComponentPtr{ new Component(profile.get(), target_id, f) }); return true; } @@ -641,7 +642,7 @@ bool PackInstallTask::createPackComponent(QString instanceRoot, std::shared_ptr< file.write(OneSixVersionFormat::versionFileToJson(f).toJson()); file.close(); - profile->appendComponent(new Component(profile.get(), target_id, f)); + profile->appendComponent(ComponentPtr{ new Component(profile.get(), target_id, f) }); return true; } @@ -649,7 +650,7 @@ void PackInstallTask::installConfigs() { qDebug() << "PackInstallTask::installConfigs: " << QThread::currentThreadId(); setStatus(tr("Downloading configs...")); - jobPtr = new NetJob(tr("Config download"), APPLICATION->network()); + jobPtr.reset(new NetJob(tr("Config download"), APPLICATION->network())); auto path = QString("Configs/%1/%2.zip").arg(m_pack_safe_name).arg(m_version_name); auto url = QString(BuildConfig.ATL_DOWNLOAD_SERVER_URL + "packs/%1/versions/%2/Configs.zip") @@ -747,7 +748,7 @@ void PackInstallTask::downloadMods() setStatus(tr("Downloading mods...")); jarmods.clear(); - jobPtr = new NetJob(tr("Mod download"), APPLICATION->network()); + jobPtr.reset(new NetJob(tr("Mod download"), APPLICATION->network())); for(const auto& mod : m_version.mods) { // skip non-client mods if(!mod.client) continue; diff --git a/launcher/modplatform/flame/FileResolvingTask.cpp b/launcher/modplatform/flame/FileResolvingTask.cpp index 7f1beb1a..d3a737bb 100644 --- a/launcher/modplatform/flame/FileResolvingTask.cpp +++ b/launcher/modplatform/flame/FileResolvingTask.cpp @@ -23,7 +23,7 @@ void Flame::FileResolvingTask::executeTask() { setStatus(tr("Resolving mod IDs...")); setProgress(0, 3); - m_dljob = new NetJob("Mod id resolver", m_network); + m_dljob.reset(new NetJob("Mod id resolver", m_network)); result.reset(new QByteArray()); //build json data to send QJsonObject object; @@ -43,7 +43,7 @@ void Flame::FileResolvingTask::netJobFinished() { setProgress(1, 3); // job to check modrinth for blocked projects - m_checkJob = new NetJob("Modrinth check", m_network); + m_checkJob.reset(new NetJob("Modrinth check", m_network)); blockedProjects = QMap(); QJsonDocument doc; diff --git a/launcher/modplatform/flame/FlameAPI.cpp b/launcher/modplatform/flame/FlameAPI.cpp index 4b926ec3..5ef9a409 100644 --- a/launcher/modplatform/flame/FlameAPI.cpp +++ b/launcher/modplatform/flame/FlameAPI.cpp @@ -13,7 +13,7 @@ Task::Ptr FlameAPI::matchFingerprints(const QList& fingerprints, QByteArray* response) { - auto* netJob = new NetJob(QString("Flame::MatchFingerprints"), APPLICATION->network()); + auto netJob = makeShared(QString("Flame::MatchFingerprints"), APPLICATION->network()); QJsonObject body_obj; QJsonArray fingerprints_arr; @@ -28,7 +28,7 @@ Task::Ptr FlameAPI::matchFingerprints(const QList& fingerprints, QByteArra netJob->addNetAction(Net::Upload::makeByteArray(QString("https://api.curseforge.com/v1/fingerprints"), response, body_raw)); - QObject::connect(netJob, &NetJob::finished, [response] { delete response; }); + QObject::connect(netJob.get(), &NetJob::finished, [response] { delete response; }); return netJob; } @@ -173,7 +173,7 @@ auto FlameAPI::getLatestVersion(VersionSearchArgs&& args) -> ModPlatform::Indexe Task::Ptr FlameAPI::getProjects(QStringList addonIds, QByteArray* response) const { - auto* netJob = new NetJob(QString("Flame::GetProjects"), APPLICATION->network()); + auto netJob = makeShared(QString("Flame::GetProjects"), APPLICATION->network()); QJsonObject body_obj; QJsonArray addons_arr; @@ -188,15 +188,15 @@ Task::Ptr FlameAPI::getProjects(QStringList addonIds, QByteArray* response) cons netJob->addNetAction(Net::Upload::makeByteArray(QString("https://api.curseforge.com/v1/mods"), response, body_raw)); - QObject::connect(netJob, &NetJob::finished, [response] { delete response; }); - QObject::connect(netJob, &NetJob::failed, [body_raw] { qDebug() << body_raw; }); + QObject::connect(netJob.get(), &NetJob::finished, [response] { delete response; }); + QObject::connect(netJob.get(), &NetJob::failed, [body_raw] { qDebug() << body_raw; }); return netJob; } Task::Ptr FlameAPI::getFiles(const QStringList& fileIds, QByteArray* response) const { - auto* netJob = new NetJob(QString("Flame::GetFiles"), APPLICATION->network()); + auto netJob = makeShared(QString("Flame::GetFiles"), APPLICATION->network()); QJsonObject body_obj; QJsonArray files_arr; @@ -211,8 +211,8 @@ Task::Ptr FlameAPI::getFiles(const QStringList& fileIds, QByteArray* response) c netJob->addNetAction(Net::Upload::makeByteArray(QString("https://api.curseforge.com/v1/mods/files"), response, body_raw)); - QObject::connect(netJob, &NetJob::finished, [response] { delete response; }); - QObject::connect(netJob, &NetJob::failed, [body_raw] { qDebug() << body_raw; }); + QObject::connect(netJob.get(), &NetJob::finished, [response] { delete response; }); + QObject::connect(netJob.get(), &NetJob::failed, [body_raw] { qDebug() << body_raw; }); return netJob; } diff --git a/launcher/modplatform/flame/FlameCheckUpdate.cpp b/launcher/modplatform/flame/FlameCheckUpdate.cpp index 7aee4f4c..06a89502 100644 --- a/launcher/modplatform/flame/FlameCheckUpdate.cpp +++ b/launcher/modplatform/flame/FlameCheckUpdate.cpp @@ -172,7 +172,7 @@ void FlameCheckUpdate::executeTask() old_version = current_ver.version; } - auto download_task = new ResourceDownloadTask(pack, latest_ver, m_mods_folder); + auto download_task = makeShared(pack, latest_ver, m_mods_folder); m_updatable.emplace_back(pack.name, mod->metadata()->hash, old_version, latest_ver.version, api.getModFileChangelog(latest_ver.addonId.toInt(), latest_ver.fileId.toInt()), ModPlatform::ResourceProvider::FLAME, download_task); diff --git a/launcher/modplatform/flame/FlameInstanceCreationTask.cpp b/launcher/modplatform/flame/FlameInstanceCreationTask.cpp index 890bff48..964b559c 100644 --- a/launcher/modplatform/flame/FlameInstanceCreationTask.cpp +++ b/launcher/modplatform/flame/FlameInstanceCreationTask.cpp @@ -373,7 +373,7 @@ bool FlameCreationTask::createInstance() instance.setManagedPack("flame", m_managed_id, m_pack.name, m_managed_version_id, m_pack.version); instance.setName(name()); - m_mod_id_resolver = new Flame::FileResolvingTask(APPLICATION->network(), m_pack); + m_mod_id_resolver.reset(new Flame::FileResolvingTask(APPLICATION->network(), m_pack)); connect(m_mod_id_resolver.get(), &Flame::FileResolvingTask::succeeded, this, [this, &loop] { idResolverSucceeded(loop); }); connect(m_mod_id_resolver.get(), &Flame::FileResolvingTask::failed, [&](QString reason) { m_mod_id_resolver.reset(); @@ -452,7 +452,7 @@ void FlameCreationTask::idResolverSucceeded(QEventLoop& loop) void FlameCreationTask::setupDownloadJob(QEventLoop& loop) { - m_files_job = new NetJob(tr("Mod download"), APPLICATION->network()); + m_files_job.reset(new NetJob(tr("Mod download"), APPLICATION->network())); for (const auto& result : m_mod_id_resolver->getResults().files) { QString filename = result.fileName; if (!result.required) { diff --git a/launcher/modplatform/helpers/HashUtils.cpp b/launcher/modplatform/helpers/HashUtils.cpp index af484be0..2177ddad 100644 --- a/launcher/modplatform/helpers/HashUtils.cpp +++ b/launcher/modplatform/helpers/HashUtils.cpp @@ -28,22 +28,22 @@ Hasher::Ptr createHasher(QString file_path, ModPlatform::ResourceProvider provid Hasher::Ptr createModrinthHasher(QString file_path) { - return new ModrinthHasher(file_path); + return makeShared(file_path); } Hasher::Ptr createFlameHasher(QString file_path) { - return new FlameHasher(file_path); + return makeShared(file_path); } Hasher::Ptr createBlockedModHasher(QString file_path, ModPlatform::ResourceProvider provider) { - return new BlockedModHasher(file_path, provider); + return makeShared(file_path, provider); } Hasher::Ptr createBlockedModHasher(QString file_path, ModPlatform::ResourceProvider provider, QString type) { - auto hasher = new BlockedModHasher(file_path, provider); + auto hasher = makeShared(file_path, provider); hasher->useHashType(type); return hasher; } diff --git a/launcher/modplatform/helpers/NetworkResourceAPI.cpp b/launcher/modplatform/helpers/NetworkResourceAPI.cpp index ac994c31..010ac15e 100644 --- a/launcher/modplatform/helpers/NetworkResourceAPI.cpp +++ b/launcher/modplatform/helpers/NetworkResourceAPI.cpp @@ -20,11 +20,11 @@ Task::Ptr NetworkResourceAPI::searchProjects(SearchArgs&& args, SearchCallbacks& auto search_url = search_url_optional.value(); auto response = new QByteArray(); - auto netJob = new NetJob(QString("%1::Search").arg(debugName()), APPLICATION->network()); + auto netJob = makeShared(QString("%1::Search").arg(debugName()), APPLICATION->network()); netJob->addNetAction(Net::Download::makeByteArray(QUrl(search_url), response)); - QObject::connect(netJob, &NetJob::succeeded, [=]{ + QObject::connect(netJob.get(), &NetJob::succeeded, [=]{ QJsonParseError parse_error{}; QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); if (parse_error.error != QJsonParseError::NoError) { @@ -40,14 +40,14 @@ Task::Ptr NetworkResourceAPI::searchProjects(SearchArgs&& args, SearchCallbacks& callbacks.on_succeed(doc); }); - QObject::connect(netJob, &NetJob::failed, [=](QString reason){ + QObject::connect(netJob.get(), &NetJob::failed, [=](QString reason){ int network_error_code = -1; if (auto* failed_action = netJob->getFailedActions().at(0); failed_action && failed_action->m_reply) network_error_code = failed_action->m_reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); callbacks.on_fail(reason, network_error_code); }); - QObject::connect(netJob, &NetJob::aborted, [=]{ + QObject::connect(netJob.get(), &NetJob::aborted, [=]{ callbacks.on_abort(); }); @@ -83,12 +83,12 @@ Task::Ptr NetworkResourceAPI::getProjectVersions(VersionSearchArgs&& args, Versi auto versions_url = versions_url_optional.value(); - auto netJob = new NetJob(QString("%1::Versions").arg(args.pack.name), APPLICATION->network()); + auto netJob = makeShared(QString("%1::Versions").arg(args.pack.name), APPLICATION->network()); auto response = new QByteArray(); netJob->addNetAction(Net::Download::makeByteArray(versions_url, response)); - QObject::connect(netJob, &NetJob::succeeded, [=] { + QObject::connect(netJob.get(), &NetJob::succeeded, [=] { QJsonParseError parse_error{}; QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); if (parse_error.error != QJsonParseError::NoError) { @@ -101,7 +101,7 @@ Task::Ptr NetworkResourceAPI::getProjectVersions(VersionSearchArgs&& args, Versi callbacks.on_succeed(doc, args.pack); }); - QObject::connect(netJob, &NetJob::finished, [response] { + QObject::connect(netJob.get(), &NetJob::finished, [response] { delete response; }); @@ -116,11 +116,11 @@ Task::Ptr NetworkResourceAPI::getProject(QString addonId, QByteArray* response) auto project_url = project_url_optional.value(); - auto netJob = new NetJob(QString("%1::GetProject").arg(addonId), APPLICATION->network()); + auto netJob = makeShared(QString("%1::GetProject").arg(addonId), APPLICATION->network()); netJob->addNetAction(Net::Download::makeByteArray(QUrl(project_url), response)); - QObject::connect(netJob, &NetJob::finished, [response] { + QObject::connect(netJob.get(), &NetJob::finished, [response] { delete response; }); diff --git a/launcher/modplatform/legacy_ftb/PackFetchTask.cpp b/launcher/modplatform/legacy_ftb/PackFetchTask.cpp index 36aa60c7..e8768c5c 100644 --- a/launcher/modplatform/legacy_ftb/PackFetchTask.cpp +++ b/launcher/modplatform/legacy_ftb/PackFetchTask.cpp @@ -47,7 +47,7 @@ void PackFetchTask::fetch() publicPacks.clear(); thirdPartyPacks.clear(); - jobPtr = new NetJob("LegacyFTB::ModpackFetch", m_network); + jobPtr.reset(new NetJob("LegacyFTB::ModpackFetch", m_network)); QUrl publicPacksUrl = QUrl(BuildConfig.LEGACY_FTB_CDN_BASE_URL + "static/modpacks.xml"); qDebug() << "Downloading public version info from" << publicPacksUrl.toString(); diff --git a/launcher/modplatform/legacy_ftb/PackInstallTask.cpp b/launcher/modplatform/legacy_ftb/PackInstallTask.cpp index 06b3788b..8d45fc5c 100644 --- a/launcher/modplatform/legacy_ftb/PackInstallTask.cpp +++ b/launcher/modplatform/legacy_ftb/PackInstallTask.cpp @@ -69,7 +69,7 @@ void PackInstallTask::downloadPack() archivePath = QString("%1/%2/%3").arg(m_pack.dir, m_version.replace(".", "_"), m_pack.file); - netJobContainer = new NetJob("Download FTB Pack", m_network); + netJobContainer.reset(new NetJob("Download FTB Pack", m_network)); QString url; if (m_pack.type == PackType::Private) { url = QString(BuildConfig.LEGACY_FTB_CDN_BASE_URL + "privatepacks/%1").arg(archivePath); diff --git a/launcher/modplatform/modpacksch/FTBPackInstallTask.cpp b/launcher/modplatform/modpacksch/FTBPackInstallTask.cpp index 2979663d..68d4751c 100644 --- a/launcher/modplatform/modpacksch/FTBPackInstallTask.cpp +++ b/launcher/modplatform/modpacksch/FTBPackInstallTask.cpp @@ -87,15 +87,15 @@ void PackInstallTask::executeTask() auto version = *version_it; - auto* netJob = new NetJob("ModpacksCH::VersionFetch", APPLICATION->network()); + auto netJob = makeShared("ModpacksCH::VersionFetch", APPLICATION->network()); auto searchUrl = QString(BuildConfig.MODPACKSCH_API_BASE_URL + "public/modpack/%1/%2").arg(m_pack.id).arg(version.id); netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchUrl), &m_response)); - QObject::connect(netJob, &NetJob::succeeded, this, &PackInstallTask::onManifestDownloadSucceeded); - QObject::connect(netJob, &NetJob::failed, this, &PackInstallTask::onManifestDownloadFailed); - QObject::connect(netJob, &NetJob::aborted, this, &PackInstallTask::abort); - QObject::connect(netJob, &NetJob::progress, this, &PackInstallTask::setProgress); + QObject::connect(netJob.get(), &NetJob::succeeded, this, &PackInstallTask::onManifestDownloadSucceeded); + QObject::connect(netJob.get(), &NetJob::failed, this, &PackInstallTask::onManifestDownloadFailed); + QObject::connect(netJob.get(), &NetJob::aborted, this, &PackInstallTask::abort); + QObject::connect(netJob.get(), &NetJob::progress, this, &PackInstallTask::setProgress); m_net_job = netJob; @@ -162,7 +162,7 @@ void PackInstallTask::resolveMods() index++; } - m_mod_id_resolver_task = new Flame::FileResolvingTask(APPLICATION->network(), manifest); + m_mod_id_resolver_task.reset(new Flame::FileResolvingTask(APPLICATION->network(), manifest)); connect(m_mod_id_resolver_task.get(), &Flame::FileResolvingTask::succeeded, this, &PackInstallTask::onResolveModsSucceeded); connect(m_mod_id_resolver_task.get(), &Flame::FileResolvingTask::failed, this, &PackInstallTask::onResolveModsFailed); @@ -294,7 +294,7 @@ void PackInstallTask::downloadPack() setStatus(tr("Downloading mods...")); setAbortable(false); - auto* jobPtr = new NetJob(tr("Mod download"), APPLICATION->network()); + auto jobPtr = makeShared(tr("Mod download"), APPLICATION->network()); for (auto const& file : m_version.files) { if (file.serverOnly || file.url.isEmpty()) continue; @@ -313,10 +313,10 @@ void PackInstallTask::downloadPack() jobPtr->addNetAction(dl); } - connect(jobPtr, &NetJob::succeeded, this, &PackInstallTask::onModDownloadSucceeded); - connect(jobPtr, &NetJob::failed, this, &PackInstallTask::onModDownloadFailed); - connect(jobPtr, &NetJob::aborted, this, &PackInstallTask::abort); - connect(jobPtr, &NetJob::progress, this, &PackInstallTask::setProgress); + connect(jobPtr.get(), &NetJob::succeeded, this, &PackInstallTask::onModDownloadSucceeded); + connect(jobPtr.get(), &NetJob::failed, this, &PackInstallTask::onModDownloadFailed); + connect(jobPtr.get(), &NetJob::aborted, this, &PackInstallTask::abort); + connect(jobPtr.get(), &NetJob::progress, this, &PackInstallTask::setProgress); m_net_job = jobPtr; diff --git a/launcher/modplatform/modrinth/ModrinthAPI.cpp b/launcher/modplatform/modrinth/ModrinthAPI.cpp index 5a16113d..29e3d129 100644 --- a/launcher/modplatform/modrinth/ModrinthAPI.cpp +++ b/launcher/modplatform/modrinth/ModrinthAPI.cpp @@ -11,19 +11,19 @@ Task::Ptr ModrinthAPI::currentVersion(QString hash, QString hash_format, QByteArray* response) { - auto* netJob = new NetJob(QString("Modrinth::GetCurrentVersion"), APPLICATION->network()); + auto netJob = makeShared(QString("Modrinth::GetCurrentVersion"), APPLICATION->network()); netJob->addNetAction(Net::Download::makeByteArray( QString(BuildConfig.MODRINTH_PROD_URL + "/version_file/%1?algorithm=%2").arg(hash, hash_format), response)); - QObject::connect(netJob, &NetJob::finished, [response] { delete response; }); + QObject::connect(netJob.get(), &NetJob::finished, [response] { delete response; }); return netJob; } Task::Ptr ModrinthAPI::currentVersions(const QStringList& hashes, QString hash_format, QByteArray* response) { - auto* netJob = new NetJob(QString("Modrinth::GetCurrentVersions"), APPLICATION->network()); + auto netJob = makeShared(QString("Modrinth::GetCurrentVersions"), APPLICATION->network()); QJsonObject body_obj; @@ -35,7 +35,7 @@ Task::Ptr ModrinthAPI::currentVersions(const QStringList& hashes, QString hash_f netJob->addNetAction(Net::Upload::makeByteArray(QString(BuildConfig.MODRINTH_PROD_URL + "/version_files"), response, body_raw)); - QObject::connect(netJob, &NetJob::finished, [response] { delete response; }); + QObject::connect(netJob.get(), &NetJob::finished, [response] { delete response; }); return netJob; } @@ -46,7 +46,7 @@ Task::Ptr ModrinthAPI::latestVersion(QString hash, std::optional loaders, QByteArray* response) { - auto* netJob = new NetJob(QString("Modrinth::GetLatestVersion"), APPLICATION->network()); + auto netJob = makeShared(QString("Modrinth::GetLatestVersion"), APPLICATION->network()); QJsonObject body_obj; @@ -67,7 +67,7 @@ Task::Ptr ModrinthAPI::latestVersion(QString hash, netJob->addNetAction(Net::Upload::makeByteArray( QString(BuildConfig.MODRINTH_PROD_URL + "/version_file/%1/update?algorithm=%2").arg(hash, hash_format), response, body_raw)); - QObject::connect(netJob, &NetJob::finished, [response] { delete response; }); + QObject::connect(netJob.get(), &NetJob::finished, [response] { delete response; }); return netJob; } @@ -78,7 +78,7 @@ Task::Ptr ModrinthAPI::latestVersions(const QStringList& hashes, std::optional loaders, QByteArray* response) { - auto* netJob = new NetJob(QString("Modrinth::GetLatestVersions"), APPLICATION->network()); + auto netJob = makeShared(QString("Modrinth::GetLatestVersions"), APPLICATION->network()); QJsonObject body_obj; @@ -101,21 +101,20 @@ Task::Ptr ModrinthAPI::latestVersions(const QStringList& hashes, netJob->addNetAction(Net::Upload::makeByteArray(QString(BuildConfig.MODRINTH_PROD_URL + "/version_files/update"), response, body_raw)); - QObject::connect(netJob, &NetJob::finished, [response] { delete response; }); + QObject::connect(netJob.get(), &NetJob::finished, [response] { delete response; }); return netJob; } Task::Ptr ModrinthAPI::getProjects(QStringList addonIds, QByteArray* response) const { - auto netJob = new NetJob(QString("Modrinth::GetProjects"), APPLICATION->network()); + auto netJob = makeShared(QString("Modrinth::GetProjects"), APPLICATION->network()); auto searchUrl = getMultipleModInfoURL(addonIds); netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchUrl), response)); - QObject::connect(netJob, &NetJob::finished, [response, netJob] { + QObject::connect(netJob.get(), &NetJob::finished, [response, netJob] { delete response; - netJob->deleteLater(); }); return netJob; diff --git a/launcher/modplatform/modrinth/ModrinthCheckUpdate.cpp b/launcher/modplatform/modrinth/ModrinthCheckUpdate.cpp index daca68d7..d1be7209 100644 --- a/launcher/modplatform/modrinth/ModrinthCheckUpdate.cpp +++ b/launcher/modplatform/modrinth/ModrinthCheckUpdate.cpp @@ -159,7 +159,7 @@ void ModrinthCheckUpdate::executeTask() pack.description = mod->description(); pack.provider = ModPlatform::ResourceProvider::MODRINTH; - auto download_task = new ResourceDownloadTask(pack, project_ver, m_mods_folder); + auto download_task = makeShared(pack, project_ver, m_mods_folder); m_updatable.emplace_back(pack.name, hash, mod->version(), project_ver.version_number, project_ver.changelog, ModPlatform::ResourceProvider::MODRINTH, download_task); diff --git a/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.cpp b/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.cpp index c5a27c9d..94c0bf77 100644 --- a/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.cpp +++ b/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.cpp @@ -223,7 +223,7 @@ bool ModrinthCreationTask::createInstance() instance.setName(name()); instance.saveNow(); - m_files_job = new NetJob(tr("Mod download"), APPLICATION->network()); + m_files_job.reset(new NetJob(tr("Mod download"), APPLICATION->network())); for (auto file : m_files) { auto path = FS::PathCombine(m_stagingPath, ".minecraft", file.path); diff --git a/launcher/modplatform/technic/SingleZipPackInstallTask.cpp b/launcher/modplatform/technic/SingleZipPackInstallTask.cpp index 6438d9ef..8fd43d21 100644 --- a/launcher/modplatform/technic/SingleZipPackInstallTask.cpp +++ b/launcher/modplatform/technic/SingleZipPackInstallTask.cpp @@ -44,7 +44,7 @@ void Technic::SingleZipPackInstallTask::executeTask() const QString path = m_sourceUrl.host() + '/' + m_sourceUrl.path(); auto entry = APPLICATION->metacache()->resolveEntry("general", path); entry->setStale(true); - m_filesNetJob = new NetJob(tr("Modpack download"), APPLICATION->network()); + m_filesNetJob.reset(new NetJob(tr("Modpack download"), APPLICATION->network())); m_filesNetJob->addNetAction(Net::Download::makeCached(m_sourceUrl, entry)); m_archivePath = entry->getFullPath(); auto job = m_filesNetJob.get(); @@ -130,7 +130,7 @@ void Technic::SingleZipPackInstallTask::extractFinished() } } - shared_qobject_ptr packProcessor = new Technic::TechnicPackProcessor(); + auto packProcessor = makeShared(); connect(packProcessor.get(), &Technic::TechnicPackProcessor::succeeded, this, &Technic::SingleZipPackInstallTask::emitSucceeded); connect(packProcessor.get(), &Technic::TechnicPackProcessor::failed, this, &Technic::SingleZipPackInstallTask::emitFailed); packProcessor->run(m_globalSettings, name(), m_instIcon, m_stagingPath, m_minecraftVersion); diff --git a/launcher/modplatform/technic/SolderPackInstallTask.cpp b/launcher/modplatform/technic/SolderPackInstallTask.cpp index 19731b38..77c503f0 100644 --- a/launcher/modplatform/technic/SolderPackInstallTask.cpp +++ b/launcher/modplatform/technic/SolderPackInstallTask.cpp @@ -70,7 +70,7 @@ void Technic::SolderPackInstallTask::executeTask() { setStatus(tr("Resolving modpack files")); - m_filesNetJob = new NetJob(tr("Resolving modpack files"), m_network); + m_filesNetJob.reset(new NetJob(tr("Resolving modpack files"), m_network)); auto sourceUrl = QString("%1/modpack/%2/%3").arg(m_solderUrl.toString(), m_pack, m_version); m_filesNetJob->addNetAction(Net::Download::makeByteArray(sourceUrl, &m_response)); @@ -107,7 +107,7 @@ void Technic::SolderPackInstallTask::fileListSucceeded() if (!build.minecraft.isEmpty()) m_minecraftVersion = build.minecraft; - m_filesNetJob = new NetJob(tr("Downloading modpack"), m_network); + m_filesNetJob.reset(new NetJob(tr("Downloading modpack"), m_network)); int i = 0; for (const auto &mod : build.mods) { @@ -219,7 +219,7 @@ void Technic::SolderPackInstallTask::extractFinished() } } - shared_qobject_ptr packProcessor = new Technic::TechnicPackProcessor(); + auto packProcessor = makeShared(); connect(packProcessor.get(), &Technic::TechnicPackProcessor::succeeded, this, &Technic::SolderPackInstallTask::emitSucceeded); connect(packProcessor.get(), &Technic::TechnicPackProcessor::failed, this, &Technic::SolderPackInstallTask::emitFailed); packProcessor->run(m_globalSettings, name(), m_instIcon, m_stagingPath, m_minecraftVersion, true); diff --git a/launcher/net/Download.cpp b/launcher/net/Download.cpp index fd3dbedc..5982c8c9 100644 --- a/launcher/net/Download.cpp +++ b/launcher/net/Download.cpp @@ -49,14 +49,9 @@ namespace Net { -Download::Download() : NetAction() -{ - m_state = State::Inactive; -} - auto Download::makeCached(QUrl url, MetaEntryPtr entry, Options options) -> Download::Ptr { - auto* dl = new Download(); + auto dl = makeShared(); dl->m_url = url; dl->m_options = options; auto md5Node = new ChecksumValidator(QCryptographicHash::Md5); @@ -67,7 +62,7 @@ auto Download::makeCached(QUrl url, MetaEntryPtr entry, Options options) -> Down auto Download::makeByteArray(QUrl url, QByteArray* output, Options options) -> Download::Ptr { - auto* dl = new Download(); + auto dl = makeShared(); dl->m_url = url; dl->m_options = options; dl->m_sink.reset(new ByteArraySink(output)); @@ -76,7 +71,7 @@ auto Download::makeByteArray(QUrl url, QByteArray* output, Options options) -> D auto Download::makeFile(QUrl url, QString path, Options options) -> Download::Ptr { - auto* dl = new Download(); + auto dl = makeShared(); dl->m_url = url; dl->m_options = options; dl->m_sink.reset(new FileSink(path)); diff --git a/launcher/net/Download.h b/launcher/net/Download.h index 3faa5db5..7e1df322 100644 --- a/launcher/net/Download.h +++ b/launcher/net/Download.h @@ -52,9 +52,6 @@ class Download : public NetAction { enum class Option { NoOptions = 0, AcceptLocalFiles = 1, MakeEternal = 2 }; Q_DECLARE_FLAGS(Options, Option) - protected: - explicit Download(); - public: ~Download() override = default; diff --git a/launcher/net/Upload.cpp b/launcher/net/Upload.cpp index f3b19022..79b6af8d 100644 --- a/launcher/net/Upload.cpp +++ b/launcher/net/Upload.cpp @@ -233,7 +233,7 @@ namespace Net { } Upload::Ptr Upload::makeByteArray(QUrl url, QByteArray *output, QByteArray m_post_data) { - auto* up = new Upload(); + auto up = makeShared(); up->m_url = std::move(url); up->m_sink.reset(new ByteArraySink(output)); up->m_post_data = std::move(m_post_data); diff --git a/launcher/net/Upload.h b/launcher/net/Upload.h index 7c194bbc..5a0b2e74 100644 --- a/launcher/net/Upload.h +++ b/launcher/net/Upload.h @@ -45,6 +45,8 @@ namespace Net { Q_OBJECT public: + using Ptr = shared_qobject_ptr; + static Upload::Ptr makeByteArray(QUrl url, QByteArray *output, QByteArray m_post_data); auto abort() -> bool override; auto canAbort() const -> bool override { return true; }; diff --git a/launcher/news/NewsChecker.cpp b/launcher/news/NewsChecker.cpp index 3b969732..1f1520d0 100644 --- a/launcher/news/NewsChecker.cpp +++ b/launcher/news/NewsChecker.cpp @@ -57,10 +57,10 @@ void NewsChecker::reloadNews() qDebug() << "Reloading news."; - NetJob* job = new NetJob("News RSS Feed", m_network); + NetJob::Ptr job{ new NetJob("News RSS Feed", m_network) }; job->addNetAction(Net::Download::makeByteArray(m_feedUrl, &newsData)); - QObject::connect(job, &NetJob::succeeded, this, &NewsChecker::rssDownloadFinished); - QObject::connect(job, &NetJob::failed, this, &NewsChecker::rssDownloadFailed); + QObject::connect(job.get(), &NetJob::succeeded, this, &NewsChecker::rssDownloadFinished); + QObject::connect(job.get(), &NetJob::failed, this, &NewsChecker::rssDownloadFailed); m_newsNetJob.reset(job); job->start(); } diff --git a/launcher/tasks/ConcurrentTask.h b/launcher/tasks/ConcurrentTask.h index b46919fb..d074d2e2 100644 --- a/launcher/tasks/ConcurrentTask.h +++ b/launcher/tasks/ConcurrentTask.h @@ -8,6 +8,8 @@ class ConcurrentTask : public Task { Q_OBJECT public: + using Ptr = shared_qobject_ptr; + explicit ConcurrentTask(QObject* parent = nullptr, QString task_name = "", int max_concurrent = 6); ~ConcurrentTask() override; diff --git a/launcher/translations/TranslationsModel.cpp b/launcher/translations/TranslationsModel.cpp index 38f48296..46db4804 100644 --- a/launcher/translations/TranslationsModel.cpp +++ b/launcher/translations/TranslationsModel.cpp @@ -670,7 +670,7 @@ void TranslationsModel::downloadIndex() return; } qDebug() << "Downloading Translations Index..."; - d->m_index_job = new NetJob("Translations Index", APPLICATION->network()); + d->m_index_job.reset(new NetJob("Translations Index", APPLICATION->network())); MetaEntryPtr entry = APPLICATION->metacache()->resolveEntry("translations", "index_v2.json"); entry->setStale(true); d->m_index_task = Net::Download::makeCached(QUrl(BuildConfig.TRANSLATIONS_BASE_URL + "index_v2.json"), entry); @@ -722,7 +722,7 @@ void TranslationsModel::downloadTranslation(QString key) dl->addValidator(new Net::ChecksumValidator(QCryptographicHash::Sha1, rawHash)); dl->setProgress(dl->getProgress(), lang->file_size); - d->m_dl_job = new NetJob("Translation for " + key, APPLICATION->network()); + d->m_dl_job.reset(new NetJob("Translation for " + key, APPLICATION->network())); d->m_dl_job->addNetAction(dl); connect(d->m_dl_job.get(), &NetJob::succeeded, this, &TranslationsModel::dlGood); diff --git a/launcher/ui/dialogs/ModUpdateDialog.cpp b/launcher/ui/dialogs/ModUpdateDialog.cpp index 4ef42d6c..8618b924 100644 --- a/launcher/ui/dialogs/ModUpdateDialog.cpp +++ b/launcher/ui/dialogs/ModUpdateDialog.cpp @@ -88,15 +88,15 @@ void ModUpdateDialog::checkCandidates() SequentialTask check_task(m_parent, tr("Checking for updates")); if (!m_modrinth_to_update.empty()) { - m_modrinth_check_task = new ModrinthCheckUpdate(m_modrinth_to_update, versions, loaders, m_mod_model); - connect(m_modrinth_check_task, &CheckUpdateTask::checkFailed, this, + 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}); }); check_task.addTask(m_modrinth_check_task); } if (!m_flame_to_update.empty()) { - m_flame_check_task = new FlameCheckUpdate(m_flame_to_update, versions, loaders, m_mod_model); - connect(m_flame_check_task, &CheckUpdateTask::checkFailed, this, + 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}); }); check_task.addTask(m_flame_check_task); } @@ -266,9 +266,9 @@ auto ModUpdateDialog::ensureMetadata() -> bool } if (!modrinth_tmp.empty()) { - auto* modrinth_task = new EnsureMetadataTask(modrinth_tmp, index_dir, ModPlatform::ResourceProvider::MODRINTH); - connect(modrinth_task, &EnsureMetadataTask::metadataReady, [this](Mod* candidate) { onMetadataEnsured(candidate); }); - connect(modrinth_task, &EnsureMetadataTask::metadataFailed, [this, &should_try_others](Mod* candidate) { + auto modrinth_task = makeShared(modrinth_tmp, index_dir, ModPlatform::ResourceProvider::MODRINTH); + connect(modrinth_task.get(), &EnsureMetadataTask::metadataReady, [this](Mod* candidate) { onMetadataEnsured(candidate); }); + connect(modrinth_task.get(), &EnsureMetadataTask::metadataFailed, [this, &should_try_others](Mod* candidate) { onMetadataFailed(candidate, should_try_others.find(candidate->internal_id()).value(), ModPlatform::ResourceProvider::MODRINTH); }); @@ -279,9 +279,9 @@ auto ModUpdateDialog::ensureMetadata() -> bool } if (!flame_tmp.empty()) { - auto* flame_task = new EnsureMetadataTask(flame_tmp, index_dir, ModPlatform::ResourceProvider::FLAME); - connect(flame_task, &EnsureMetadataTask::metadataReady, [this](Mod* candidate) { onMetadataEnsured(candidate); }); - connect(flame_task, &EnsureMetadataTask::metadataFailed, [this, &should_try_others](Mod* candidate) { + auto flame_task = makeShared(flame_tmp, index_dir, ModPlatform::ResourceProvider::FLAME); + connect(flame_task.get(), &EnsureMetadataTask::metadataReady, [this](Mod* candidate) { onMetadataEnsured(candidate); }); + connect(flame_task.get(), &EnsureMetadataTask::metadataFailed, [this, &should_try_others](Mod* candidate) { onMetadataFailed(candidate, should_try_others.find(candidate->internal_id()).value(), ModPlatform::ResourceProvider::FLAME); }); @@ -334,9 +334,9 @@ void ModUpdateDialog::onMetadataFailed(Mod* mod, bool try_others, ModPlatform::R if (try_others) { auto index_dir = indexDir(); - auto* task = new EnsureMetadataTask(mod, index_dir, next(first_choice)); - connect(task, &EnsureMetadataTask::metadataReady, [this](Mod* candidate) { onMetadataEnsured(candidate); }); - connect(task, &EnsureMetadataTask::metadataFailed, [this](Mod* candidate) { onMetadataFailed(candidate, false); }); + auto task = makeShared(mod, index_dir, next(first_choice)); + connect(task.get(), &EnsureMetadataTask::metadataReady, [this](Mod* candidate) { onMetadataEnsured(candidate); }); + connect(task.get(), &EnsureMetadataTask::metadataFailed, [this](Mod* candidate) { onMetadataFailed(candidate, false); }); m_second_try_metadata->addTask(task); } else { @@ -388,9 +388,9 @@ void ModUpdateDialog::appendMod(CheckUpdateTask::UpdatableMod const& info) ui->modTreeWidget->addTopLevelItem(item_top); } -auto ModUpdateDialog::getTasks() -> const QList +auto ModUpdateDialog::getTasks() -> const QList { - QList list; + QList list; auto* item = ui->modTreeWidget->topLevelItem(0); diff --git a/launcher/ui/dialogs/ModUpdateDialog.h b/launcher/ui/dialogs/ModUpdateDialog.h index 3e3dd90d..1a92f613 100644 --- a/launcher/ui/dialogs/ModUpdateDialog.h +++ b/launcher/ui/dialogs/ModUpdateDialog.h @@ -25,7 +25,7 @@ class ModUpdateDialog final : public ReviewMessageBox { void appendMod(const CheckUpdateTask::UpdatableMod& info); - const QList getTasks(); + const QList getTasks(); auto indexDir() const -> QDir { return m_mod_model->indexDir(); } auto noUpdates() const -> bool { return m_no_updates; }; @@ -41,8 +41,8 @@ class ModUpdateDialog final : public ReviewMessageBox { private: QWidget* m_parent; - ModrinthCheckUpdate* m_modrinth_check_task = nullptr; - FlameCheckUpdate* m_flame_check_task = nullptr; + shared_qobject_ptr m_modrinth_check_task; + shared_qobject_ptr m_flame_check_task; const std::shared_ptr m_mod_model; @@ -50,11 +50,11 @@ class ModUpdateDialog final : public ReviewMessageBox { QList m_modrinth_to_update; QList m_flame_to_update; - ConcurrentTask* m_second_try_metadata; + ConcurrentTask::Ptr m_second_try_metadata; QList> m_failed_metadata; QList> m_failed_check_update; - QHash m_tasks; + QHash m_tasks; BaseInstance* m_instance; bool m_no_updates = false; diff --git a/launcher/ui/dialogs/ResourceDownloadDialog.cpp b/launcher/ui/dialogs/ResourceDownloadDialog.cpp index b9367c16..fa829bfb 100644 --- a/launcher/ui/dialogs/ResourceDownloadDialog.cpp +++ b/launcher/ui/dialogs/ResourceDownloadDialog.cpp @@ -147,7 +147,7 @@ void ResourceDownloadDialog::addResource(ModPlatform::IndexedPack& pack, ModPlat removeResource(pack, ver); ver.is_currently_selected = true; - m_selected.insert(pack.name, new ResourceDownloadTask(pack, ver, getBaseModel(), is_indexed)); + m_selected.insert(pack.name, makeShared(pack, ver, getBaseModel(), is_indexed)); m_buttons.button(QDialogButtonBox::Ok)->setEnabled(!m_selected.isEmpty()); } diff --git a/launcher/ui/pages/instance/VersionPage.cpp b/launcher/ui/pages/instance/VersionPage.cpp index d200652a..94315395 100644 --- a/launcher/ui/pages/instance/VersionPage.cpp +++ b/launcher/ui/pages/instance/VersionPage.cpp @@ -660,7 +660,7 @@ void VersionPage::onGameUpdateError(QString error) CustomMessageBox::selectable(this, tr("Error updating instance"), error, QMessageBox::Warning)->show(); } -Component * VersionPage::current() +ComponentPtr VersionPage::current() { auto row = currentRow(); if(row < 0) diff --git a/launcher/ui/pages/instance/VersionPage.h b/launcher/ui/pages/instance/VersionPage.h index 166f36bb..183bad9a 100644 --- a/launcher/ui/pages/instance/VersionPage.h +++ b/launcher/ui/pages/instance/VersionPage.h @@ -99,7 +99,7 @@ private slots: void updateVersionControls(); private: - Component * current(); + ComponentPtr current(); int currentRow(); void updateButtons(int row = -1); void preselect(int row = 0); diff --git a/launcher/ui/pages/modplatform/ResourceModel.cpp b/launcher/ui/pages/modplatform/ResourceModel.cpp index 8af70104..db7d26f8 100644 --- a/launcher/ui/pages/modplatform/ResourceModel.cpp +++ b/launcher/ui/pages/modplatform/ResourceModel.cpp @@ -265,7 +265,7 @@ std::optional ResourceModel::getIcon(QModelIndex& index, const QUrl& url) return { pixmap }; if (!m_current_icon_job) - m_current_icon_job = new NetJob("IconJob", APPLICATION->network()); + m_current_icon_job.reset(new NetJob("IconJob", APPLICATION->network())); if (m_currently_running_icon_actions.contains(url)) return {}; diff --git a/launcher/ui/pages/modplatform/atlauncher/AtlListModel.cpp b/launcher/ui/pages/modplatform/atlauncher/AtlListModel.cpp index 2ce04068..9ad26f47 100644 --- a/launcher/ui/pages/modplatform/atlauncher/AtlListModel.cpp +++ b/launcher/ui/pages/modplatform/atlauncher/AtlListModel.cpp @@ -86,14 +86,14 @@ void ListModel::request() modpacks.clear(); endResetModel(); - auto *netJob = new NetJob("Atl::Request", APPLICATION->network()); + auto netJob = makeShared("Atl::Request", APPLICATION->network()); auto url = QString(BuildConfig.ATL_DOWNLOAD_SERVER_URL + "launcher/json/packsnew.json"); netJob->addNetAction(Net::Download::makeByteArray(QUrl(url), &response)); jobPtr = netJob; jobPtr->start(); - QObject::connect(netJob, &NetJob::succeeded, this, &ListModel::requestFinished); - QObject::connect(netJob, &NetJob::failed, this, &ListModel::requestFailed); + QObject::connect(netJob.get(), &NetJob::succeeded, this, &ListModel::requestFinished); + QObject::connect(netJob.get(), &NetJob::failed, this, &ListModel::requestFailed); } void ListModel::requestFinished() diff --git a/launcher/ui/pages/modplatform/flame/FlameModel.cpp b/launcher/ui/pages/modplatform/flame/FlameModel.cpp index 127c3de5..5961ea02 100644 --- a/launcher/ui/pages/modplatform/flame/FlameModel.cpp +++ b/launcher/ui/pages/modplatform/flame/FlameModel.cpp @@ -155,7 +155,7 @@ void ListModel::fetchMore(const QModelIndex& parent) void ListModel::performPaginatedSearch() { - NetJob* netJob = new NetJob("Flame::Search", APPLICATION->network()); + auto netJob = makeShared("Flame::Search", APPLICATION->network()); auto searchUrl = QString( "https://api.curseforge.com/v1/mods/search?" "gameId=432&" @@ -172,8 +172,8 @@ void ListModel::performPaginatedSearch() netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchUrl), &response)); jobPtr = netJob; jobPtr->start(); - QObject::connect(netJob, &NetJob::succeeded, this, &ListModel::searchRequestFinished); - QObject::connect(netJob, &NetJob::failed, this, &ListModel::searchRequestFailed); + QObject::connect(netJob.get(), &NetJob::succeeded, this, &ListModel::searchRequestFinished); + QObject::connect(netJob.get(), &NetJob::failed, this, &ListModel::searchRequestFailed); } void ListModel::searchWithTerm(const QString& term, int sort) diff --git a/launcher/ui/pages/modplatform/ftb/FtbListModel.cpp b/launcher/ui/pages/modplatform/ftb/FtbListModel.cpp index ce2b2b18..e8065415 100644 --- a/launcher/ui/pages/modplatform/ftb/FtbListModel.cpp +++ b/launcher/ui/pages/modplatform/ftb/FtbListModel.cpp @@ -109,14 +109,14 @@ void ListModel::request() modpacks.clear(); endResetModel(); - auto *netJob = new NetJob("Ftb::Request", APPLICATION->network()); + auto netJob = makeShared("Ftb::Request", APPLICATION->network()); auto url = QString(BuildConfig.MODPACKSCH_API_BASE_URL + "public/modpack/all"); netJob->addNetAction(Net::Download::makeByteArray(QUrl(url), &response)); jobPtr = netJob; jobPtr->start(); - QObject::connect(netJob, &NetJob::succeeded, this, &ListModel::requestFinished); - QObject::connect(netJob, &NetJob::failed, this, &ListModel::requestFailed); + QObject::connect(netJob.get(), &NetJob::succeeded, this, &ListModel::requestFinished); + QObject::connect(netJob.get(), &NetJob::failed, this, &ListModel::requestFailed); } void ListModel::abortRequest() @@ -158,14 +158,14 @@ void ListModel::requestFailed(QString reason) void ListModel::requestPack() { - auto *netJob = new NetJob("Ftb::Search", APPLICATION->network()); + auto netJob = makeShared("Ftb::Search", APPLICATION->network()); auto searchUrl = QString(BuildConfig.MODPACKSCH_API_BASE_URL + "public/modpack/%1").arg(currentPack); netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchUrl), &response)); jobPtr = netJob; jobPtr->start(); - QObject::connect(netJob, &NetJob::succeeded, this, &ListModel::packRequestFinished); - QObject::connect(netJob, &NetJob::failed, this, &ListModel::packRequestFailed); + QObject::connect(netJob.get(), &NetJob::succeeded, this, &ListModel::packRequestFinished); + QObject::connect(netJob.get(), &NetJob::failed, this, &ListModel::packRequestFailed); } void ListModel::packRequestFinished() @@ -281,16 +281,16 @@ void ListModel::requestLogo(QString logo, QString url) bool stale = entry->isStale(); - NetJob *job = new NetJob(QString("ModpacksCH Icon Download %1").arg(logo), APPLICATION->network()); + auto job = makeShared(QString("ModpacksCH Icon Download %1").arg(logo), APPLICATION->network()); job->addNetAction(Net::Download::makeCached(QUrl(url), entry)); auto fullPath = entry->getFullPath(); - QObject::connect(job, &NetJob::finished, this, [this, logo, fullPath, stale] + QObject::connect(job.get(), &NetJob::finished, this, [this, logo, fullPath, stale] { logoLoaded(logo, stale); }); - QObject::connect(job, &NetJob::failed, this, [this, logo] + QObject::connect(job.get(), &NetJob::failed, this, [this, logo] { logoFailed(logo); }); diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp index 80850b4c..346a00b0 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp @@ -127,7 +127,7 @@ bool ModpackListModel::setData(const QModelIndex &index, const QVariant &value, void ModpackListModel::performPaginatedSearch() { // TODO: Move to standalone API - NetJob* netJob = new NetJob("Modrinth::SearchModpack", APPLICATION->network()); + auto netJob = makeShared("Modrinth::SearchModpack", APPLICATION->network()); auto searchAllUrl = QString(BuildConfig.MODRINTH_PROD_URL + "/search?" "offset=%1&" @@ -142,7 +142,7 @@ void ModpackListModel::performPaginatedSearch() netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchAllUrl), &m_all_response)); - QObject::connect(netJob, &NetJob::succeeded, this, [this] { + QObject::connect(netJob.get(), &NetJob::succeeded, this, [this] { QJsonParseError parse_error_all{}; QJsonDocument doc_all = QJsonDocument::fromJson(m_all_response, &parse_error_all); @@ -155,7 +155,7 @@ void ModpackListModel::performPaginatedSearch() searchRequestFinished(doc_all); }); - QObject::connect(netJob, &NetJob::failed, this, &ModpackListModel::searchRequestFailed); + QObject::connect(netJob.get(), &NetJob::failed, this, &ModpackListModel::searchRequestFailed); jobPtr = netJob; jobPtr->start(); diff --git a/launcher/ui/pages/modplatform/technic/TechnicModel.cpp b/launcher/ui/pages/modplatform/technic/TechnicModel.cpp index b2af1ac0..50f0c72d 100644 --- a/launcher/ui/pages/modplatform/technic/TechnicModel.cpp +++ b/launcher/ui/pages/modplatform/technic/TechnicModel.cpp @@ -112,7 +112,7 @@ void Technic::ListModel::searchWithTerm(const QString& term) void Technic::ListModel::performSearch() { - NetJob *netJob = new NetJob("Technic::Search", APPLICATION->network()); + auto netJob = makeShared("Technic::Search", APPLICATION->network()); QString searchUrl = ""; if (currentSearchTerm.isEmpty()) { searchUrl = QString("%1trending?build=%2") @@ -137,8 +137,8 @@ void Technic::ListModel::performSearch() netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchUrl), &response)); jobPtr = netJob; jobPtr->start(); - QObject::connect(netJob, &NetJob::succeeded, this, &ListModel::searchRequestFinished); - QObject::connect(netJob, &NetJob::failed, this, &ListModel::searchRequestFailed); + QObject::connect(netJob.get(), &NetJob::succeeded, this, &ListModel::searchRequestFinished); + QObject::connect(netJob.get(), &NetJob::failed, this, &ListModel::searchRequestFailed); } void Technic::ListModel::searchRequestFinished() diff --git a/launcher/ui/pages/modplatform/technic/TechnicPage.cpp b/launcher/ui/pages/modplatform/technic/TechnicPage.cpp index b15af244..859da97e 100644 --- a/launcher/ui/pages/modplatform/technic/TechnicPage.cpp +++ b/launcher/ui/pages/modplatform/technic/TechnicPage.cpp @@ -141,10 +141,10 @@ void TechnicPage::suggestCurrent() return; } - NetJob *netJob = new NetJob(QString("Technic::PackMeta(%1)").arg(current.name), APPLICATION->network()); + auto netJob = makeShared(QString("Technic::PackMeta(%1)").arg(current.name), APPLICATION->network()); QString slug = current.slug; netJob->addNetAction(Net::Download::makeByteArray(QString("%1modpack/%2?build=%3").arg(BuildConfig.TECHNIC_API_BASE_URL, slug, BuildConfig.TECHNIC_API_BUILD), &response)); - QObject::connect(netJob, &NetJob::succeeded, this, [this, slug] + QObject::connect(netJob.get(), &NetJob::succeeded, this, [this, slug] { jobPtr.reset(); @@ -247,11 +247,11 @@ void TechnicPage::metadataLoaded() // version so we can display something quicker ui->versionSelectionBox->addItem(current.currentVersion); - auto* netJob = new NetJob(QString("Technic::SolderMeta(%1)").arg(current.name), APPLICATION->network()); + auto netJob = makeShared(QString("Technic::SolderMeta(%1)").arg(current.name), APPLICATION->network()); auto url = QString("%1/modpack/%2").arg(current.url, current.slug); netJob->addNetAction(Net::Download::makeByteArray(QUrl(url), &response)); - QObject::connect(netJob, &NetJob::succeeded, this, &TechnicPage::onSolderLoaded); + QObject::connect(netJob.get(), &NetJob::succeeded, this, &TechnicPage::onSolderLoaded); jobPtr = netJob; jobPtr->start(); diff --git a/tests/DummyResourceAPI.h b/tests/DummyResourceAPI.h index e91be96c..0cc90958 100644 --- a/tests/DummyResourceAPI.h +++ b/tests/DummyResourceAPI.h @@ -36,12 +36,11 @@ class DummyResourceAPI : public ResourceAPI { [[nodiscard]] Task::Ptr searchProjects(SearchArgs&&, SearchCallbacks&& callbacks) const override { - auto task = new SearchTask; - QObject::connect(task, &Task::succeeded, [=] { + auto task = makeShared(); + QObject::connect(task.get(), &Task::succeeded, [=] { auto json = searchRequestResult(); callbacks.on_succeed(json); }); - QObject::connect(task, &Task::finished, task, &Task::deleteLater); return task; } }; diff --git a/tests/Task_test.cpp b/tests/Task_test.cpp index 6649b724..558cd2c0 100644 --- a/tests/Task_test.cpp +++ b/tests/Task_test.cpp @@ -49,10 +49,10 @@ class BigConcurrentTask : public QThread { // NOTE: Arbitrary value that manages to trigger a problem when there is one. static const unsigned s_num_tasks = 1 << 14; - auto sub_tasks = new BasicTask*[s_num_tasks]; + auto sub_tasks = new BasicTask::Ptr[s_num_tasks]; for (unsigned i = 0; i < s_num_tasks; i++) { - sub_tasks[i] = new BasicTask(false); + sub_tasks[i] = makeShared(false); big_task.addTask(sub_tasks[i]); } @@ -119,21 +119,21 @@ class TaskTest : public QObject { } void test_basicConcurrentRun(){ - BasicTask t1; - BasicTask t2; - BasicTask t3; + auto t1 = makeShared(); + auto t2 = makeShared(); + auto t3 = makeShared(); ConcurrentTask t; - t.addTask(&t1); - t.addTask(&t2); - t.addTask(&t3); + t.addTask(t1); + t.addTask(t2); + t.addTask(t3); QObject::connect(&t, &Task::finished, [&]{ QVERIFY2(t.wasSuccessful(), "Task finished but was not successful when it should have been."); - QVERIFY(t1.wasSuccessful()); - QVERIFY(t2.wasSuccessful()); - QVERIFY(t3.wasSuccessful()); + QVERIFY(t1->wasSuccessful()); + QVERIFY(t2->wasSuccessful()); + QVERIFY(t3->wasSuccessful()); }); t.start(); @@ -144,31 +144,39 @@ class TaskTest : public QObject { // Tests if starting new tasks after the 6 initial ones is working void test_moreConcurrentRun(){ - BasicTask t1, t2, t3, t4, t5, t6, t7, t8, t9; + auto t1 = makeShared(); + auto t2 = makeShared(); + auto t3 = makeShared(); + auto t4 = makeShared(); + auto t5 = makeShared(); + auto t6 = makeShared(); + auto t7 = makeShared(); + auto t8 = makeShared(); + auto t9 = makeShared(); ConcurrentTask t; - t.addTask(&t1); - t.addTask(&t2); - t.addTask(&t3); - t.addTask(&t4); - t.addTask(&t5); - t.addTask(&t6); - t.addTask(&t7); - t.addTask(&t8); - t.addTask(&t9); + t.addTask(t1); + t.addTask(t2); + t.addTask(t3); + t.addTask(t4); + t.addTask(t5); + t.addTask(t6); + t.addTask(t7); + t.addTask(t8); + t.addTask(t9); QObject::connect(&t, &Task::finished, [&]{ QVERIFY2(t.wasSuccessful(), "Task finished but was not successful when it should have been."); - QVERIFY(t1.wasSuccessful()); - QVERIFY(t2.wasSuccessful()); - QVERIFY(t3.wasSuccessful()); - QVERIFY(t4.wasSuccessful()); - QVERIFY(t5.wasSuccessful()); - QVERIFY(t6.wasSuccessful()); - QVERIFY(t7.wasSuccessful()); - QVERIFY(t8.wasSuccessful()); - QVERIFY(t9.wasSuccessful()); + QVERIFY(t1->wasSuccessful()); + QVERIFY(t2->wasSuccessful()); + QVERIFY(t3->wasSuccessful()); + QVERIFY(t4->wasSuccessful()); + QVERIFY(t5->wasSuccessful()); + QVERIFY(t6->wasSuccessful()); + QVERIFY(t7->wasSuccessful()); + QVERIFY(t8->wasSuccessful()); + QVERIFY(t9->wasSuccessful()); }); t.start(); @@ -178,21 +186,21 @@ class TaskTest : public QObject { } void test_basicSequentialRun(){ - BasicTask t1; - BasicTask t2; - BasicTask t3; + auto t1 = makeShared(); + auto t2 = makeShared(); + auto t3 = makeShared(); SequentialTask t; - t.addTask(&t1); - t.addTask(&t2); - t.addTask(&t3); + t.addTask(t1); + t.addTask(t2); + t.addTask(t3); QObject::connect(&t, &Task::finished, [&]{ QVERIFY2(t.wasSuccessful(), "Task finished but was not successful when it should have been."); - QVERIFY(t1.wasSuccessful()); - QVERIFY(t2.wasSuccessful()); - QVERIFY(t3.wasSuccessful()); + QVERIFY(t1->wasSuccessful()); + QVERIFY(t2->wasSuccessful()); + QVERIFY(t3->wasSuccessful()); }); t.start(); @@ -202,21 +210,21 @@ class TaskTest : public QObject { } void test_basicMultipleOptionsRun(){ - BasicTask t1; - BasicTask t2; - BasicTask t3; + auto t1 = makeShared(); + auto t2 = makeShared(); + auto t3 = makeShared(); MultipleOptionsTask t; - t.addTask(&t1); - t.addTask(&t2); - t.addTask(&t3); + t.addTask(t1); + t.addTask(t2); + t.addTask(t3); QObject::connect(&t, &Task::finished, [&]{ QVERIFY2(t.wasSuccessful(), "Task finished but was not successful when it should have been."); - QVERIFY(t1.wasSuccessful()); - QVERIFY(!t2.wasSuccessful()); - QVERIFY(!t3.wasSuccessful()); + QVERIFY(t1->wasSuccessful()); + QVERIFY(!t2->wasSuccessful()); + QVERIFY(!t3->wasSuccessful()); }); t.start(); From 4d2b5c2f42a34888ad26700461deb8c4e6f7b28c Mon Sep 17 00:00:00 2001 From: leo78913 Date: Thu, 26 Jan 2023 19:48:21 -0300 Subject: [PATCH 137/152] refactor: clean up some MainWindow stuff this makes the accounts button and menubar item share the same QMenu and also refactors some code Signed-off-by: leo78913 --- launcher/ui/MainWindow.cpp | 57 +++++++++++++------------------------- launcher/ui/MainWindow.h | 2 -- launcher/ui/MainWindow.ui | 6 ++++ 3 files changed, 26 insertions(+), 39 deletions(-) diff --git a/launcher/ui/MainWindow.cpp b/launcher/ui/MainWindow.cpp index a51cd55f..9bc0d61f 100644 --- a/launcher/ui/MainWindow.cpp +++ b/launcher/ui/MainWindow.cpp @@ -189,15 +189,19 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWi } - // set the menu for the folders and help tool buttons + // set the menu for the folders help, and accounts tool buttons { auto foldersMenuButton = dynamic_cast(ui->mainToolBar->widgetForAction(ui->actionFoldersButton)); - foldersMenuButton->setMenu(ui->foldersMenu); + ui->actionFoldersButton->setMenu(ui->foldersMenu); foldersMenuButton->setPopupMode(QToolButton::InstantPopup); helpMenuButton = dynamic_cast(ui->mainToolBar->widgetForAction(ui->actionHelpButton)); - helpMenuButton->setMenu(ui->helpMenu); + ui->actionHelpButton->setMenu(ui->helpMenu); helpMenuButton->setPopupMode(QToolButton::InstantPopup); + + auto accountMenuButton = dynamic_cast(ui->mainToolBar->widgetForAction(ui->actionAccountsButton)); + ui->actionAccountsButton->setMenu(ui->accountsMenu); + accountMenuButton->setPopupMode(QToolButton::InstantPopup); } // hide, disable and show stuff @@ -209,9 +213,8 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWi ui->actionCheckUpdate->setVisible(BuildConfig.UPDATER_ENABLED); +#ifndef Q_OS_MAC ui->actionAddToPATH->setVisible(false); -#ifdef Q_OS_MAC - ui->actionAddToPATH->setVisible(true); #endif // disabled until we have an instance selected @@ -338,16 +341,11 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWi spacer->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); ui->mainToolBar->insertWidget(ui->actionAccountsButton, spacer); - accountMenu = new QMenu(this); // Use undocumented property... https://stackoverflow.com/questions/7121718/create-a-scrollbar-in-a-submenu-qt - accountMenu->setStyleSheet("QMenu { menu-scrollable: 1; }"); + ui->accountsMenu->setStyleSheet("QMenu { menu-scrollable: 1; }"); repopulateAccountsMenu(); - accountMenuButton = dynamic_cast(ui->mainToolBar->widgetForAction(ui->actionAccountsButton)); - accountMenuButton->setMenu(accountMenu); - accountMenuButton->setPopupMode(QToolButton::InstantPopup); - // Update the menu when the active account changes. // Shouldn't have to use lambdas here like this, but if I don't, the compiler throws a fit. // Template hell sucks... @@ -434,10 +432,10 @@ void MainWindow::retranslateUi() MinecraftAccountPtr defaultAccount = accounts->defaultAccount(); if(defaultAccount) { auto profileLabel = profileInUseFilter(defaultAccount->profileName(), defaultAccount->isInUse()); - accountMenuButton->setText(profileLabel); + ui->actionAccountsButton->setText(profileLabel); } else { - accountMenuButton->setText(tr("Accounts")); + ui->actionAccountsButton->setText(tr("Accounts")); } if (m_selectedInstance) { @@ -687,7 +685,6 @@ void MainWindow::updateThemeMenu() void MainWindow::repopulateAccountsMenu() { - accountMenu->clear(); ui->accountsMenu->clear(); auto accounts = APPLICATION->accounts(); @@ -697,18 +694,16 @@ void MainWindow::repopulateAccountsMenu() if (defaultAccount) { // this can be called before accountMenuButton exists - if (accountMenuButton) + if (ui->actionAccountsButton) { auto profileLabel = profileInUseFilter(defaultAccount->profileName(), defaultAccount->isInUse()); - accountMenuButton->setText(profileLabel); + ui->actionAccountsButton->setText(profileLabel); } } if (accounts->count() <= 0) { - ui->actionNoAccountsAdded->setText(tr("No accounts added!")); ui->actionNoAccountsAdded->setEnabled(false); - accountMenu->addAction(ui->actionNoAccountsAdded); ui->accountsMenu->addAction(ui->actionNoAccountsAdded); } else @@ -740,33 +735,21 @@ void MainWindow::repopulateAccountsMenu() action->setShortcut(QKeySequence(tr("Ctrl+%1").arg(i + 1))); } - accountMenu->addAction(action); ui->accountsMenu->addAction(action); connect(action, SIGNAL(triggered(bool)), SLOT(changeActiveAccount())); } } - accountMenu->addSeparator(); ui->accountsMenu->addSeparator(); - ui->actionNoDefaultAccount = new QAction(this); - ui->actionNoDefaultAccount->setObjectName(QStringLiteral("actionNoDefaultAccount")); - ui->actionNoDefaultAccount->setText(tr("No Default Account")); - ui->actionNoDefaultAccount->setCheckable(true); - ui->actionNoDefaultAccount->setIcon(APPLICATION->getThemedIcon("noaccount")); ui->actionNoDefaultAccount->setData(-1); - ui->actionNoDefaultAccount->setShortcut(QKeySequence(tr("Ctrl+0"))); - if (!defaultAccount) { - ui->actionNoDefaultAccount->setChecked(true); - } + ui->actionNoDefaultAccount->setChecked(!defaultAccount); - accountMenu->addAction(ui->actionNoDefaultAccount); ui->accountsMenu->addAction(ui->actionNoDefaultAccount); + connect(ui->actionNoDefaultAccount, SIGNAL(triggered(bool)), SLOT(changeActiveAccount())); - accountMenu->addSeparator(); ui->accountsMenu->addSeparator(); - accountMenu->addAction(ui->actionManageAccounts); ui->accountsMenu->addAction(ui->actionManageAccounts); } @@ -811,20 +794,20 @@ void MainWindow::defaultAccountChanged() if (account && account->profileName() != "") { auto profileLabel = profileInUseFilter(account->profileName(), account->isInUse()); - accountMenuButton->setText(profileLabel); + ui->actionAccountsButton->setText(profileLabel); auto face = account->getFace(); if(face.isNull()) { - accountMenuButton->setIcon(APPLICATION->getThemedIcon("noaccount")); + ui->actionAccountsButton->setIcon(APPLICATION->getThemedIcon("noaccount")); } else { - accountMenuButton->setIcon(face); + ui->actionAccountsButton->setIcon(face); } return; } // Set the icon to the "no account" icon. - accountMenuButton->setIcon(APPLICATION->getThemedIcon("noaccount")); - accountMenuButton->setText(tr("Accounts")); + ui->actionAccountsButton->setIcon(APPLICATION->getThemedIcon("noaccount")); + ui->actionAccountsButton->setText(tr("Accounts")); } bool MainWindow::eventFilter(QObject *obj, QEvent *ev) diff --git a/launcher/ui/MainWindow.h b/launcher/ui/MainWindow.h index fab21a8f..56ecf575 100644 --- a/launcher/ui/MainWindow.h +++ b/launcher/ui/MainWindow.h @@ -239,10 +239,8 @@ private: QToolButton *newsLabel = nullptr; QLabel *m_statusLeft = nullptr; QLabel *m_statusCenter = nullptr; - QMenu *accountMenu = nullptr; LabeledToolButton *changeIconButton = nullptr; LabeledToolButton *renameButton = nullptr; - QToolButton *accountMenuButton = nullptr; QToolButton *helpMenuButton = nullptr; KonamiCode * secretEventFilter = nullptr; diff --git a/launcher/ui/MainWindow.ui b/launcher/ui/MainWindow.ui index 42f70996..3967709a 100644 --- a/launcher/ui/MainWindow.ui +++ b/launcher/ui/MainWindow.ui @@ -487,6 +487,9 @@ + + true + .. @@ -494,6 +497,9 @@ No Default Account + + Ctrl+0 + From 357b6ee99169277755f9da41ff2683c58853d07c Mon Sep 17 00:00:00 2001 From: leo78913 Date: Fri, 27 Jan 2023 12:35:41 -0300 Subject: [PATCH 138/152] Update launcher/ui/MainWindow.ui Co-authored-by: flow Signed-off-by: leo78913 --- launcher/ui/MainWindow.ui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/launcher/ui/MainWindow.ui b/launcher/ui/MainWindow.ui index 3967709a..a328a92f 100644 --- a/launcher/ui/MainWindow.ui +++ b/launcher/ui/MainWindow.ui @@ -336,7 +336,7 @@ &Kill - Kill the running instance + Kill the running instance. Ctrl+K From d5a0d4b452360a148d2bead883726ab2de75d974 Mon Sep 17 00:00:00 2001 From: leo78913 Date: Fri, 27 Jan 2023 12:35:53 -0300 Subject: [PATCH 139/152] Update launcher/ui/MainWindow.ui Co-authored-by: flow Signed-off-by: leo78913 --- launcher/ui/MainWindow.ui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/launcher/ui/MainWindow.ui b/launcher/ui/MainWindow.ui index a328a92f..c9c9af94 100644 --- a/launcher/ui/MainWindow.ui +++ b/launcher/ui/MainWindow.ui @@ -578,7 +578,7 @@ &Matrix Space - Open %1 Matrix space + Open %1 Matrix space. From df8df41621f5ca0dd3fd7100918d689183289b1e Mon Sep 17 00:00:00 2001 From: leo78913 Date: Fri, 27 Jan 2023 12:40:27 -0300 Subject: [PATCH 140/152] Remove unused BarEntry variable Signed-off-by: leo78913 --- launcher/ui/widgets/WideBar.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/launcher/ui/widgets/WideBar.cpp b/launcher/ui/widgets/WideBar.cpp index 540d599d..ffc2dfd1 100644 --- a/launcher/ui/widgets/WideBar.cpp +++ b/launcher/ui/widgets/WideBar.cpp @@ -132,8 +132,7 @@ void WideBar::insertWidgetBefore(QAction* before, QWidget* widget) if (iter == m_entries.end()) return; - BarEntry entry; - entry.bar_action = insertWidget(iter->bar_action, widget); + insertWidget(iter->bar_action, widget); } void WideBar::insertSpacer(QAction* action) From a27564ed70861c0b6676e870c2965332fbd2bf45 Mon Sep 17 00:00:00 2001 From: leo78913 Date: Fri, 27 Jan 2023 13:48:12 -0300 Subject: [PATCH 141/152] better fix for WideBar::insertSeparator Signed-off-by: leo78913 --- launcher/ui/widgets/WideBar.cpp | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/launcher/ui/widgets/WideBar.cpp b/launcher/ui/widgets/WideBar.cpp index ffc2dfd1..ac34e3aa 100644 --- a/launcher/ui/widgets/WideBar.cpp +++ b/launcher/ui/widgets/WideBar.cpp @@ -157,9 +157,7 @@ void WideBar::insertSeparator(QAction* before) return; BarEntry entry; - entry.bar_action = new QAction("", this); - entry.bar_action->setSeparator(true); - insertAction(iter->bar_action, entry.bar_action); + entry.bar_action = QToolBar::insertSeparator(iter->bar_action); entry.type = BarEntry::Type::Separator; m_entries.insert(iter, entry); From 2b0252d4ae344b427c36c638e5fcd781f8e5b3ec Mon Sep 17 00:00:00 2001 From: leo78913 Date: Sat, 28 Jan 2023 15:09:26 -0300 Subject: [PATCH 142/152] Fix: fix some regressions in the main window this removes the update action from the help button and fixes the add to path action not showing on macos Signed-off-by: leo78913 --- launcher/ui/MainWindow.cpp | 4 +++- launcher/ui/MainWindow.ui | 6 ------ 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/launcher/ui/MainWindow.cpp b/launcher/ui/MainWindow.cpp index e8765b3d..6d21f5ed 100644 --- a/launcher/ui/MainWindow.cpp +++ b/launcher/ui/MainWindow.cpp @@ -192,7 +192,9 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWi foldersMenuButton->setPopupMode(QToolButton::InstantPopup); helpMenuButton = dynamic_cast(ui->mainToolBar->widgetForAction(ui->actionHelpButton)); - ui->actionHelpButton->setMenu(ui->helpMenu); + ui->actionHelpButton->setMenu(new QMenu(this)); + ui->actionHelpButton->menu()->addActions(ui->helpMenu->actions()); + ui->actionHelpButton->menu()->removeAction(ui->actionCheckUpdate); helpMenuButton->setPopupMode(QToolButton::InstantPopup); auto accountMenuButton = dynamic_cast(ui->mainToolBar->widgetForAction(ui->actionAccountsButton)); diff --git a/launcher/ui/MainWindow.ui b/launcher/ui/MainWindow.ui index c9c9af94..2b6a10b1 100644 --- a/launcher/ui/MainWindow.ui +++ b/launcher/ui/MainWindow.ui @@ -621,9 +621,6 @@ - - false - .. @@ -634,9 +631,6 @@ Install a %1 symlink to /usr/local/bin - - false - From 7cc39cd35710cda179d93de0d9463be8d8075255 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 30 Jan 2023 13:29:47 +0000 Subject: [PATCH 143/152] chore(deps): update actions/cache action to v3.2.4 --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1373815c..6ec4f6a4 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -167,7 +167,7 @@ jobs: - name: Retrieve ccache cache (Windows MinGW-w64) if: runner.os == 'Windows' && matrix.msystem != '' && inputs.build_type == 'Debug' - uses: actions/cache@v3.2.3 + uses: actions/cache@v3.2.4 with: path: '${{ github.workspace }}\.ccache' key: ${{ matrix.os }}-mingw-w64 From ec5bb944b24413c1dee30a2a8429f484231c60c1 Mon Sep 17 00:00:00 2001 From: KosmX Date: Wed, 1 Feb 2023 14:59:11 +0100 Subject: [PATCH 144/152] thread-safe logger Signed-off-by: KosmX --- launcher/Application.cpp | 2 ++ launcher/Application.h | 2 ++ 2 files changed, 4 insertions(+) diff --git a/launcher/Application.cpp b/launcher/Application.cpp index 387f735c..ae7a69c6 100644 --- a/launcher/Application.cpp +++ b/launcher/Application.cpp @@ -150,6 +150,8 @@ namespace { /** This is used so that we can output to the log file in addition to the CLI. */ void appDebugOutput(QtMsgType type, const QMessageLogContext &context, const QString &msg) { + const std::lock_guard lock(APPLICATION->loggerMutex); // synchronized, QFile logFile is not thread-safe + QString out = qFormatLogMessage(type, context, msg); out += QChar::LineFeed; diff --git a/launcher/Application.h b/launcher/Application.h index 1b3dc499..caee074d 100644 --- a/launcher/Application.h +++ b/launcher/Application.h @@ -45,6 +45,7 @@ #include #include +#include #include "minecraft/launch/MinecraftServerTarget.h" @@ -310,4 +311,5 @@ public: QList m_zipsToImport; QString m_instanceIdToShowWindowOf; std::unique_ptr logFile; + std::mutex loggerMutex; }; From e593faf24512e4e9077508f2b557fc51d2e5d595 Mon Sep 17 00:00:00 2001 From: flow Date: Wed, 1 Feb 2023 11:44:50 -0300 Subject: [PATCH 145/152] fix(tests): improve the reliability of the Task's stack test This actually takes into account the amount of stuff put into the stack in each iteration, and thus avoids having to change the stack size of the thread, and using ad-hoc values for the other stuff. It also reduces the time the test takes to run. Signed-off-by: flow --- tests/Task_test.cpp | 30 ++++++++++++++++++++++-------- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/tests/Task_test.cpp b/tests/Task_test.cpp index 558cd2c0..95eb4a30 100644 --- a/tests/Task_test.cpp +++ b/tests/Task_test.cpp @@ -7,6 +7,8 @@ #include #include +#include + /* Does nothing. Only used for testing. */ class BasicTask : public Task { Q_OBJECT @@ -35,10 +37,23 @@ class BasicTask_MultiStep : public Task { void executeTask() override {}; }; -class BigConcurrentTask : public QThread { +class BigConcurrentTask : public ConcurrentTask { Q_OBJECT - ConcurrentTask big_task; + void startNext() override + { + // This is here only to help fill the stack a bit more quickly (if there's an issue, of course :^)) + // Each tasks thus adds 1024 * 4 bytes to the stack, at the very least. + [[maybe_unused]] volatile std::array some_data_on_the_stack {}; + + ConcurrentTask::startNext(); + } +}; + +class BigConcurrentTaskThread : public QThread { + Q_OBJECT + + BigConcurrentTask big_task; void run() override { @@ -48,7 +63,9 @@ class BigConcurrentTask : public QThread { deadline.start(); // NOTE: Arbitrary value that manages to trigger a problem when there is one. - static const unsigned s_num_tasks = 1 << 14; + // Considering each tasks, in a problematic state, adds 1024 * 4 bytes to the stack, + // this number is enough to fill up 16 MiB of stack, more than enough to cause a problem. + static const unsigned s_num_tasks = 1 << 12; auto sub_tasks = new BasicTask::Ptr[s_num_tasks]; for (unsigned i = 0; i < s_num_tasks; i++) { @@ -237,12 +254,9 @@ class TaskTest : public QObject { { QEventLoop loop; - auto thread = new BigConcurrentTask; - // NOTE: This is an arbitrary value, big enough to not cause problems on normal execution, but low enough - // so that the number of tasks that needs to get ran to potentially cause a problem isn't too big. - thread->setStackSize(32 * 1024); + auto thread = new BigConcurrentTaskThread; - connect(thread, &BigConcurrentTask::finished, &loop, &QEventLoop::quit); + connect(thread, &BigConcurrentTaskThread::finished, &loop, &QEventLoop::quit); thread->start(); From 121a7a9e23ffbaaecceeafd7186da420b5dfad7e Mon Sep 17 00:00:00 2001 From: TheLastRar Date: Wed, 1 Feb 2023 20:12:19 +0000 Subject: [PATCH 146/152] CI: Log ccache stats for msys2 Signed-off-by: TheLastRar --- .github/workflows/build.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6ec4f6a4..8ea02ea5 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -510,6 +510,13 @@ jobs: with: name: PrismLauncher-${{ runner.os }}-${{ env.VERSION }}-${{ inputs.build_type }}-x86_64.AppImage path: PrismLauncher-${{ runner.os }}-${{ env.VERSION }}-${{ inputs.build_type }}-x86_64.AppImage + + - name: ccache stats (Windows MinGW-w64) + if: runner.os == 'Windows' && matrix.msystem != '' + shell: msys2 {0} + run: | + ccache -s + snap: runs-on: ubuntu-20.04 steps: From 1a609612f2b5123a62c88977d1345661ae8eac44 Mon Sep 17 00:00:00 2001 From: TheLastRar Date: Sat, 29 Oct 2022 16:29:17 +0100 Subject: [PATCH 147/152] CI: Move mingw restore cache before setup ccache Signed-off-by: TheLastRar --- .github/workflows/build.yml | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8ea02ea5..ec1a06ed 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -149,6 +149,15 @@ jobs: with: key: ${{ matrix.os }}-qt${{ matrix.qt_ver }}-${{ matrix.architecture }} + - name: Retrieve ccache cache (Windows MinGW-w64) + if: runner.os == 'Windows' && matrix.msystem != '' && inputs.build_type == 'Debug' + uses: actions/cache@v3.2.4 + with: + path: '${{ github.workspace }}\.ccache' + key: ${{ matrix.os }}-mingw-w64 + restore-keys: | + ${{ matrix.os }}-mingw-w64 + - name: Setup ccache (Windows MinGW-w64) if: runner.os == 'Windows' && matrix.msystem != '' && inputs.build_type == 'Debug' shell: msys2 {0} @@ -165,15 +174,6 @@ jobs: run: | echo "CCACHE_VAR=ccache" >> $GITHUB_ENV - - name: Retrieve ccache cache (Windows MinGW-w64) - if: runner.os == 'Windows' && matrix.msystem != '' && inputs.build_type == 'Debug' - uses: actions/cache@v3.2.4 - with: - path: '${{ github.workspace }}\.ccache' - key: ${{ matrix.os }}-mingw-w64 - restore-keys: | - ${{ matrix.os }}-mingw-w64 - - name: Set short version shell: bash run: | From 75683039c5859d9ec3c7e94a21f4c3a3b38c16b9 Mon Sep 17 00:00:00 2001 From: TheLastRar Date: Sat, 29 Oct 2022 17:30:26 +0100 Subject: [PATCH 148/152] CI: Always update windows ccache Also change name to avoid pulling the stale cache Signed-off-by: TheLastRar --- .github/workflows/build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ec1a06ed..86e88fa1 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -154,9 +154,9 @@ jobs: uses: actions/cache@v3.2.4 with: path: '${{ github.workspace }}\.ccache' - key: ${{ matrix.os }}-mingw-w64 + key: ${{ matrix.os }}-mingw-w64-ccache-${{ github.run_id }} restore-keys: | - ${{ matrix.os }}-mingw-w64 + ${{ matrix.os }}-mingw-w64-ccache - name: Setup ccache (Windows MinGW-w64) if: runner.os == 'Windows' && matrix.msystem != '' && inputs.build_type == 'Debug' From 35a62d97875360132d8d67c0e6e6d69dd48481f5 Mon Sep 17 00:00:00 2001 From: KosmX Date: Wed, 1 Feb 2023 23:31:12 +0100 Subject: [PATCH 149/152] commit requested change, make the lock static Signed-off-by: KosmX --- launcher/Application.cpp | 4 +++- launcher/Application.h | 2 -- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/launcher/Application.cpp b/launcher/Application.cpp index ae7a69c6..0d3b086f 100644 --- a/launcher/Application.cpp +++ b/launcher/Application.cpp @@ -77,6 +77,7 @@ #include "ApplicationMessage.h" #include +#include #include #include @@ -150,7 +151,8 @@ namespace { /** This is used so that we can output to the log file in addition to the CLI. */ void appDebugOutput(QtMsgType type, const QMessageLogContext &context, const QString &msg) { - const std::lock_guard lock(APPLICATION->loggerMutex); // synchronized, QFile logFile is not thread-safe + static std::mutex loggerMutex; + const std::lock_guard lock(loggerMutex); // synchronized, QFile logFile is not thread-safe QString out = qFormatLogMessage(type, context, msg); out += QChar::LineFeed; diff --git a/launcher/Application.h b/launcher/Application.h index caee074d..1b3dc499 100644 --- a/launcher/Application.h +++ b/launcher/Application.h @@ -45,7 +45,6 @@ #include #include -#include #include "minecraft/launch/MinecraftServerTarget.h" @@ -311,5 +310,4 @@ public: QList m_zipsToImport; QString m_instanceIdToShowWindowOf; std::unique_ptr logFile; - std::mutex loggerMutex; }; From 435273e08a3cf6cb8197acabb31b1d4889a87254 Mon Sep 17 00:00:00 2001 From: flow Date: Tue, 31 Jan 2023 10:28:39 -0300 Subject: [PATCH 150/152] fix(Inst.Import): don't allow bad file path in mrpack import This checks the URL of the path of the file to be downloaded, ensuring that it always contains the root .minecraft target folder, following the warning in the mrpack documentation. Signed-off-by: flow --- .../modrinth/ModrinthInstanceCreationTask.cpp | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.cpp b/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.cpp index 94c0bf77..6814e645 100644 --- a/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.cpp +++ b/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.cpp @@ -225,10 +225,19 @@ bool ModrinthCreationTask::createInstance() m_files_job.reset(new NetJob(tr("Mod download"), APPLICATION->network())); + auto root_modpack_path = FS::PathCombine(m_stagingPath, ".minecraft"); + auto root_modpack_url = QUrl::fromLocalFile(root_modpack_path); + for (auto file : m_files) { - auto path = FS::PathCombine(m_stagingPath, ".minecraft", file.path); - qDebug() << "Will try to download" << file.downloads.front() << "to" << path; - auto dl = Net::Download::makeFile(file.downloads.dequeue(), path); + auto file_path = FS::PathCombine(root_modpack_path, file.path); + 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 + setError(tr("One of the files has a path that leads to an arbitrary location (%1). This is a security risk and isn't allowed.").arg(file.path)); + return false; + } + + qDebug() << "Will try to download" << file.downloads.front() << "to" << file_path; + auto dl = Net::Download::makeFile(file.downloads.dequeue(), file_path); dl->addValidator(new Net::ChecksumValidator(file.hashAlgorithm, file.hash)); m_files_job->addNetAction(dl); @@ -236,8 +245,8 @@ bool ModrinthCreationTask::createInstance() // FIXME: This really needs to be put into a ConcurrentTask of // MultipleOptionsTask's , once those exist :) auto param = dl.toWeakRef(); - connect(dl.get(), &NetAction::failed, [this, &file, path, param] { - auto ndl = Net::Download::makeFile(file.downloads.dequeue(), path); + connect(dl.get(), &NetAction::failed, [this, &file, file_path, param] { + auto ndl = Net::Download::makeFile(file.downloads.dequeue(), file_path); ndl->addValidator(new Net::ChecksumValidator(file.hashAlgorithm, file.hash)); m_files_job->addNetAction(ndl); if (auto shared = param.lock()) shared->succeeded(); From 4166d9ab7b4ce374e2705f2f8ed22101d3d5f48c Mon Sep 17 00:00:00 2001 From: flow Date: Tue, 31 Jan 2023 15:01:25 -0300 Subject: [PATCH 151/152] fix: give error when components have bad uids This allows other code to reject proceeding when the UID is bad, which is generally a good idea. :p Co-authored-by: Sefa Eyeoglu Signed-off-by: flow --- launcher/minecraft/OneSixVersionFormat.cpp | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/launcher/minecraft/OneSixVersionFormat.cpp b/launcher/minecraft/OneSixVersionFormat.cpp index 280f6b26..c2e33f4b 100644 --- a/launcher/minecraft/OneSixVersionFormat.cpp +++ b/launcher/minecraft/OneSixVersionFormat.cpp @@ -39,6 +39,8 @@ #include "minecraft/ParseUtils.h" #include +#include + using namespace Json; static void readString(const QJsonObject &root, const QString &key, QString &variable) @@ -121,6 +123,15 @@ VersionFilePtr OneSixVersionFormat::versionFileFromJson(const QJsonDocument &doc out->uid = root.value("fileId").toString(); } + const QRegularExpression valid_uid_regex{ QRegularExpression::anchoredPattern(QStringLiteral(R"(\w+(?:\.\w+)*)")) }; + if (!valid_uid_regex.match(out->uid).hasMatch()) { + qCritical() << "The component's 'uid' contains illegal characters! UID:" << out->uid; + out->addProblem( + ProblemSeverity::Error, + QObject::tr("The component's 'uid' contains illegal characters! This can cause security issues.") + ); + } + out->version = root.value("version").toString(); MojangVersionFormat::readVersionProperties(root, out.get()); From 6ac073e7792e3a2831ce9b7a5b5d2808c0464f90 Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Fri, 3 Feb 2023 18:32:57 +0100 Subject: [PATCH 152/152] fix: fix component uid regex Signed-off-by: Sefa Eyeoglu --- launcher/minecraft/OneSixVersionFormat.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/launcher/minecraft/OneSixVersionFormat.cpp b/launcher/minecraft/OneSixVersionFormat.cpp index c2e33f4b..888b6860 100644 --- a/launcher/minecraft/OneSixVersionFormat.cpp +++ b/launcher/minecraft/OneSixVersionFormat.cpp @@ -123,7 +123,7 @@ VersionFilePtr OneSixVersionFormat::versionFileFromJson(const QJsonDocument &doc out->uid = root.value("fileId").toString(); } - const QRegularExpression valid_uid_regex{ QRegularExpression::anchoredPattern(QStringLiteral(R"(\w+(?:\.\w+)*)")) }; + const QRegularExpression valid_uid_regex{ QRegularExpression::anchoredPattern(QStringLiteral(R"([a-zA-Z0-9-_]+(?:\.[a-zA-Z0-9-_]+)*)")) }; if (!valid_uid_regex.match(out->uid).hasMatch()) { qCritical() << "The component's 'uid' contains illegal characters! UID:" << out->uid; out->addProblem(