From fdbd8d9d2b2e04cd68fd800882309b40c05aba2c Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Tue, 1 Nov 2022 22:45:15 +0100 Subject: [PATCH 001/199] 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 ea8d23261..8fe5a8bff 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 7884227a1..23c70e4ce 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 e8afa6b8f..528c79906 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 9ff448549..000000000 --- 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 715554e53..000000000 --- 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 1d8d70833..8ecf715db 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 5ae53c1c5..c730fdbf2 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 76b867437..d9e785c52 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 5a62e4d06..b1ea5ee96 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 929f2a85c..0595634fb 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 0aa01ee2f..53db49192 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 edb4ff7d4..eeeeb709f 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 88552b239..f13e36e86 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 9e82531ac..000000000 --- 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 07cbe09f1..000000000 --- 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 5eb9d88ac..000000000 --- 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 cae0635f0..a4c2755c7 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 f38c922e2..c60156c2c 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 c44718a18..fb36608d0 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 31c3e925d..9985f426e 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 c22706af9..cdd53cc95 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 d64bcb767..a4f9f3309 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 85cc01ff0..7819d0779 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 6b1f6b899..2343b79f2 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 3be377a1d..6e6be4b9e 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 48fe767a6..000000000 --- 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 19a6265cf..000000000 --- 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 4bc7dfa99..000000000 --- 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 46a679efe..000000000 --- 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 d50dbd685..cee19f7c7 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 ca6da55af..07337176d 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 78d979ff1..000000000 --- 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 42ef318bb..000000000 --- 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 c44f9310593452f4430034890f4fbe5df3560a1d Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Mon, 12 Dec 2022 19:03:31 +0100 Subject: [PATCH 002/199] fix: update installers-regex for winget releaser Signed-off-by: Sefa Eyeoglu --- .github/workflows/winget.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/winget.yml b/.github/workflows/winget.yml index 5c34040f7..b4136df5b 100644 --- a/.github/workflows/winget.yml +++ b/.github/workflows/winget.yml @@ -11,5 +11,5 @@ jobs: with: identifier: PrismLauncher.PrismLauncher version: ${{ github.event.release.tag_name }} - installers-regex: 'PrismLauncher-Windows-Setup-.+\.exe$' + installers-regex: 'PrismLauncher-Windows-MSVC(:?-arm64)?-Setup-.+\.exe$' token: ${{ secrets.WINGET_TOKEN }} From 64585d8f78ddf69488f6f67a789d566ed653b128 Mon Sep 17 00:00:00 2001 From: flow Date: Tue, 13 Dec 2022 00:31:41 -0300 Subject: [PATCH 003/199] fix(Inst.Import): don't assert extra data when importing from ZIP ZIPs don't have the necessary data in those cases. Signed-off-by: flow --- launcher/InstanceImportTask.cpp | 64 +++++++++++++++++++++------------ 1 file changed, 41 insertions(+), 23 deletions(-) diff --git a/launcher/InstanceImportTask.cpp b/launcher/InstanceImportTask.cpp index b97870da4..6b3fd296f 100644 --- a/launcher/InstanceImportTask.cpp +++ b/launcher/InstanceImportTask.cpp @@ -257,20 +257,26 @@ void InstanceImportTask::extractAborted() void InstanceImportTask::processFlame() { - auto pack_id_it = m_extra_info.constFind("pack_id"); - Q_ASSERT(pack_id_it != m_extra_info.constEnd()); - auto pack_id = pack_id_it.value(); + FlameCreationTask* inst_creation_task = nullptr; + if (!m_extra_info.isEmpty()) { + auto pack_id_it = m_extra_info.constFind("pack_id"); + Q_ASSERT(pack_id_it != m_extra_info.constEnd()); + auto pack_id = pack_id_it.value(); - auto pack_version_id_it = m_extra_info.constFind("pack_version_id"); - Q_ASSERT(pack_version_id_it != m_extra_info.constEnd()); - auto pack_version_id = pack_version_id_it.value(); + auto pack_version_id_it = m_extra_info.constFind("pack_version_id"); + Q_ASSERT(pack_version_id_it != m_extra_info.constEnd()); + auto pack_version_id = pack_version_id_it.value(); - QString original_instance_id; - auto original_instance_id_it = m_extra_info.constFind("original_instance_id"); - if (original_instance_id_it != m_extra_info.constEnd()) - original_instance_id = original_instance_id_it.value(); + QString original_instance_id; + auto original_instance_id_it = m_extra_info.constFind("original_instance_id"); + if (original_instance_id_it != m_extra_info.constEnd()) + original_instance_id = original_instance_id_it.value(); - auto* inst_creation_task = new FlameCreationTask(m_stagingPath, m_globalSettings, m_parent, pack_id, pack_version_id, original_instance_id); + inst_creation_task = new FlameCreationTask(m_stagingPath, m_globalSettings, m_parent, pack_id, pack_version_id, original_instance_id); + } else { + // FIXME: Find a way to get IDs in directly imported ZIPs + inst_creation_task = new FlameCreationTask(m_stagingPath, m_globalSettings, m_parent, {}, {}); + } inst_creation_task->setName(*this); inst_creation_task->setIcon(m_instIcon); @@ -335,21 +341,33 @@ void InstanceImportTask::processMultiMC() void InstanceImportTask::processModrinth() { - auto pack_id_it = m_extra_info.constFind("pack_id"); - Q_ASSERT(pack_id_it != m_extra_info.constEnd()); - auto pack_id = pack_id_it.value(); + ModrinthCreationTask* inst_creation_task = nullptr; + if (!m_extra_info.isEmpty()) { + auto pack_id_it = m_extra_info.constFind("pack_id"); + Q_ASSERT(pack_id_it != m_extra_info.constEnd()); + auto pack_id = pack_id_it.value(); - QString pack_version_id; - auto pack_version_id_it = m_extra_info.constFind("pack_version_id"); - if (pack_version_id_it != m_extra_info.constEnd()) - pack_version_id = pack_version_id_it.value(); + QString pack_version_id; + auto pack_version_id_it = m_extra_info.constFind("pack_version_id"); + if (pack_version_id_it != m_extra_info.constEnd()) + pack_version_id = pack_version_id_it.value(); - QString original_instance_id; - auto original_instance_id_it = m_extra_info.constFind("original_instance_id"); - if (original_instance_id_it != m_extra_info.constEnd()) - original_instance_id = original_instance_id_it.value(); + QString original_instance_id; + auto original_instance_id_it = m_extra_info.constFind("original_instance_id"); + if (original_instance_id_it != m_extra_info.constEnd()) + original_instance_id = original_instance_id_it.value(); - auto* inst_creation_task = new ModrinthCreationTask(m_stagingPath, m_globalSettings, m_parent, pack_id, pack_version_id, original_instance_id); + inst_creation_task = new ModrinthCreationTask(m_stagingPath, m_globalSettings, m_parent, pack_id, pack_version_id, original_instance_id); + } else { + QString pack_id; + if (!m_sourceUrl.isEmpty()) { + QRegularExpression regex(R"(data\/(.*)\/versions)"); + pack_id = regex.match(m_sourceUrl.toString()).captured(1); + } + + // FIXME: Find a way to get the ID in directly imported ZIPs + inst_creation_task = new ModrinthCreationTask(m_stagingPath, m_globalSettings, m_parent, pack_id); + } inst_creation_task->setName(*this); inst_creation_task->setIcon(m_instIcon); From 5450e0edf305090c2cab81e335c8d5366d7f1f13 Mon Sep 17 00:00:00 2001 From: flow Date: Tue, 13 Dec 2022 13:43:27 -0300 Subject: [PATCH 004/199] fix(Inst.Import): don't set managed pack info from imported ZIPs This prevents the Managed Pack page from showing up even though there's no way for it to work correctly. Signed-off-by: flow --- launcher/modplatform/flame/FlameInstanceCreationTask.cpp | 4 +++- .../modplatform/modrinth/ModrinthInstanceCreationTask.cpp | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/launcher/modplatform/flame/FlameInstanceCreationTask.cpp b/launcher/modplatform/flame/FlameInstanceCreationTask.cpp index 729268d7d..1d441f092 100644 --- a/launcher/modplatform/flame/FlameInstanceCreationTask.cpp +++ b/launcher/modplatform/flame/FlameInstanceCreationTask.cpp @@ -361,7 +361,9 @@ bool FlameCreationTask::createInstance() FS::deletePath(jarmodsPath); } - instance.setManagedPack("flame", m_managed_id, m_pack.name, m_managed_version_id, m_pack.version); + // Don't add managed info to packs without an ID (most likely imported from ZIP) + if (!m_managed_id.isEmpty()) + 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); diff --git a/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.cpp b/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.cpp index 1c0e89798..5632f6a32 100644 --- a/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.cpp +++ b/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.cpp @@ -217,7 +217,9 @@ bool ModrinthCreationTask::createInstance() instance.setIconKey("modrinth"); } - instance.setManagedPack("modrinth", m_managed_id, m_managed_name, m_managed_version_id, version()); + // Don't add managed info to packs without an ID (most likely imported from ZIP) + if (!m_managed_id.isEmpty()) + instance.setManagedPack("modrinth", m_managed_id, m_managed_name, m_managed_version_id, version()); instance.setName(name()); instance.saveNow(); From 127b094c4158f7a2315bb35cea05f5644a0db1c5 Mon Sep 17 00:00:00 2001 From: TheKodeToad Date: Wed, 14 Dec 2022 15:02:04 +0000 Subject: [PATCH 005/199] 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 3e8e10a51..b3af4f4eb 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 ac8937258..15233b66a 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 90fcf3376..d310f8b95 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 0fbcfd7c1..7c572d928 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 5a62e4d06..241354cb8 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 5e109383a..bf93b3c5b 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 2f1976cc0..4ddef6d49 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 c66d13688..41ccd1db4 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 2d1a5b516..d17fbb7f1 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 31c3e925d..2a6504a2a 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 0a2e6155e..627e71e50 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 f20adf34d..ff58b38a5 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 0c1939c60..ad444e6b0 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 0092aef33..fff21670f 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 a625e20b6..c636b2367 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 93458ce4f..74cb5a058 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 7c68bfaee..d74dd0796 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 006/199] 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 c8a65f103..413b2f853 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 52dc9068e558046a84353df924031091b3e28b7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Edgars=20C=C4=ABrulis?= Date: Thu, 15 Dec 2022 15:43:46 +0200 Subject: [PATCH 007/199] ApplicationMessage: Use QHash instead of QMap MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit QHash provides faster lookup times than QMap because it uses a hash table to store the elements, while QMap uses a self-balancing binary tree. Signed-off-by: Edgars CÄ«rulis --- launcher/ApplicationMessage.cpp | 8 ++++---- launcher/ApplicationMessage.h | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/launcher/ApplicationMessage.cpp b/launcher/ApplicationMessage.cpp index ca276b89c..700e43ced 100644 --- a/launcher/ApplicationMessage.cpp +++ b/launcher/ApplicationMessage.cpp @@ -47,8 +47,8 @@ void ApplicationMessage::parse(const QByteArray & input) { args.clear(); auto parsedArgs = root.value("args").toObject(); - for(auto iter = parsedArgs.begin(); iter != parsedArgs.end(); iter++) { - args[iter.key()] = iter.value().toString(); + for(auto iter = parsedArgs.constBegin(); iter != parsedArgs.constEnd(); iter++) { + args.insert(iter.key(), iter.value().toString()); } } @@ -56,8 +56,8 @@ QByteArray ApplicationMessage::serialize() { QJsonObject root; root.insert("command", command); QJsonObject outArgs; - for (auto iter = args.begin(); iter != args.end(); iter++) { - outArgs[iter.key()] = iter.value(); + for (auto iter = args.constBegin(); iter != args.constEnd(); iter++) { + outArgs.insert(iter.key(), iter.value()); } root.insert("args", outArgs); diff --git a/launcher/ApplicationMessage.h b/launcher/ApplicationMessage.h index 745bdeadc..d66456ebd 100644 --- a/launcher/ApplicationMessage.h +++ b/launcher/ApplicationMessage.h @@ -1,12 +1,12 @@ #pragma once #include -#include +#include #include struct ApplicationMessage { QString command; - QMap args; + QHash args; QByteArray serialize(); void parse(const QByteArray & input); From 4ee29b388d48cf84d4b120f49bf2313b1d994dca Mon Sep 17 00:00:00 2001 From: leo78913 Date: Thu, 15 Dec 2022 12:02:08 -0300 Subject: [PATCH 008/199] 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 39023f698..d491d980f 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 f336bec4c..16d2bb328 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 4ccc5d4d5..5aadc2f17 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 93980319d..6898f6eb4 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 f9bd811e6..0c37f3a39 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 dd578354c448dbb92b12438886d80ac6c0c0bea7 Mon Sep 17 00:00:00 2001 From: flow Date: Thu, 15 Dec 2022 13:45:50 -0300 Subject: [PATCH 009/199] feat(Tasks): add ConcurrentTask::clear to allow re-using tasks This way old runs won't pile up in the internal DSs Signed-off-by: flow --- launcher/tasks/ConcurrentTask.cpp | 16 ++++++++++++++++ launcher/tasks/ConcurrentTask.h | 5 +++++ 2 files changed, 21 insertions(+) diff --git a/launcher/tasks/ConcurrentTask.cpp b/launcher/tasks/ConcurrentTask.cpp index ce08a6a29..bc3cf4d3c 100644 --- a/launcher/tasks/ConcurrentTask.cpp +++ b/launcher/tasks/ConcurrentTask.cpp @@ -73,6 +73,22 @@ bool ConcurrentTask::abort() return suceedeed; } +void ConcurrentTask::clear() +{ + Q_ASSERT(!isRunning()); + + m_done.clear(); + m_failed.clear(); + m_queue.clear(); + + m_aborted = false; + + m_progress = 0; + m_stepProgress = 0; + + m_total_size = 0; +} + void ConcurrentTask::startNext() { if (m_aborted || m_doing.count() > m_total_max_size) diff --git a/launcher/tasks/ConcurrentTask.h b/launcher/tasks/ConcurrentTask.h index f1279d324..30b2ae4d0 100644 --- a/launcher/tasks/ConcurrentTask.h +++ b/launcher/tasks/ConcurrentTask.h @@ -24,6 +24,11 @@ public: public slots: bool abort() override; + /** Resets the internal state of the task. + * This allows the same task to be re-used. + */ + void clear(); + protected slots: void executeTask() override; From b0c866bfaa8e784bb89d25fd4a847686e11c3ab5 Mon Sep 17 00:00:00 2001 From: flow Date: Thu, 15 Dec 2022 13:46:51 -0300 Subject: [PATCH 010/199] feat(Tasks): allow adding subtasks while running in ConcurrentTask Signed-off-by: flow --- launcher/tasks/ConcurrentTask.cpp | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/launcher/tasks/ConcurrentTask.cpp b/launcher/tasks/ConcurrentTask.cpp index bc3cf4d3c..8f688f073 100644 --- a/launcher/tasks/ConcurrentTask.cpp +++ b/launcher/tasks/ConcurrentTask.cpp @@ -27,10 +27,8 @@ auto ConcurrentTask::getStepTotalProgress() const -> qint64 void ConcurrentTask::addTask(Task::Ptr task) { - if (!isRunning()) - m_queue.append(task); - else - qWarning() << "Tried to add a task to a running concurrent task!"; + m_queue.append(task); + m_total_size += 1; } void ConcurrentTask::executeTask() @@ -117,9 +115,14 @@ void ConcurrentTask::startNext() setStepStatus(next->isMultiStep() ? next->getStepStatus() : next->getStatus()); updateState(); - QCoreApplication::processEvents(); + QMetaObject::invokeMethod(next.get(), &Task::start, Qt::QueuedConnection); - next->start(); + // 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 c440f331226b3cc32fac6269d88758a4a0c23fc0 Mon Sep 17 00:00:00 2001 From: flow Date: Thu, 15 Dec 2022 13:51:07 -0300 Subject: [PATCH 011/199] fix(ResourceModel): use a single ConcurrentTask for parsing tasks This avoids creating a bunch of threads that fills up the maximum amount allowed by QThreadPool, and causes a deadlock between the helper threads and the main thread (main thread tries to create threads in painting code, but isn't able to, so it keeps waiting for a thread to free up, but all the threads are waiting on the main thread to process some events). Signed-off-by: flow --- launcher/minecraft/mod/ResourceFolderModel.cpp | 7 ++++++- launcher/minecraft/mod/ResourceFolderModel.h | 2 ++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/launcher/minecraft/mod/ResourceFolderModel.cpp b/launcher/minecraft/mod/ResourceFolderModel.cpp index 0310c8f6e..a52c5db34 100644 --- a/launcher/minecraft/mod/ResourceFolderModel.cpp +++ b/launcher/minecraft/mod/ResourceFolderModel.cpp @@ -20,6 +20,7 @@ ResourceFolderModel::ResourceFolderModel(QDir dir, QObject* parent) : QAbstractL m_dir.setSorting(QDir::Name | QDir::IgnoreCase | QDir::LocaleAware); connect(&m_watcher, &QFileSystemWatcher::directoryChanged, this, &ResourceFolderModel::directoryChanged); + connect(&m_helper_thread_task, &ConcurrentTask::finished, this, [this]{ m_helper_thread_task.clear(); }); } ResourceFolderModel::~ResourceFolderModel() @@ -275,7 +276,11 @@ void ResourceFolderModel::resolveResource(Resource* res) connect( task, &Task::finished, this, [=] { m_active_parse_tasks.remove(ticket); }, Qt::ConnectionType::QueuedConnection); - QThreadPool::globalInstance()->start(task); + m_helper_thread_task.addTask(task); + + if (!m_helper_thread_task.isRunning()) { + QThreadPool::globalInstance()->start(&m_helper_thread_task); + } } void ResourceFolderModel::onUpdateSucceeded() diff --git a/launcher/minecraft/mod/ResourceFolderModel.h b/launcher/minecraft/mod/ResourceFolderModel.h index fe283b043..f1bc2dd78 100644 --- a/launcher/minecraft/mod/ResourceFolderModel.h +++ b/launcher/minecraft/mod/ResourceFolderModel.h @@ -10,6 +10,7 @@ #include "Resource.h" #include "tasks/Task.h" +#include "tasks/ConcurrentTask.h" class QSortFilterProxyModel; @@ -197,6 +198,7 @@ class ResourceFolderModel : public QAbstractListModel { // Represents the relationship between a resource's internal ID and it's row position on the model. QMap m_resources_index; + ConcurrentTask m_helper_thread_task; QMap m_active_parse_tasks; std::atomic m_next_resolution_ticket = 0; }; From c8d8dda79a8ac184c98922372ef7c3531f897486 Mon Sep 17 00:00:00 2001 From: leo78913 Date: Thu, 15 Dec 2022 16:34:52 -0300 Subject: [PATCH 012/199] fix: only show scrollbars when needed Signed-off-by: leo78913 --- launcher/ui/dialogs/IconPickerDialog.cpp | 1 - launcher/ui/dialogs/ImportResourcePackDialog.cpp | 1 - launcher/ui/pages/instance/NotesPage.ui | 3 --- launcher/ui/pages/instance/OtherLogsPage.ui | 3 --- launcher/ui/pages/instance/VersionPage.ui | 3 --- launcher/ui/widgets/ModListView.cpp | 1 - 6 files changed, 12 deletions(-) diff --git a/launcher/ui/dialogs/IconPickerDialog.cpp b/launcher/ui/dialogs/IconPickerDialog.cpp index fcb645db4..6fa265085 100644 --- a/launcher/ui/dialogs/IconPickerDialog.cpp +++ b/launcher/ui/dialogs/IconPickerDialog.cpp @@ -47,7 +47,6 @@ IconPickerDialog::IconPickerDialog(QWidget *parent) contentsWidget->setUniformItemSizes(true); contentsWidget->setTextElideMode(Qt::ElideRight); contentsWidget->setVerticalScrollMode(QAbstractItemView::ScrollPerPixel); - contentsWidget->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOn); contentsWidget->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); contentsWidget->setItemDelegate(new ListViewDelegate()); diff --git a/launcher/ui/dialogs/ImportResourcePackDialog.cpp b/launcher/ui/dialogs/ImportResourcePackDialog.cpp index e807e9265..e89026569 100644 --- a/launcher/ui/dialogs/ImportResourcePackDialog.cpp +++ b/launcher/ui/dialogs/ImportResourcePackDialog.cpp @@ -29,7 +29,6 @@ ImportResourcePackDialog::ImportResourcePackDialog(QWidget* parent) : QDialog(pa // NOTE: We can't have uniform sizes because the text may wrap if it's too long. If we set this, it will cut off the wrapped text. contentsWidget->setUniformItemSizes(false); contentsWidget->setVerticalScrollMode(QAbstractItemView::ScrollPerPixel); - contentsWidget->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOn); contentsWidget->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); contentsWidget->setItemDelegate(new ListViewDelegate()); diff --git a/launcher/ui/pages/instance/NotesPage.ui b/launcher/ui/pages/instance/NotesPage.ui index 67cb261c1..5f1984ad3 100644 --- a/launcher/ui/pages/instance/NotesPage.ui +++ b/launcher/ui/pages/instance/NotesPage.ui @@ -25,9 +25,6 @@ - - Qt::ScrollBarAlwaysOn - true diff --git a/launcher/ui/pages/instance/OtherLogsPage.ui b/launcher/ui/pages/instance/OtherLogsPage.ui index 77f3e6477..3fdb023fe 100644 --- a/launcher/ui/pages/instance/OtherLogsPage.ui +++ b/launcher/ui/pages/instance/OtherLogsPage.ui @@ -48,9 +48,6 @@ false - - Qt::ScrollBarAlwaysOn - true diff --git a/launcher/ui/pages/instance/VersionPage.ui b/launcher/ui/pages/instance/VersionPage.ui index 74b9568a4..4cd508853 100644 --- a/launcher/ui/pages/instance/VersionPage.ui +++ b/launcher/ui/pages/instance/VersionPage.ui @@ -28,9 +28,6 @@ - - Qt::ScrollBarAlwaysOn - Qt::ScrollBarAlwaysOff diff --git a/launcher/ui/widgets/ModListView.cpp b/launcher/ui/widgets/ModListView.cpp index c8ccd2925..d1860f57b 100644 --- a/launcher/ui/widgets/ModListView.cpp +++ b/launcher/ui/widgets/ModListView.cpp @@ -31,7 +31,6 @@ ModListView::ModListView ( QWidget* parent ) setSelectionMode ( QAbstractItemView::ExtendedSelection ); setHeaderHidden ( false ); setSelectionBehavior(QAbstractItemView::SelectRows); - setVerticalScrollBarPolicy ( Qt::ScrollBarAlwaysOn ); setHorizontalScrollBarPolicy ( Qt::ScrollBarAsNeeded ); setDropIndicatorShown(true); setDragEnabled(true); From 7b805a38b9d87c5c5a765b692beca25f6944e897 Mon Sep 17 00:00:00 2001 From: seth Date: Thu, 15 Dec 2022 18:33:03 -0500 Subject: [PATCH 013/199] feat: improve msvc build flags - adds /GL, /Gy, and /LTCG for better optimizations - adds /Gw for a smaller binary size - adds /guard:cf for added security at runtime - removes unneeded /GS flag as that's already enabled by default Signed-off-by: seth --- CMakeLists.txt | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 75360f866..db731c07f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -28,19 +28,23 @@ set(CMAKE_CXX_STANDARD 17) set(CMAKE_C_STANDARD 11) include(GenerateExportHeader) if(MSVC) - # Use /W4 as /Wall includes unnesserey warnings such as added padding to structs + # /GL enables whole program optimizations + # /Gw helps reduce binary size + # /Gy allows the compiler to package individual functions + # /guard:cf enables control flow guard # /permissive- specify standards-conforming compiler behavior, also enabled by Qt6, default on with std:c++20 - # /GS Adds buffer security checks, default on but incuded anyway to mirror gcc's fstack-protector flag - set(CMAKE_CXX_FLAGS "/W4 /permissive- /GS ${CMAKE_CXX_FLAGS}") + # Use /W4 as /Wall includes unnesserey warnings such as added padding to structs + set(CMAKE_CXX_FLAGS "/GL /Gw /Gy /guard:cf /permissive- /W4 ${CMAKE_CXX_FLAGS}") # LINK accepts /SUBSYSTEM whics sets if we are a WINDOWS (gui) or a CONSOLE programs # This implicitly selects an entrypoint specific to the subsystem selected # qtmain/QtEntryPointLib provides the correct entrypoint (wWinMain) for gui programs # Additinaly LINK autodetects we use a GUI so we can omit /SUBSYSTEM # This allows tests to still use have console without using seperate linker flags + # /LTCG allows for linking wholy optimizated programs # /MANIFEST:NO disables generating a manifest file, we instead provide our own # /STACK sets the stack reserve size, ATL's pack list needs 3-4 MiB as of November 2022, provide 8 MiB - set(CMAKE_EXE_LINKER_FLAGS "/MANIFEST:NO /STACK:8388608 ${CMAKE_EXE_LINKER_FLAGS}") + set(CMAKE_EXE_LINKER_FLAGS "/LTCG /MANIFEST:NO /STACK:8388608 ${CMAKE_EXE_LINKER_FLAGS}") # See https://github.com/ccache/ccache/issues/1040 # Note, CMake 3.25 replaces this with CMAKE_MSVC_DEBUG_INFORMATION_FORMAT From aa3633d2d7591f4a68208d425e645fa4fc5c10a1 Mon Sep 17 00:00:00 2001 From: leo78913 Date: Fri, 16 Dec 2022 11:13:54 -0300 Subject: [PATCH 014/199] 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 d491d980f..1be8e7e30 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 a61daa40dcc93c7ab24bdf9bd1ff08d01576ae34 Mon Sep 17 00:00:00 2001 From: seth Date: Fri, 16 Dec 2022 16:05:12 -0500 Subject: [PATCH 015/199] fix: re-enable /GS and only use some flags on release builds Signed-off-by: seth --- CMakeLists.txt | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index db731c07f..de9b6fe17 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -28,13 +28,10 @@ set(CMAKE_CXX_STANDARD 17) set(CMAKE_C_STANDARD 11) include(GenerateExportHeader) if(MSVC) - # /GL enables whole program optimizations - # /Gw helps reduce binary size - # /Gy allows the compiler to package individual functions - # /guard:cf enables control flow guard + # /GS Adds buffer security checks, default on but incuded anyway to mirror gcc's fstack-protector flag # /permissive- specify standards-conforming compiler behavior, also enabled by Qt6, default on with std:c++20 # Use /W4 as /Wall includes unnesserey warnings such as added padding to structs - set(CMAKE_CXX_FLAGS "/GL /Gw /Gy /guard:cf /permissive- /W4 ${CMAKE_CXX_FLAGS}") + set(CMAKE_CXX_FLAGS "/GS /permissive- /W4 ${CMAKE_CXX_FLAGS}") # LINK accepts /SUBSYSTEM whics sets if we are a WINDOWS (gui) or a CONSOLE programs # This implicitly selects an entrypoint specific to the subsystem selected @@ -46,6 +43,14 @@ if(MSVC) # /STACK sets the stack reserve size, ATL's pack list needs 3-4 MiB as of November 2022, provide 8 MiB set(CMAKE_EXE_LINKER_FLAGS "/LTCG /MANIFEST:NO /STACK:8388608 ${CMAKE_EXE_LINKER_FLAGS}") + # /GL enables whole program optimizations + # /Gw helps reduce binary size + # /Gy allows the compiler to package individual functions + # /guard:cf enables control flow guard + foreach(lang C CXX) + set("CMAKE_${lang}_FLAGS_RELEASE" "/GL /Gw /Gy /guard:cf") + endforeach() + # See https://github.com/ccache/ccache/issues/1040 # Note, CMake 3.25 replaces this with CMAKE_MSVC_DEBUG_INFORMATION_FORMAT # See https://cmake.org/cmake/help/v3.25/variable/CMAKE_MSVC_DEBUG_INFORMATION_FORMAT.html From 3653e9d5e3c3dd24d73734db10bb341dd4f53687 Mon Sep 17 00:00:00 2001 From: leo78913 Date: Fri, 16 Dec 2022 10:59:18 -0300 Subject: [PATCH 016/199] let the theme decide the notes page right margin Signed-off-by: leo78913 --- launcher/ui/pages/instance/NotesPage.ui | 3 --- 1 file changed, 3 deletions(-) diff --git a/launcher/ui/pages/instance/NotesPage.ui b/launcher/ui/pages/instance/NotesPage.ui index 5f1984ad3..4b506da70 100644 --- a/launcher/ui/pages/instance/NotesPage.ui +++ b/launcher/ui/pages/instance/NotesPage.ui @@ -17,9 +17,6 @@ 0 - - 0 - 0 From 22aebc22159196a83f7f1447a951717b4359f528 Mon Sep 17 00:00:00 2001 From: flow Date: Sat, 17 Dec 2022 12:34:27 -0300 Subject: [PATCH 017/199] fix(Inst. Import): correctly set component versions when updating This makes it so that the later call to parse the old manifest doesn't change the class data, so that the new data con continue there and be reflected on the component list later. Co-authored-by: Sefa Eyeoglu Signed-off-by: flow --- .../modrinth/ModrinthInstanceCreationTask.cpp | 32 ++++++++++--------- .../modrinth/ModrinthInstanceCreationTask.h | 2 +- 2 files changed, 18 insertions(+), 16 deletions(-) diff --git a/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.cpp b/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.cpp index 5632f6a32..4f63ca824 100644 --- a/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.cpp +++ b/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.cpp @@ -279,7 +279,7 @@ bool ModrinthCreationTask::createInstance() return ended_well; } -bool ModrinthCreationTask::parseManifest(const QString& index_path, std::vector& files, bool set_managed_info, bool show_optional_dialog) +bool ModrinthCreationTask::parseManifest(const QString& index_path, std::vector& files, bool set_internal_data, bool show_optional_dialog) { try { auto doc = Json::requireDocument(index_path); @@ -291,7 +291,7 @@ bool ModrinthCreationTask::parseManifest(const QString& index_path, std::vector< throw JSONValidationError("Unknown game: " + game); } - if (set_managed_info) { + if (set_internal_data) { if (m_managed_version_id.isEmpty()) m_managed_version_id = Json::ensureString(obj, "versionId", {}, "Managed ID"); m_managed_name = Json::ensureString(obj, "name", {}, "Managed Name"); @@ -367,19 +367,21 @@ bool ModrinthCreationTask::parseManifest(const QString& index_path, std::vector< files.push_back(file); } - auto dependencies = Json::requireObject(obj, "dependencies", "modrinth.index.json"); - for (auto it = dependencies.begin(), end = dependencies.end(); it != end; ++it) { - QString name = it.key(); - if (name == "minecraft") { - minecraftVersion = Json::requireString(*it, "Minecraft version"); - } else if (name == "fabric-loader") { - fabricVersion = Json::requireString(*it, "Fabric Loader version"); - } else if (name == "quilt-loader") { - quiltVersion = Json::requireString(*it, "Quilt Loader version"); - } else if (name == "forge") { - forgeVersion = Json::requireString(*it, "Forge version"); - } else { - throw JSONValidationError("Unknown dependency type: " + name); + if (set_internal_data) { + auto dependencies = Json::requireObject(obj, "dependencies", "modrinth.index.json"); + for (auto it = dependencies.begin(), end = dependencies.end(); it != end; ++it) { + QString name = it.key(); + if (name == "minecraft") { + minecraftVersion = Json::requireString(*it, "Minecraft version"); + } else if (name == "fabric-loader") { + fabricVersion = Json::requireString(*it, "Fabric Loader version"); + } else if (name == "quilt-loader") { + quiltVersion = Json::requireString(*it, "Quilt Loader version"); + } else if (name == "forge") { + forgeVersion = Json::requireString(*it, "Forge version"); + } else { + throw JSONValidationError("Unknown dependency type: " + name); + } } } } else { diff --git a/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.h b/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.h index 122fc5ce3..5cf295f49 100644 --- a/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.h +++ b/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.h @@ -37,7 +37,7 @@ class ModrinthCreationTask final : public InstanceCreationTask { bool createInstance() override; private: - bool parseManifest(const QString&, std::vector&, bool set_managed_info = true, bool show_optional_dialog = true); + bool parseManifest(const QString&, std::vector&, bool set_internal_data = true, bool show_optional_dialog = true); private: QWidget* m_parent = nullptr; From e3f8d9908747863b9e5870b9a3d9c79f3407997f Mon Sep 17 00:00:00 2001 From: flow Date: Sat, 17 Dec 2022 12:41:10 -0300 Subject: [PATCH 018/199] refactor(Inst. Import): use m_* for member variables in MR components Makes it clearer what is being changed when. Signed-off-by: flow --- .../modrinth/ModrinthInstanceCreationTask.cpp | 22 +++++++++---------- .../modrinth/ModrinthInstanceCreationTask.h | 2 +- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.cpp b/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.cpp index 4f63ca824..c5a27c9dc 100644 --- a/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.cpp +++ b/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.cpp @@ -202,14 +202,14 @@ bool ModrinthCreationTask::createInstance() auto components = instance.getPackProfile(); components->buildingFromScratch(); - components->setComponentVersion("net.minecraft", minecraftVersion, true); + components->setComponentVersion("net.minecraft", m_minecraft_version, true); - if (!fabricVersion.isEmpty()) - components->setComponentVersion("net.fabricmc.fabric-loader", fabricVersion); - if (!quiltVersion.isEmpty()) - components->setComponentVersion("org.quiltmc.quilt-loader", quiltVersion); - if (!forgeVersion.isEmpty()) - components->setComponentVersion("net.minecraftforge", forgeVersion); + if (!m_fabric_version.isEmpty()) + components->setComponentVersion("net.fabricmc.fabric-loader", m_fabric_version); + if (!m_quilt_version.isEmpty()) + components->setComponentVersion("org.quiltmc.quilt-loader", m_quilt_version); + if (!m_forge_version.isEmpty()) + components->setComponentVersion("net.minecraftforge", m_forge_version); if (m_instIcon != "default") { instance.setIconKey(m_instIcon); @@ -372,13 +372,13 @@ bool ModrinthCreationTask::parseManifest(const QString& index_path, std::vector< for (auto it = dependencies.begin(), end = dependencies.end(); it != end; ++it) { QString name = it.key(); if (name == "minecraft") { - minecraftVersion = Json::requireString(*it, "Minecraft version"); + m_minecraft_version = Json::requireString(*it, "Minecraft version"); } else if (name == "fabric-loader") { - fabricVersion = Json::requireString(*it, "Fabric Loader version"); + m_fabric_version = Json::requireString(*it, "Fabric Loader version"); } else if (name == "quilt-loader") { - quiltVersion = Json::requireString(*it, "Quilt Loader version"); + m_quilt_version = Json::requireString(*it, "Quilt Loader version"); } else if (name == "forge") { - forgeVersion = Json::requireString(*it, "Forge version"); + m_forge_version = Json::requireString(*it, "Forge version"); } else { throw JSONValidationError("Unknown dependency type: " + name); } diff --git a/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.h b/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.h index 5cf295f49..6de24fd40 100644 --- a/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.h +++ b/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.h @@ -42,7 +42,7 @@ class ModrinthCreationTask final : public InstanceCreationTask { private: QWidget* m_parent = nullptr; - QString minecraftVersion, fabricVersion, quiltVersion, forgeVersion; + QString m_minecraft_version, m_fabric_version, m_quilt_version, m_forge_version; QString m_managed_id, m_managed_version_id, m_managed_name; std::vector m_files; From 81fedbf03c9218f04b0908b474c32005414e3600 Mon Sep 17 00:00:00 2001 From: flow Date: Sat, 17 Dec 2022 12:55:03 -0300 Subject: [PATCH 019/199] refactor(Tasks): remove 'm_total_size' from ConcurrentTask We can use the queues directly instead. Signed-off-by: flow --- launcher/net/NetJob.cpp | 4 ++-- launcher/tasks/ConcurrentTask.cpp | 11 +++-------- launcher/tasks/ConcurrentTask.h | 4 +++- launcher/tasks/MultipleOptionsTask.cpp | 4 ++-- launcher/tasks/SequentialTask.cpp | 4 ++-- 5 files changed, 12 insertions(+), 15 deletions(-) diff --git a/launcher/net/NetJob.cpp b/launcher/net/NetJob.cpp index 8ced1b7ef..9b5d4f1be 100644 --- a/launcher/net/NetJob.cpp +++ b/launcher/net/NetJob.cpp @@ -123,7 +123,7 @@ auto NetJob::getFailedFiles() -> QList void NetJob::updateState() { - emit progress(m_done.count(), m_total_size); + emit progress(m_done.count(), totalSize()); setStatus(tr("Executing %1 task(s) (%2 out of %3 are done)") - .arg(QString::number(m_doing.count()), QString::number(m_done.count()), QString::number(m_total_size))); + .arg(QString::number(m_doing.count()), QString::number(m_done.count()), QString::number(totalSize()))); } diff --git a/launcher/tasks/ConcurrentTask.cpp b/launcher/tasks/ConcurrentTask.cpp index 8f688f073..a890013ef 100644 --- a/launcher/tasks/ConcurrentTask.cpp +++ b/launcher/tasks/ConcurrentTask.cpp @@ -28,15 +28,12 @@ auto ConcurrentTask::getStepTotalProgress() const -> qint64 void ConcurrentTask::addTask(Task::Ptr task) { m_queue.append(task); - m_total_size += 1; } void ConcurrentTask::executeTask() { - m_total_size = m_queue.size(); - // Start the least amount of tasks needed, but at least one - int num_starts = std::max(1, std::min(m_total_max_size, m_total_size)); + int num_starts = qMax(1, qMin(m_total_max_size, m_queue.size())); for (int i = 0; i < num_starts; i++) { QMetaObject::invokeMethod(this, &ConcurrentTask::startNext, Qt::QueuedConnection); } @@ -83,8 +80,6 @@ void ConcurrentTask::clear() m_progress = 0; m_stepProgress = 0; - - m_total_size = 0; } void ConcurrentTask::startNext() @@ -164,7 +159,7 @@ void ConcurrentTask::subTaskProgress(qint64 current, qint64 total) void ConcurrentTask::updateState() { - setProgress(m_done.count(), m_total_size); + setProgress(m_done.count(), totalSize()); setStatus(tr("Executing %1 task(s) (%2 out of %3 are done)") - .arg(QString::number(m_doing.count()), QString::number(m_done.count()), QString::number(m_total_size))); + .arg(QString::number(m_doing.count()), QString::number(m_done.count()), QString::number(totalSize()))); } diff --git a/launcher/tasks/ConcurrentTask.h b/launcher/tasks/ConcurrentTask.h index 30b2ae4d0..b46919fb4 100644 --- a/launcher/tasks/ConcurrentTask.h +++ b/launcher/tasks/ConcurrentTask.h @@ -41,6 +41,9 @@ slots: void subTaskProgress(qint64 current, qint64 total); protected: + // NOTE: This is not thread-safe. + [[nodiscard]] unsigned int totalSize() const { return m_queue.size() + m_doing.size() + m_done.size(); } + void setStepStatus(QString status) { m_step_status = status; emit stepStatus(status); }; virtual void updateState(); @@ -56,7 +59,6 @@ protected: QHash m_failed; int m_total_max_size; - int m_total_size; qint64 m_stepProgress = 0; qint64 m_stepTotalProgress = 100; diff --git a/launcher/tasks/MultipleOptionsTask.cpp b/launcher/tasks/MultipleOptionsTask.cpp index 5ad6181f8..034499dfa 100644 --- a/launcher/tasks/MultipleOptionsTask.cpp +++ b/launcher/tasks/MultipleOptionsTask.cpp @@ -22,6 +22,6 @@ void MultipleOptionsTask::startNext() void MultipleOptionsTask::updateState() { - setProgress(m_done.count(), m_total_size); - setStatus(tr("Attempting task %1 out of %2").arg(QString::number(m_doing.count() + m_done.count()), QString::number(m_total_size))); + setProgress(m_done.count(), totalSize()); + setStatus(tr("Attempting task %1 out of %2").arg(QString::number(m_doing.count() + m_done.count()), QString::number(totalSize()))); } diff --git a/launcher/tasks/SequentialTask.cpp b/launcher/tasks/SequentialTask.cpp index a34137cbf..b2f86328a 100644 --- a/launcher/tasks/SequentialTask.cpp +++ b/launcher/tasks/SequentialTask.cpp @@ -17,6 +17,6 @@ void SequentialTask::startNext() void SequentialTask::updateState() { - setProgress(m_done.count(), m_total_size); - setStatus(tr("Executing task %1 out of %2").arg(QString::number(m_doing.count() + m_done.count()), QString::number(m_total_size))); + setProgress(m_done.count(), totalSize()); + setStatus(tr("Executing task %1 out of %2").arg(QString::number(m_doing.count() + m_done.count()), QString::number(totalSize()))); } From 6e07c11f651b590a9223db550218eb2c827a69df Mon Sep 17 00:00:00 2001 From: DioEgizio <83089242+DioEgizio@users.noreply.github.com> Date: Sun, 18 Dec 2022 11:03:48 +0100 Subject: [PATCH 020/199] fix: exclude unused tls backends makes bundles slightly smaller on windows and macos: - qopensslbackend will not be used neither on macos nor on qt6 windows, so let's just not copy it - qcertonlybackend won't be used and wouldn't work for prism anyways as it doesn't support some features we use Signed-off-by: DioEgizio <83089242+DioEgizio@users.noreply.github.com> --- launcher/CMakeLists.txt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index 439feb443..a0d92b6ee 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -1166,6 +1166,8 @@ if(INSTALL_BUNDLE STREQUAL "full") CONFIGURATIONS Debug RelWithDebInfo "" DESTINATION ${PLUGIN_DEST_DIR} COMPONENT Runtime + PATTERN "*qopensslbackend*" EXCLUDE + PATTERN "*qcertonlybackend*" EXCLUDE ) install( DIRECTORY "${QT_PLUGINS_DIR}/tls" @@ -1175,6 +1177,8 @@ if(INSTALL_BUNDLE STREQUAL "full") REGEX "dd\\." EXCLUDE REGEX "_debug\\." EXCLUDE REGEX "\\.dSYM" EXCLUDE + PATTERN "*qopensslbackend*" EXCLUDE + PATTERN "*qcertonlybackend*" EXCLUDE ) endif() configure_file( 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 021/199] 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 f02b56958..c5a95dbe2 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 022/199] 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 c5a95dbe2..8765da93b 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 483a5b6cae314cf42c3977406f3927d88c0540ad Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Mon, 19 Dec 2022 15:44:05 +0100 Subject: [PATCH 023/199] fix(nix): use jdk17 instead of jdk See NixOS/nixpkgs#206806 Co-authored-by: Infinidoge Signed-off-by: Sefa Eyeoglu --- nix/default.nix | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/nix/default.nix b/nix/default.nix index 6050fd37f..b188504da 100644 --- a/nix/default.nix +++ b/nix/default.nix @@ -2,7 +2,7 @@ , stdenv , cmake , jdk8 -, jdk +, jdk17 , zlib , file , wrapQtAppsHook @@ -18,7 +18,7 @@ , extra-cmake-modules , ghc_filesystem , msaClientID ? "" -, jdks ? [ jdk jdk8 ] +, jdks ? [ jdk17 jdk8 ] # flake , self @@ -33,7 +33,7 @@ stdenv.mkDerivation rec { src = lib.cleanSource self; - nativeBuildInputs = [ extra-cmake-modules cmake file jdk wrapQtAppsHook ]; + nativeBuildInputs = [ extra-cmake-modules cmake file jdk17 wrapQtAppsHook ]; buildInputs = [ qtbase qtsvg From fc11dfd6b4f447e79614076cbea54a7eb07d1ee6 Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Mon, 19 Dec 2022 15:48:29 +0100 Subject: [PATCH 024/199] refactor(nix): use tomlplusplus from nixpkgs Signed-off-by: Sefa Eyeoglu --- flake.lock | 19 +------------------ flake.nix | 7 +++---- nix/default.nix | 8 ++------ 3 files changed, 6 insertions(+), 28 deletions(-) diff --git a/flake.lock b/flake.lock index 7c0bb2f8a..6aae71f83 100644 --- a/flake.lock +++ b/flake.lock @@ -52,24 +52,7 @@ "inputs": { "flake-compat": "flake-compat", "libnbtplusplus": "libnbtplusplus", - "nixpkgs": "nixpkgs", - "tomlplusplus": "tomlplusplus" - } - }, - "tomlplusplus": { - "flake": false, - "locked": { - "lastModified": 1666091090, - "narHash": "sha256-djpMCFPvkJcfynV8WnsYdtwLq+J7jpV1iM4C6TojiyM=", - "owner": "marzer", - "repo": "tomlplusplus", - "rev": "1e4a3833d013aee08f58c5b31c69f709afc69f73", - "type": "github" - }, - "original": { - "owner": "marzer", - "repo": "tomlplusplus", - "type": "github" + "nixpkgs": "nixpkgs" } } }, diff --git a/flake.nix b/flake.nix index b1e07c910..5615a758f 100644 --- a/flake.nix +++ b/flake.nix @@ -5,10 +5,9 @@ nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable"; flake-compat = { url = "github:edolstra/flake-compat"; flake = false; }; libnbtplusplus = { url = "github:PrismLauncher/libnbtplusplus"; flake = false; }; - tomlplusplus = { url = "github:marzer/tomlplusplus"; flake = false; }; }; - outputs = { self, nixpkgs, libnbtplusplus, tomlplusplus, ... }: + outputs = { self, nixpkgs, libnbtplusplus, ... }: let # User-friendly version number. version = builtins.substring 0 8 self.lastModifiedDate; @@ -23,8 +22,8 @@ pkgs = forAllSystems (system: nixpkgs.legacyPackages.${system}); packagesFn = pkgs: rec { - prismlauncher-qt5 = pkgs.libsForQt5.callPackage ./nix { inherit version self libnbtplusplus tomlplusplus; }; - prismlauncher = pkgs.qt6Packages.callPackage ./nix { inherit version self libnbtplusplus tomlplusplus; }; + prismlauncher-qt5 = pkgs.libsForQt5.callPackage ./nix { inherit version self libnbtplusplus; }; + prismlauncher = pkgs.qt6Packages.callPackage ./nix { inherit version self libnbtplusplus; }; }; in { diff --git a/nix/default.nix b/nix/default.nix index b188504da..82ba9c7d0 100644 --- a/nix/default.nix +++ b/nix/default.nix @@ -16,6 +16,7 @@ , glfw , openal , extra-cmake-modules +, tomlplusplus , ghc_filesystem , msaClientID ? "" , jdks ? [ jdk17 jdk8 ] @@ -24,7 +25,6 @@ , self , version , libnbtplusplus -, tomlplusplus }: stdenv.mkDerivation rec { @@ -40,6 +40,7 @@ stdenv.mkDerivation rec { zlib quazip ghc_filesystem + tomlplusplus ] ++ lib.optional (lib.versionAtLeast qtbase.version "6") qtwayland; cmakeFlags = lib.optionals (msaClientID != "") [ "-DLauncher_MSA_CLIENT_ID=${msaClientID}" ] @@ -52,11 +53,6 @@ stdenv.mkDerivation rec { ln -s ${libnbtplusplus}/* source/libraries/libnbtplusplus chmod -R +r+w source/libraries/libnbtplusplus chown -R $USER: source/libraries/libnbtplusplus - rm -rf source/libraries/tomlplusplus - mkdir source/libraries/tomlplusplus - ln -s ${tomlplusplus}/* source/libraries/tomlplusplus - chmod -R +r+w source/libraries/tomlplusplus - chown -R $USER: source/libraries/tomlplusplus ''; postInstall = From b98b4f10279eb99be0d663afec1dcfa65c9d9205 Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Mon, 19 Dec 2022 15:49:01 +0100 Subject: [PATCH 025/199] chore(nix): update flakes Signed-off-by: Sefa Eyeoglu --- flake.lock | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/flake.lock b/flake.lock index 6aae71f83..051e1664c 100644 --- a/flake.lock +++ b/flake.lock @@ -3,11 +3,11 @@ "flake-compat": { "flake": false, "locked": { - "lastModified": 1650374568, - "narHash": "sha256-Z+s0J8/r907g149rllvwhb4pKi8Wam5ij0st8PwAh+E=", + "lastModified": 1668681692, + "narHash": "sha256-Ht91NGdewz8IQLtWZ9LCeNXMSXHUss+9COoqu6JLmXU=", "owner": "edolstra", "repo": "flake-compat", - "rev": "b4a34015c698c7793d592d66adbab377907a2be8", + "rev": "009399224d5e398d03b22badca40a37ac85412a1", "type": "github" }, "original": { @@ -34,11 +34,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1666057921, - "narHash": "sha256-VpQqtXdj6G7cH//SvoprjR7XT3KS7p+tCVebGK1N6tE=", + "lastModified": 1671417167, + "narHash": "sha256-JkHam6WQOwZN1t2C2sbp1TqMv3TVRjzrdoejqfefwrM=", "owner": "nixos", "repo": "nixpkgs", - "rev": "88eab1e431cabd0ed621428d8b40d425a07af39f", + "rev": "bb31220cca6d044baa6dc2715b07497a2a7c4bc7", "type": "github" }, "original": { From 8cbec3d5a0604e75b52c57fcd4ee958935bac921 Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Mon, 19 Dec 2022 16:24:13 +0100 Subject: [PATCH 026/199] fix: update installers-regex for winget releaser again Signed-off-by: Sefa Eyeoglu --- .github/workflows/winget.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/winget.yml b/.github/workflows/winget.yml index b4136df5b..ef9561cf1 100644 --- a/.github/workflows/winget.yml +++ b/.github/workflows/winget.yml @@ -11,5 +11,5 @@ jobs: with: identifier: PrismLauncher.PrismLauncher version: ${{ github.event.release.tag_name }} - installers-regex: 'PrismLauncher-Windows-MSVC(:?-arm64)?-Setup-.+\.exe$' + installers-regex: 'PrismLauncher-Windows-MSVC(:?-arm64|-Legacy)?-Setup-.+\.exe$' token: ${{ secrets.WINGET_TOKEN }} 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 027/199] 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 1ba5d0e48..fe2cb647b 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 028/199] 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 3f313ee4a..eef4fd7a6 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 029/199] 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 eef4fd7a6..ff34a168d 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 f979e4571..e1ebfff32 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 030/199] 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 1262ce8e4..02c021cf2 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 031/199] 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 fe2cb647b..f415741d9 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 032/199] 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 02c021cf2..353731896 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 033/199] 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 241354cb8..6a22ec2f4 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 4ddef6d49..7442b9552 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 41ccd1db4..6f1abbffc 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 ad444e6b0..1be2a3f8d 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 fff21670f..4b7567665 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 c636b2367..6925ffb4d 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 413b2f853..08ab8641e 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 74cb5a058..c98f1e5a1 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 034/199] 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 f415741d9..dd27ba30d 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 035/199] 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 | 6 + .../DataPackParse/test_data_pack_boogaloo.zip | Bin 0 -> 898 bytes .../DataPackParse/test_folder/pack.mcmeta | 6 + 15 files changed, 622 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 a0d92b6ee..c12e67409 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 000000000..3f275160c --- /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 000000000..17d9b65ec --- /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 3a2fd771b..47da4feac 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 000000000..8bc8278b7 --- /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 000000000..ee64df461 --- /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 6fd4b0245..18d7383de 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 69dbd6ad6..d0c24c2b1 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 adb19aca9..e4492f12e 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 9f7aab75f..1589f8cbd 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 630f1200e..be33b8db6 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 000000000..7307035fc --- /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 000000000..5509d007b --- /dev/null +++ b/tests/testdata/DataPackParse/another_test_folder/pack.mcmeta @@ -0,0 +1,6 @@ +{ + "pack": { + "pack_format": 6, + "description": "Some data pack three, leaves on the tree" + } +} 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 000000000..dbfc7e9b8 --- /dev/null +++ b/tests/testdata/DataPackParse/test_folder/pack.mcmeta @@ -0,0 +1,6 @@ +{ + "pack": { + "pack_format": 10, + "description": "Some data pack, maybe" + } +} 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 036/199] fix: force add of ignored testdata files Signed-off-by: Rachel Powers <508861+Ryex@users.noreply.github.com> --- .../another_test_folder/data/dummy/tags/item/foo_proof/bar.json | 1 + .../test_folder/data/dummy/tags/item/foo_proof/bar.json | 1 + 2 files changed, 2 insertions(+) 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 000000000..9e26dfeeb --- /dev/null +++ b/tests/testdata/DataPackParse/another_test_folder/data/dummy/tags/item/foo_proof/bar.json @@ -0,0 +1 @@ +{} \ No newline at end of file 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 000000000..9e26dfeeb --- /dev/null +++ b/tests/testdata/DataPackParse/test_folder/data/dummy/tags/item/foo_proof/bar.json @@ -0,0 +1 @@ +{} \ No newline at end of file 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 037/199] 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 3f275160c..6c3332857 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 39023f698..8b00354d8 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 f336bec4c..b6d264fef 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 dd84b0a3f..176e4fc14 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 ee64df461..9f6ece5cc 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 774f61145..e8fd39b6c 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 413eb2d18..c9512166a 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 000000000..e69de29bb diff --git a/launcher/minecraft/mod/tasks/LocalShaderPackParseTask.h b/launcher/minecraft/mod/tasks/LocalShaderPackParseTask.h new file mode 100644 index 000000000..e69de29bb 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 038/199] 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 000000000..b8d427c77 --- /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 000000000..e6ee07574 --- /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 9f6ece5cc..54e3d398f 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 18d7383de..2c41c9ae3 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 e69de29bb..000000000 diff --git a/launcher/minecraft/mod/tasks/LocalShaderPackParseTask.cpp b/launcher/minecraft/mod/tasks/LocalShaderPackParseTask.cpp new file mode 100644 index 000000000..088853b9d --- /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 e69de29bb..5d1135089 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 039/199] 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 000000000..9a626fc1e --- /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 000000000..f48f42b98 --- /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 000000000..5405d308d --- /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 000000000..441537359 --- /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 040/199] 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 | 0 .../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 | 0 .../minecraft_save_4/saves/world_4/level.dat | 0 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 c12e67409..853e1c036 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 e6ee07574..a0dad7a16 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 f48f42b98..f703f34cb 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 5405d308d..b7f2420a9 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 be33b8db6..9f84a9a7b 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 7307035fc..61ce1e2b1 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 000000000..7df105c61 --- /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 000000000..4a8c3d29c --- /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 000000000..e69de29bb 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 000000000..e69de29bb 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 041/199] 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 e4492f12e..38f1d7c1f 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 441537359..aa5db0c2a 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 568c3b633..4192da312 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 0771f79f1..4ddc0a3a1 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 042/199] 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 6c3332857..ea1d097be 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 47da4feac..87995215e 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 1d441f092..2b1bc8d04 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 3a1c729fc..498e1d6e4 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 043/199] 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 2b1bc8d04..204d5c1f6 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 044/199] 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 204d5c1f6..b62d05abb 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 045/199] 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 6a22ec2f4..855ab4007 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 7442b9552..c8a1fddc3 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 6f1abbffc..1115ddc3b 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 1be2a3f8d..bbdd73248 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 4b7567665..ca368d3b7 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 6925ffb4d..6f8591a18 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 08ab8641e..d200652ac 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 c98f1e5a1..0020c4619 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 046/199] 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 855ab4007..29467c3cc 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 bf93b3c5b..96ebd9a2d 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 2a6504a2a..8f9e569e0 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 047/199] 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 13a860d9a..d4905a901 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 048/199] 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 d4905a901..b2d565e4f 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 049/199] 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 b2d565e4f..967089603 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 050/199] 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 ea1d097be..5c58f6b27 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 17d9b65ec..fc2703c7a 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 8b00354d8..3439b6eed 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 87995215e..876d5c3ee 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 b8d427c77..6a9641de2 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 a0dad7a16..ec0f9404e 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 9a626fc1e..7123f5123 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 f703f34cb..5985fc8ad 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 8bc8278b7..3fcb2110a 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 54e3d398f..12fd8c82c 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 e8fd39b6c..8bfe2c844 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 c9512166a..38dae1357 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 2c41c9ae3..4bf0b80d8 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 088853b9d..a9949735b 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 5d1135089..6be2183cd 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 b7f2420a9..cbc8f8cee 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 aa5db0c2a..9dcdca2b9 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 b62d05abb..79104e17d 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 4192da312..7f2f86bf1 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 051/199] 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 f415741d9..14c5b5e5d 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 052/199] 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 a0d92b6ee..215970812 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 000000000..d0e304733 --- /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 000000000..0fcb30b7b --- /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 47473899b..f3d9ad56b 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 f726244fa..bd6042926 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 8c53f037f..8a26cbe77 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 16afcb427..6fc8d468e 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 b39b93266..6cfa7c1cf 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 6a1eb7a0d..8c3785882 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 14bde47e0..b397b7349 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 738fe1dbe..644c419b1 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 b68c92e03..a5bfbbf37 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 f78fd6200..17ee379b7 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 053/199] 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 de9b6fe17..c7ba9e9f3 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 215970812..4057c876b 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 d0e304733..000000000 --- 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 0fcb30b7b..000000000 --- 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 f764feb6a..643244ede 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 a5bfbbf37..0401df3c0 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 17ee379b7..f49fcb7d7 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 054/199] 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 4057c876b..6ca88ec60 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 055/199] 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 1be8e7e30..9cd0056c2 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 16d2bb328..8185c8fc9 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 5aadc2f17..f258ad692 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 056/199] 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 c8ccd2925..40d23f457 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 057/199] 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 3e8e10a51..4390eed94 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 ac8937258..1e3a60d96 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 79104e17d..0a91879d8 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 058/199] 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 853e1c036..9826d5435 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 000000000..244b2f547 --- /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 000000000..b3e2829d9 --- /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 0a91879d8..dc69769ab 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 059/199] 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 dd27ba30d..9f2860140 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 060/199] 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 244b2f547..19ddc8995 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 b3e2829d9..b07a874c0 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 061/199] 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 11e3de152..5dd551eea 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 1d37224aa..d0a5ed314 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 af2ba7c80..18f5f2ac6 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 7450188d5..b80db99a8 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 b064367d1..ef86ed7e1 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 062/199] 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 ef86ed7e1..a88fdb542 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 063/199] 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 18f5f2ac6..1c3989f61 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 064/199] 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 1c3989f61..a870c01b8 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 065/199] 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 a870c01b8..6a823d5f4 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 066/199] 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 6a823d5f4..4b7c0f839 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 067/199] 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 5dd551eea..9741fd95a 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 4b7c0f839..24b261ba7 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 a88fdb542..1b9861842 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 068/199] 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 95feb4b28..df713a725 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 069/199] 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 677bc4d66..75be25b2d 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 070/199] 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 e55faf15e..7ed9410b7 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; 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 literal 0 HcmV?d00001 diff --git a/launcher/ui/pages/global/LauncherPage.cpp b/launcher/ui/pages/global/LauncherPage.cpp index cae0635f0..bd7cec6a2 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 c44718a18..ded333aa4 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 071/199] 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 7ed9410b7..87e709355 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 zcmXtfWl$X7)Aiy67PrOSZE<%31P>0u7I$}dcXtaKBoN%)g1ZF`?gaOj-~X+rrf$vD z{V+9my1M)H>C=(Qiqa@Z1V{h?0OgB}qzV84mGiN_Bf@{|fNZ~ueca%U7HYv=}Gg_agamE=%(qaS$33{ z6KuvgE5k8MhX6~LOjQVgHW@JTA0pLmU(R3J(4(~(_pSc-0LCBy)Dbo4~%M59vr{L!i7$q3gfW8$78tzkctHHIpt zfD@<=!T{tR7(*4az zk`UoCL)BU<#3#YI>ucz&Nst61bxI0QvbeE^ow&Mju?^Z#*ZTDU@P-*fympcPI!S!- z@s$B{b#BO2kl{jCo&b}HY#wU^XR<7sA*AOHm51+n?n;~JaF2a7B?}7E{FRMwZL?3$ zhMdpxtz-tn0nWi#@%|YcZWWppVDvy|)M_phkT^XY1vEyWV-3oj8avPzMTi>au{>#e zld+_$QRHUQC9fvVwF-upH~~XLBp?;A0cXefQZjQtKVS3Vn=AS%u1pW3J0U1+)+@ zJPa@ZVt$%7wkuwI~bjxKHUQbWCXF(kK+ zHB@J)5Ky-Nl$!2_f`AGH{V<)w@5SOsLQgUP0D9^aAUO~w#+grq<68x`9Bm5lT^6Rh zTja)!Uumm5S8qNQi*2~E^uTN$YjDNs#(vE<{S(a-;D*1cJ&dg>m5qDnal^1e*9jI6 zK7eG7^W(@zvL%9eziTZB_FK@6j95hOV=lZm6{zRF$IHiu=FXf0CJ6~GfC5m}_W+N8 zh)GI{xX^J%000j<=K2^9qmL{AUX;wCw=e~|VNpd^6h^Wb-&k42OOg{|3yk55-=o>P z91TIY30ZvKrP|?}}UnuwUeL-o=fD%XsJV2C@IdyUHsIJbu zVNnVC0UG3O)@^XkBr8I`X0s9_&SjK&XN4XH`dZLPQw>ENwjKpmkbK|awfln}R*^yg ziP2MI!kI+tbAW*oYWnLm2Sx-mz{~DoVn$NQ_fN~x+kT5K%6Vc>5e?T#zkj0bdJ*Y)p#b*w!iA?`ii03q&weE;_{>uW?|IHw2Kh`=-LszJ z{ywNUPc0t0r#=m?en$aUIMCFM?RmApR$|-_^L*MgZs|LK@)PzfREM=(M~4}vf>s+Xf#qg?izPOld2Yjj7qr>yqk4) z8H_IGNK#0O&bE_gYM!F&b;>w9Gj{Vw5rqD`!0+KS!mSVVFaS=!1@!X893Z=_Ne6 z5K5?mz<|2r&0=6L3Ti(%BgCmkYy8)lFb{5fjOA)epMreV*<_v|yU1L{(lKCiimcLc zhY3LkX3JRe29=^o(b{%t*7IaVJ@z8(D-6NVk6#4FV0o-kQz!uF06h$9j&r)%2-ey? zCwh5k{jhU~7%>Q)zq}@0mfiwvl*2K{iPF!>H0CCg0gYoo;drjD<$O>kTF3d4`6=X) zeuS(%JU&UMBKP-w{9St;-qgc}^(pzcOd_m&@*;1LvHdx<{!#p(!Kx;USLc9!Dx^gd zaprp`G~>hsS(6R1(TXq~x)l|MWB_z9z7l!6nSc|XKJt2M|9B#d=wUENP*RfoScAga zq~+F*Q-zCZoMV|%#`CaAIls~c+LaHXo0}_zFQf4b$Dqm;)aHH7e2D%~EB%k5$Oy|4 z`b)vA{Cwj*qr?{{IB`VYrhfT;^iY*V?KA~Ig)%)fOa_EZja(c*hz-63hiLdSd#)5y zUH~L(D~2-nyzj??wc{Hcp3!w$_2C6+ND5|&3Pyl^j-nOk9&D*hYzU=ZG^&PfvzQRfZ?s+69B(O*m$(X=~}YE~Ja>V_FpHTFUIha6`!b>Ksf zK3f}m_Zptz7=g$`=Tgf};^;x1fGxU`7D&eKGO8{Q>f%Cd;q3{=nA_ZZMe-#BNeZx+ z58;a8+mfM-%idP0x8V({@t7;mjQWy(Sm3rPh*T)}aYY+e3{Z{D$qD+1oyLm56CV4a z=M8sKJO3rymeVA;y62$vy5tWN`f9Zl-B zFIS5haHBmQQz?^QN2BgHM(y}fHvKrKanu9 zg`B?d$*iP31A)V0G31?hM7!KM(H^Bkh(BBqjS)!tdlCHc>Yqu(v{(Wn3Q~|vLBgN5 zYF-QPXRWEeum28xFr~%)Mj70bAiMV}Ybp{^vqq}6UkMyh6g$~;#=1H|nUl4qjdk|F zatC|7r7E`(3`5jlh@p$EGPPtOZ{v;~FSiJe`-EuX5243bI+Q^1lz)#tAXwVQMSu9B z&A)8TFXN{-gd`r5XTLES^fkw04e-KxSsjLWaL3@*&}kyrjA8;K(9xIlzPTIU-rgG- z_VaOactNwi-Ch`;XG$wqVE48FjEPK!$*}Z5tKKke8g^10_OPj~^zy_W*rDd#VgC-l zB+On4p^1Z(Wqa=#9E{kiHXHDH^!+I(*Bn`9k64s%;7>yB%_nnuCD24d?0x>8(0S|M zl7t5`O&%B|i9Pff+8tK<|GVkm<2e>O`v_$bwm6IE0ZLdixdR!o5g}=Z=Rvwn>3 zt>TUCpHCEhM`fhF$o@Bg4i4$w2(e6V6+$;7hl^MlV6~Ld3IWItdiEatG~ayn&)^NO zJn}D?H~8ZDBkt3|1ryd15fV1p4Js^cM%-F2Qe?^mMf}kJY@5efUEIdNd~oe4cr~B= z(6{-*MN>vY-j7~tSuuSbU-oDI0;&JuC13D%n(&uj8D+&QQ$Q?)7VsjB(HRY@FSkM$ zH(V-7-%cid$KH62TdOtOW5`HTACs{@5Iq^%B?p9oYi%6R1>8>u+HMJzuLMK%;p$wF z2F0OZQPd~DI~=Qz6N$tuPctm9^U*1g8BGrqPzuM+F2H4C^n9wtvMgS*k$$_WERV?# zK=MJype+)sR`ZXGy!c4%FP{P@WC7jwI(Kj}Y~N%*p#%`ZBflCVO{2xq^J$25O*POq zrueG-KD#pMV~Hb?aC%jwdY1mkiBN?rv89|P6(Aq{5zpx zF+nBpHrDW#n6$sCT_hAovlHv83&Voi@ut#0>^s^UchCzb-9m5;^3VP-0pTe1rOoKm zu-u4S)F5P-5>;u3)3JBrviCb@#8PRf5<_in-C?+bIvWcRo}L@ux$i}1%TYCt(c=iB z%~(y)1lqqX{9blu7k5`NEHJkhd<=2|4Q5zk1G9PTh3DMbb2{lVkJ}3hUh@fHQc0v< z8-D9r^0YeU8h#mAr4xGJbs_9?=}X!ZwY_K9*l=)+RGv=LJYy({5&$|FaWStoox?3| zYh&Z%`#PafSt1+02mL$wdj@X<+EL@$mdic2pij}UM5)WIZ3R5Hq-Z%5N1~M1ApdwS zZ;*o>l1`Xmj`PqmKlR=d5!NPw>MvWXBfcxDTZWk3jO9$@K2lZocL``0V2}ML@|yip zhz5RUkOoqWb7=~d#p{$YgP@N8gcj(K2x~YcC4H@C&Xll}jY~M}kEdopHm=FpH8gVm z*XBg5VtDyENwY!tT{nGHVkO93DxM)bbVHE|gQ-)+-$0<|9gwxX2+X~`2fPmD5m)Iw zHz}2KaOA;wLzndKp*Pj$R-$evlP63elzPv;7;3r~3@FrF>XA1UjS(9KlyzU5#`xcp zChB*?FxGVAG1~ixq#0asx4=~PC?LR!E9dlA*uSCZzg`}EXEWQ7 zYWZ!oUa8vC9W}u4iY$x|9YbAu$6Pf#_1aQUOoBA*)}stOx6&Q=O49r5a%ujljG5^7 zWb-3gbOI!?^(gTCxPdTZ(rLyg`_&m)Ss{4vbU4v;yVr=a)UekabmNLf76;S`f5ODo zRgAd!P%wTPaQW51FDEnd7~efW9_Eav*#T@c1-D4mjpL4gXV`GKapdx_byh9Z73)dh!ojB9eKM>v*8=ZqZO+t<+CuOR|(I3 z)O^|{w|83Ou{gtu1>SRT5%(xOqH49P%0}DQ3~HH5uLoXL!?Cmp+RZSNJxFS1GzDXx zDag)mdRryh~0j!Y7ZZYnY=h8d#?~ zxcIWA(Orz2k+JUn1QK`#Xte993IXJzqW}(8VH6r5Q~OgcsO`avN_w&Pi>cM;@qmB6 zF~YCRg6|0V0lasDO6Hagvf3{aAPIk7W&XTHo6j8!NxMC~Y^;pz0d>e(#UVnEQ1yo{hj zRWP~^#3}(LSILM9ctX}w74X*9WkywPAUVBPBW&%ANW+p}GCx;iR_1;du{$CaQv=5^ zWq&(2s3iOL`81vHT^vp(F{Re9F`e*W9?iFgS=LZs8l|HAqkkMZ zg4rJ##Pl;3>NA}DjtuL!*xet@BU6(-+4-R?2ZbrH5lnNxoLYMWC+bz_D!s(8UatnO z@~I=wesm?;fMC1#5Z;DmE^%}tGT?a+pf%Wo2jJ{`!|q?anc8p6c-K2cg|1)$DUx83 zP!BuAqwU5mJ50kWvFQ?M+2k_Ehv@Eb4ucnFGLg8MhMw>!YrcZArj4weRe97v2 zdEsE4P{=z1C=Tm4Hwi6#UPZH`9TM&hm02t#Pm|x7%YVCH?z+uE{8G68v)lz2j+D4i zbbh~4ho^J$7gLo%GwPKI0c{-d;OQw-P)~SO3Y+sGJ(XsS^!*3{I)({yqEVwNcNdId zjA5B~f^IARby*xmPId^AuZ1WbJ&)K)_;3mlTtJ>^4csLk`GC|<6-VDGEFF5%o?vsB z939ryDZ)~|KG5I7l-h8ft-g34Ts=x_$KyKtp6py$3=AI0r$3cH-`+S_J3TnP;)Lf| zM#z@W28^=h=P*MfISyAm;bKhOjhEHNF4;9wmnCD5ESUyeJ*vI*sOm>*Hc>4i`*wLv zN?`FllvSc$PIdpd<&tdkO7N%D$&;bQS>LMa%{hL42j6lS|4jK;A9YDHq^)9Yx@woh zBmi%I{aMR(c$m#;!q&mLpKJ&!3fj#TyEPlYF%Dm@8Y}d?$FAsm!N~isWU6=bUBRV9 z<-=eBi{RFtu(N}$E;Ju>`}S=!CPJbhuw;F538htlo#QetV~Kg{Qd@i5^jtC=WnbcV zR%16e__?{{Fz~p1tJdk}tia7>Vdj<5|Gcb0zv3rZ(K{8;IDkz`$9lOA=VzyH&V|FeB6-PqAz3e=)8K4V2szc`f>cd~jPVybeCF^CpGjaVD;E6i$ngBB@r zeFc8!dxV-9IcNZ6Lyl{K**=m6%0gR_rr0;=Ri62;XB~v*&cYuZ2CN zO8#Q<3twJA>F;hN7FdBWRd(glN8}rQy6ge#x7TIDLaDNkZ04(?!xON%E1n}E8K#0n zMeVnZmEWge-%1h&#+5U@jV$IY%CwkBmgMnvz1{pwYfQK}0O}vP1baO_J8rf5UJ9bg zTEos6UzSL2rs;v1%IK{t&np)H;@?gu&M&0#nH3>3T(bF}q65!@k$X+)5K>ZT3sWQ{ zyX`W}WEnrp(mRlIHiRuc$>&@8x9eS*#4WQ>JgKutA_3@2*dF%VYu~ExMKichJ)k#U zfTj8#NC{AY>CtplG`y(LVG_UN(bB8}BCI_yVhtx#eK-J>C-RLrJ!lDfJSJ<8Blg(* zJK{2@u(w((c>pMUjVEaL-);H0V*l6_#fXvw4BY`TMVd@;D1?b|a`GP$-qb5+;N;_q zK0ycwu+3w;)+&-Fi;5@#(!sLq@4LLc7+$>hhd**X=yNbOwD^=MnWr5a>gN}GAWJV# z>V@?4<)=vB01s(MARx@}vI($I7h{uyR89|QeSkHIY$z|pOAN_%)SX<{Epo(7rvSTuO5i&qb z_Rs6KL62>72XUHZQ#LNMws}Lrb;FoO^rW4Ut(lLBYgKL%TbLNI0iCQ z`Bjf12n+=d{mVrT9IcLzJSrUL@N#apWN1h zue;siI0rwpJ!G%`O2@SXNA9L8;{JYo0CMm$n2d?7OaL$ z0^1coHzgR7{xKkae#gpT_^;u}S147ArAQvpzKHoQ3AfbTw zMJ`yra?prv0PB-rw>_!sAcaZ=3u<+&xS#OyhRDCp16pbI|HZpnB8!L!l3HDJ=7S*X z5{DVCrJeqZcRs3}D>&uIA?qv}dBzsydd*?2WRu1503FOv4e;~g4If7{l^ApQ02Z|I zq}Hb~+8KhudCqrzU))&}YH&-6P=#{z34f-2S;E;Z+zn- zeRC?{_qNm^6U()mx zRt*G@+)Zcb)Gxd|%}8S9G2z1hO@!6nuqeQH5N5zc=KhxI;0;d@T@W|qd=qp^=_+J5 zSEc_?hx?n*qjoUaj@}2p398i%;1NOA&ItWs_cpXHS>!(Ucwj#(&g5=>9Uoia@aB3# zr_I9CQ^Ntgc8{1D3Sdwpdn1b#e|4{r^*5)q$}Kb z^95S*^~l_CjAC|_s#wio9n_X=`Nwn0`?4w9UQCI*U9F(xvGglplXH^{3Nv#VbJ8~8 zX3q(1gFJ_&njqVUCb3qTQZM`49Bo2A=kPz^fTofKx24H?$C>hrJ z87fazON5x~R9!?=fN%PG%vtlh2)bfN8ofqz;lM1S59#%=fn-L0jX}P$O3f(S z)h!$K0p+i(>OexigHpAmCpW#=^KJEMGO)%(Le%fDh!Lw0hT6rIxEi)@3*z{iSDPWs zLw$GDkvOqOoRCM$1dUqxX#BA4ppmTHEET??_?U{?OD}Ibcj`8U-UshgjqcyD1`?8` zDg+cO!Q0vXsFn7uq>(nP#igOp+DzzIbgF7zb)eXq!bM*iRmd-`3UOmfWx}s`V^>i3ZN=0wdXIiAxR=t$@L7LD@&rmQTQrnePeu__DH!F?$Uv6+}EFB&vm)Fgf}bzS)qh zG#aVS_RX02kv8~m(KImg;W0(ObjCIbn`trcE$8nta8<^50#;Z|N+Ny_&^t(F&P&lE@TqdVh{+L(xL4?sHpg>D84J`nTj1|p+Q-EXVQF8}gGKJR|t zgsiW+8sgJn1Lr_r z;@wWdenVML)? zj=pE>_3L%})7x_o;;^~V^9?+M3UF`Y-=vt=*~dSZ;QQMr;i8Dq$K^h4j3VK6%<` zwcFJl;JQ{2b}yX?@#~1wSZmhTm-l&XYfW8Wwobj1D3W;NIM2<|O-@l>`grWIn^cR5 zI=T@EWxMxAxby6LuY23VE^2e6!Toqr65s$(HWTeRebOjJJNWa;w!3Ki+3|%}QTT~# z#0hG_Hlz_{Ys^?Sucj`04DbiSrx=DDJieFhu<`s36QkBtH?p*n76nG4ObpCC?;R*K z?+uoak(`zJiwXm(3$G31#qGa4dK$F7nq`` zahb3$LtA`}g|ga-K&gSP0`13WAM?C%+3GC)HSWgK z`;_TW=&IsCS>5;)uTtq90=Wqi{o|6@_CRCPdi^)zK0aU}fNb?&xqkA3?2i*-B7i7; zY@|79@3P1XRO!7C>^F3qwLleuX(UZOUuL_`BQvVx(L2#;4Q^pV#=;VOrOIS!Ilv5N zfn<59oXG9GYDR80R78u)Z&V+DyW#05+B{N`g%9yUXVW6%f)l{sB4Z!jI0h>q`e!Sy zd&g@oSiYXmSGu_^+(Pj^3Od1#!x2$#BBs90z7TlTdEtv-Y;62N;zc=`YN~05YQRKEzK}PM>**7 zJu2EbDpcgHU#r=u(d_DtiDcJlfqzxVLrNlyACBE2gB#7Fg>BZB<+vu*7OrhHVGq+* z%Iv|JF!5sHI3r-HAuV}&*Dln4OTcFeuXxKfHh_r9NKDHS*Pj zDp2n@rZ7;)e3huFzh)&6Zqfv-DE8NXP?qE;~fo(wwc;0kZ}zA>+8eKut=z` zea-6~^w)yOV{V1dq_$?0DjOMgg^~>(ZtCvR^Sn;Grr2_!t7#pWVUQm1gpQ~|q0c{@ z6dI-e<8$c_`xctvMt36ZF=`F`PhuRX>g%3RkP<|DGa_eJ!8jc#n}U=qBGrHS&V~no zhZf5)Jmf7S+#Wv)+qhI zxFfCh30Hi_*lM2@A_o7!TZCEFS*;i{%}_yc!l>NTFKK^uS&n3*3ezCR=Kg^B0hdVA z3$_WN;Mh0G()ORc9Ai&R&ykDRSES9kq;l?9+g@drCli(2wyX3ZSGZ%GQ?9zSy{!e) zF~$$Jq(f0B=V7_OIg95Hea+`jIX=68twu{g^38de6xn&$eACQ$oKr50SC*xtPdt+S z+m+u=>g}etob&I`d|D|nV0y*0){TBjE=aRdu=?BnH*e{a|SGcn)>08d^}$mTALmwME0Xg_Z0GoHvJ^bGJsQR$shFzsCUI( z)v_CA1+2KMJ7HJwMLe#f{u+-Dj@!5u2=KtOSOOjzg)aH0-CC5Im8m?M_=ISVG)RIscUz1SXL; zsQqy@)+5+eLkb-pO38VN9JMGFFj`$K^M;4taGadHW;Kx%I4CRiYDE{fR9OA6O&S{H zHv0lPzJzdV16!x+RjF~Hz9_U#8R_qoV2rWB1C!APCsV)Vc&r3{WT39dhU41Va{WnP zXx!m^T=gL?{wdSZWVDf>?LKxRUsJVUvzp^kpQlkB=%r`4&UjcfC)rY4Eh9f3>#{Le z)x6_B=^K|3&kO3%a;~Ca-hNIpVf^2N4S$g|6cn!ZfHN9Z0rC!F2TuKN*s({k&Sk4% zt+Y@u@M@#Z#1OF`y>soZXY{ULk&5th(ydLS#h;?D z_Fum>sPFy*sq=Vp455%hJl?TkDCYfS0y*O%T9*p`SZe?oeE|^i@_BLfY=`(&_b}{d>14TX(d>`*hf1X zYl=}H`Kz_m;_Y6Ur?Myqs+2r|L??3gluDVNd|XLfQ=1vhbeR}l?4rQZ|J`Nl zRco$<^^~+2r?j0WPyV%c>mLqJOHI3tkq?vK@d(CFSO9z>IyB?O3v8O+l^dvO4z@G~ z*MuF{6|AGSek?SdA^5->_r73HMl#;dtsq0c?P+@BV(PF)d@)plcV@fcN6FvvHLOd9 zy)=z%ReL$kE;av~9cEg>{NyhznO84FQ9+Vn=jVH4H#_(~&IzZIDzd#y2@b3&cR-o3 zh@${)nxW2bCFK6)zEE%h&Sbj>tDXOvKrfsd3VA&aYU`AQ6+H$&n2VLV6JH(FX;2qS@Wf-Y)JdfVv2nACigLbo!209>>7&GAJb$5*V(G;H(?FQt zJ*iV{M-Ic>^^wM40E4POw?^{cX=^KtYk?n#xu95*dp-2q=9Qvu*ZnN__`VHWdYXo! zZM^m06y_4>hJLV4k+-yfo0@daaMJk(ld34Lg``UeospUE^>3^rbNRgn8DF03Ww2CY{IjEab9@VVM|UD8(8ocxB} zJ|gAiOYgx~CS1cT=#n~B^z~PnI2%BG$uw63wBiMO0)$){`FUVosp9*>!wfcr)1OKa z;21Z32AxgVKT65kPc`@~6{g;m;Lbd^x;vn&$`Kgr?uV_%$Q_8$%S91V^Lh1$I{>lh zb9Q5)iFkqc9?fN**f_BW5LW|0jR7xQtTvM84hPTLLeLi}wlIltY{6jf>}7xBo84#A zXzb~dxF{#EqXIJ?N6yxO`B;}sH!&6P`x-Gg0YGdX#EL9<@$m0_ybx>fy&W@`IcyLi zq*Xes*BoW64RP8IB8-L;R;!0sQ3Ld0XM359H+-1QhJtVnjuk1$Nvyv(e_!U>@$s1Z z?r}Bs&M{y2iVn=k%M`6p7Hz5|bniUS|F44NGMmfdoU^+sTMV8|Iv-Dqa-Kyq6O)YI z<9LPHZPsBj|Bq(#+CyZ(iDz1CIw&(Qkfq5NG|(o)1CJrLKW2KI-*hiO#z zb-HgNuQgdSCd7GIw)tjf(QH{@Rasgc71p_&WOFB%SF?*9j^2@F%5IO+<^u|KMA&(} zpd%O7&`-`QCQXO3tdxs>OU_kL@>+mZr6G^aV%BV4rgJOg`;h}Lv%gfY6gX7 z(u_E(=^bnX4J$k>+6jmo+z)kbdrCl4m}c6zG-YKsM>8`C&Mcy8ba+vF%rO0%@7iAy zKUA$J(@g|!&^@#$b3{8i&Qd+G&0WNJIo-%is~6v0)oo5ZKi}6Enq3xZtlLB;ow#LM zwY{EzC_fq$;{3_IjNxdS_xKPZa?r1nCNYqd>X`zkJ~R`yCd{Eo|gIN*m;AqkEQHR zLLHu@r`xZQs=D^M2f{VPg8H}A9k6{0YhbyWC@4meW?U>5lei~V#K*F|e59MHgciJO zxL%L|6lr0pXW=|TVYADhz|@d$afK;Sj1|rjY1gT9V$?5R^N00>d{a>RItP|X!`Zc1 zQybv$lvTt)&}nC)Y*;YOeu&mJ-aHxV>!xYVW*cASemkFW)XC^ZVUb1G^Ip<+N6mKm zhZ;LMkQ;*F({9nWo6*V|dNa$*-1yZFDo6rY_~SQgBf^EH)Zo+WKH)3LNY&6IOM7xFZnWT_t&dw@t~nJa<;FE1vi?P%ZuPI7S;!V(-LM6s&?A z4m1xlDka_$u7SxiFA$ow`X&{YNHWA8P_VH8n3%9@eq`N8M8m+H2RN*a{SrU4ecY&+ z!;Ff>4xORlv1jccT4C>x0skcc5d!+)5&;)&XKtshhCCqFqMDjndEVOv{`)tUn+??sDGZmgd$YYqb!C3Ii`+BX8wTla zoe-@!^dPpOTED+iF7EEq<)WQ?zpV1GEfSYf^mXhkCjMwQEHu=>V^2%DMG)}_@Pym@ zJ!gCDXO><49t;1@_-g=|k$*fp%Sn+JAfug{5Ik@>|A7{3t7C z9`gchP2w+PSwP}qo`}n?_yd{oXA92v^-kWaara(6X&IvRY_~v;#e~`-Boj(%9%z=F zANpGDz|L>F9EoF1*2_yt_$rRTsMPCA4kt^-1JJc zc49ywmZFR(b9IHSku}j9L5Y?2Fqv+~M90Qz>7pfej@X=rmvBuL*yyscSLUQcH#{UE zqKHyNrQn1(T8A-a`~Ey+w8LBHhnR=!msjvDZ?g_C8-))rnwM9GP+HAYJ6hn{XJP^ zD0$N#yZ+i)Syn6qo`ITlO5_=+FL)_vbcEgJxhp0U)k@wlRdu02=DxlIa$wRfoX_Yajz@D@O)tI-*bqKLX%nGWy(wq~5k2)gLTPb->iS3K+yF!$l<$H^c`hN72{CuMK zp3y-?Po~YW<0Q`gZ&Eai-K3PYIf{KzE>_7J9iWZRQwrkvs>m3?siSj}W;rKi6fHYb zjbB(Tgi3jBzxQviNQ-VmEuL3MmW@o5~V^E?B&aEs8pZA))PAT~ZPdbN- z&R%gqExo-VaAvKBF}PliN1+*rI=KC9y8C0tj_~pGG4t`!#hlJf=-(^wq8IUeC3}9S zBa`^~_BWOXk5SI57EoZ4;2H@B!pe7n$SM~7W6QldV|QW4dVlk*2foNnQFyqL`uv0) zYw`x#k|h)3M!UDQ3!)f?t~$otWC$qb?E8U^aE@|1cN z?m2DA@NSh%LPBL1Is$cE)>)L|xA(eV6fR$Q_kUkUv^p;L_1yAo#zeba*wxfqE;*Hk z=I~^EF)pA$I4bl#5oIGk`)l$jW zeta_~z2x>^;i2*W$1aA!DA9l<_JO!-H+3FO3Au#AisgFKri$_o^osIDtv^NIZMy)C zBQDXj6Ni~!1)mPx+1R)~4dG++pwq#N0IfFof`cRv5fa3-vNb=;`@QFjg?$3U(2~Bf zpLRi*x?gZJDwvpf;%~i$_^r;LI#f|%5KDD=)i1+U4qgm)Brg2fAT~%>Pz@0V(eZotU(c=)eAiz#B+AfPQaa45X&uW&RZ&Tja z#K^~KO0>T>qshwC0owt$RXuTQB0*ONB!dH3n*bWcK%}`##t&l9#-m(E)BQW`zpuhq z6bxnhLmbatFf_pBOut$!%ot>?b)ni>A(@{W@U*GWe~X&pg%B{rD$&WHKgLu-5q;69 zm}q z&WRXd*sc*Lh6S94(Z^HOox3K*>i)#K1S&>UL(I#4U2L+SWo#PU!a+`JtV81)*$(^p z??57`2rXS}SIZLJr*Vz`H~)Kt)KV7MgW9>0Z9oh^S(iEEOeT_;Glfq`x_Ww%%_<+i1_XW0SFYOkR%Md4TLwEnIomge}} zW!s8>qfhN^n8E>Omo&f{dO;KKe4;9UkSZ(W*w~a_eAGF$cG(L zqNu_qt6US+#FNsV5T^K1^M~oCr!hpt4SPVVar=NiEC^bv`|-1b6iN@pP!A{&BRB=B zfQ$EIDCH9bG_4Y(zP}9z6@p`Qv)wIiwjgD)cwsXlOk>cI0W%TZ$U1W0KDgtK3k|#UB)VA6up88m4SNxLLj)`6rCuVS&2$>Egr*;__^zt~BUotmSz| zJVLsGkRuFBEdicv8w>42qTk~O#GaN39g&RPcjmhrox27oK6HhP%G#fV;$>Vy6Oa;r z{tA5*+YWXaTtk<04P0QqE^_pVQs!JQ8d;p{Ir%H(e@v(UbTZX93-vMSRVaBejqZ0P zArfEM+j}QpoSU=o3G zxzM;X8{676IFWkoTrU9dxh?YyJSqO%9mh00KwN5j!TB*LgkICWO{3dnZhsvY+OzW% zM3d#01`V+Kf@15Px40U#2qf$qoh!R6>Gne(bgE;xaKMt#?0^}4+cS(I-w+7sI`i&L z!nf#$WrG?*0b78}(_HNIHc1F{tPv-zS=wvh9}5kU;3d@N;uLy&zeqU1_kwsSwMthE zVM{Fo9OIN390X9oFfi>ugOiV}Z%is#&u>NjgoLq8so4M3eNR%hHuG@FCyY&Ffo@2U zwXKEIm}S9(Ia^&_$o5*S5m>da#-Te2ptZR2vKml96axudV!KeIEyX%wLZ=$lC1C9v z8~PBC_>TJ-!E!C`%BZ#()}VB{F%sqHioIDt%kuc z17%OB`9YE=%V`N~xUSiDK>#Mj|gIIj4t(fVetKn*w zZhn7`4mTLM8OIjf45xw5Q!J?TUlDhf0*t%Vehe0{`SdPi@OvTBlD-n`qYA-kfDkBy zCdAc#Q$OS(RAYA-RkZoXT}W7}w}%|V!FHhyX*c#Lj}Be9RzGX4pcV8QYC)$46to~S zF2Q&kQm37DlB_Ib<1V7ZuD#SnhISY3 zW9`WG%w-w=k<5@E+|pPlKBzVrvbU+b`Ldy9@2G|RX-$VXZBmvC-h6;?>=+Y`)#yKQpptHQYu{FFnG3R5_*D*0iqQy z*pQ#PIS%K#?+l%wX{fPU4`_8ty@zSMxL_^tJZj;0BZV<4|6@YP89&2oXQ~w}E%&Mm^{D$@9DU0dxF;y0!}zKuZ+txRjjhAc8ue!}RJx_;lS0gS zJ6yj2S|NuwzT82a@zK0$!hyc{^tYg3P;N%xD7wT1K}w8+X-%v$G#81)AY0lhc@KWW z0mR3M2$@mHB5hty(HK#z^OL&;_S-ZlY|*}V_l?rf4)B07FHO5$tmor=L^Z?9ksg*+ zhd-jAGo9%fE4Lpm>g=)F3>COIehQ@Y#S8s|p;k#m!68vo#}K6_^{Yjjn+neIg3TlI zH4g(PjHdRA`Jz8_WlSqeB)G25(Lba9jKh3#!;8)+I;QSj$bu8qap;d?^LHt?(@dck zj&5z$ziU=+Pxw_0>1Z|ML^pQ7k=oE^il1OGOkt+4Pjd^v(lEs1ekuYC#?TI{Y|89lIwA85!k!O zgZY120JKIm8T}z{8?Ek_(<^fdT8HWL2BC@_+phyWXqc0Rul{xCEG^wIo!+fqS%ECdO?$pWl=iY0hiXotUU4M6C((OMCC(BNQP5;J|qAY0lfQv z0PsK$zoBlOX6G+F5AKGo7~QlDxb5xeEX<;_vIN9E%pQLlz4i*aQ5P=Lm=G=$aFx)-%9wjbi-3t>AtGj_bgyHQ@R+fU~4RAUzdnUj|r=s`MKIxyG3$ zo-|YwjrL`zFsC5jHv*s_J9^qA7euC?+9N2^gE9RM5G4j#!KS7A0^Y}M!W@77jc>;4 zt1qE*bQPWM3YM3aY7A^E=lnyB;o(~XKd37ub)=p6h9CbG;cY!Q6t!2-TDJ*)y@BSo z{mJizB+*RWJ0)Fk0G?Bn(W|*yskhXdP1mG@JMOp>^+tmz3smGDU8VD`D=3|0&;!Nh zWc7ooi^nH}atn)#IR4U$P)a4NF~5QO*i@#IMHy2%4KmaR1<;V(L4AB38sqDrG%&j3 zhGge16*$xI1WW7o*^6*|A4(}iVGsZAr+yau_wMU+ z-}Qsmt|69YD{xfxU9@%6*u50=sfILp-so}qplGENt2ApXo|DjeV%v>3Jo3sDPjB^t zAaFbvQ5YTkiCfOl9BaUOlaSV5O60Oa<3$LP031Utn(^^ z`qLQu0+Vn|um>yp-5@=NB#Dm`HECh#$kCpNNY72C=M>7~U}=_I(yAh>CV8=!L$jn`|2C|MVYG&ag#bI!Xg0U`eoX*~hj#8--v5sG)qLM&&>BiW2A>;l9cKB*n^WG5Sr-~ znf?xi3r%5buD1{lnwxeY*t#F9J%#zJ)68*Pu4JscD@#ka{pd$rHnO3nHNccin18;P zRFtSjinyhcWMD%#W$%K%q^VT# zm=p;TNIr}Cb|5<|;KEIOeo!tPOmcTbQ5C@z`S06QS<4q*!Q6wNMBw}2oMX5-jDPi`|0W^F z6z=IYDtoP~t9`q-LQ65a+bGS#P}>ev5CkmLfWdO^Waa7ztXNi8c~M!@289HdH{;mk z`1lRAdgF>81mV!gh@+vei{fa*AmI$wKWfQtZ-{e#eXUjt9M^^KdDy*UXEKjsCybI$ zI_p12Ljm-vofPY{Yhx5ABGr;kC{^wDD!>@DQi!7n-1U=Pu*fyDqI9UyG0l5&stQFp z7e$Z>SL8c=N)M*ZaGQ`|5&|P|TC9OCfpzHa+i0bk$|6!lasU+r&8jn%4=fsJs9;%; z23&{u=$M|55wg?-UNR4!@&4hq8Jo%>D+=RD5;skd7etZ3$mY0dva{hd6Z`yH0PY8v zzH|{vDa3KC#!Dlurjlu|E7OqrP9VI{iTQkgM|vG^mu`{()ce^>s%$z0KUfM{Q^Xobz17)Ut_|#1h;v^&(PX zCBIvZ^Ur)6Gmm~5>nEo$HMt(WZU^_i{T&z@YN2}Tf33x;;kT*)Z*Rz0zxT5s=@Na} z!bD-gb9UvTQ8Yuco}~g@rzuRHe~&3Wboj=bc4?)Q2AJpj^*D~Mf1`%G0ZZ+le(iNU zH_%#h0N8!-5Y|mj^`UZF5fl}>HmW71^)(($n~hTfxr>>?$k8Ba@W+~?zHui2p7a)*?95eK0vM_=tqfqnMjXXBAj!N5<0-va6yL|JP!*+Bv`X2oq!F)( zwX@61`8RXrBpYLCF~_J4wxJBG6h1WHkF9f2y2xlZDgyw}lX=Brps}nXpo4ttk+2g3!>yTh*9=ux&9eOKb%?b(AYmPv;URGH-M4nF) z;sA91x4B(HnJ47V(mY=I%pYL15r8pG3Z6h^ZrX-i26WyqQJ z5wI-{MKenhr(zb(Mh&uRhuDLZhC{3Jn^gwcepczZFu>h9wcZ;U9&^%6pblK`Mpe z>-K|-q@nB(+@8{uDMeXPgpaVnOc3)#*|H~t)mR=}&BqmKoi26R(9C=-C7BOO@iRp# z8-XHa*-7mVOu(n`Rwqh+&P6_JqSD0SiegVi=`xdzidULm5|!9dq!=x@Vwx(DTwjM& zS+qJb>@lf<)#;0P;&=W>%pZLSJZNBS<94iEJr9I!a3RoE4i0|T2jRDdvxI1dQibOi zf+UUcSL1Ggz;C9dd&PV{EVMd5#bcm{8cYeFr!f>CL^_2A zYx-Oo=*R?b+IXi`h6#wCNY2$LKT{SNWtmYMXnIvVk;8ZfcB?x%6+5#gsF32Pyo>_j zf!J3|l~iH0M`Ef)p7uw2vN(6+rj?kf&Fh~#&pd|> zQ{!ld8pAig2je%~RcPB8h*avls)~rxwL@lqA`L`wa$Pj3c+951yRwS;Z+!tC2jVb> z@Ax=)^UXMX<4x7GFZM>MGC80czUnKA*(Wief`DVswZTFI>?(z!KkQw#8Pxq;c>CB- zGX@UUVhUli)xyy5NPX77(&TKd-Q*zQ3|4jjW|RxD&2b$r;|PJ{V&A@f6%Um?`Oi?Y z)9oj4TSzSA7<4jGS3HQ?+S;b?CACU}W)A8b=9Fn3XF6jj%E%a4$+3qLL8#Ou0fZ7o zr)y&MWBLi1C?=8AF*el(+Uv%7LNaD(lng{ASX3JDsTP<^48%;Lp}nbYT%3O~y%STd z#R`bY?@6lMtkv|B475lkb0S&vjWGkEE-{mX1|qWY=EjUpN?bkq5YGS2e}V5faD5-m zkzs6N(>OJA2F~ah;5D(~j_-yRE@G9l>J+K4jEaD=i5d#%k>9T*K0=IacxIpZI zI06>Ib3M3@i+8^LUNoCc3tz2#KFVLSeu)OvGEP-{sH&f>RF2jaHqsTFJDPwvicp}v zyQoAND*CS^S#ri|tyYUUbsv)><09U)b^Yaw=SKmozR^P7fHd$=y8j*4CPI&`Z)F7Tbq$6o%ed4y(m+49ysPo@irf6dTC4rE*NF z9!=mTI*&tz9(yt$q;y!3%|&~zlqF@@VGhfKVAe}Q{{D1sM3H$JQ>kUlFIA**o87(5 zpe?8f5NN3z11nLUzvVY$6FkPs6xxgA0=rUR(v^TaBlWSeAelIilrGkE?F z{v%k}!RpEqR=aJ~n{~{deHmIwymtNq!jW}oZP*S^05ul{lR7gWWgx@A0eq(@EGkl} zwyACfVe$1Ourfb`lYjdMXnF#!PlC@Z)*)?7XgN`@i8V`SNdLnk)bgR)rXs|>$e6K&S3pR z*FMMd8iI3AfMfrSH=@~SRLG&DdiT?>s_t8CQ!)W^9U8jZVpyZ(Pkm`JO%?@KYa&kg z4h52cORSWs&?87PFgtf@zpQmq4VY&=qd_Y|3ueWt2v+bn_SBiPCh|n&u%g$TQMU$} zlPQR0TkJ50CH)(zi@{e?zZ_DvnW`jXZ;|h-wBq8V=})QSRDr-D%rs8kk21$H?YLeC z=N|eij5|QL*#grV;~O`?;SQos7mnv4Xl=lrcmE)2!y}ph4?S-)lm3l;;BKx z8LZd04iAqqx!S(duLYxy>vA0{Y}&LrQOeb8KS$*Ow?1DNFmc`IXDWI@sKA!f?>j<( zGX~8VG#6NzzKk$d8FiMwLQyc& z=%PVeqy`KAm+2Q&6D%glQIo`D_lZnTDzm~)KflY1kW}VtLlP<{6@p>>fha4)B-tty zd(oUgm;f+)`Z>JvXTOGeYbZ_J8i5~RX<-4o_V361;u50zW{mAST$-CSpTi^*kIWbG zbU;b5qMT1A-l-EyX#ij{Li?5HP!kNo5ztCv=idF;wPOd_*ysvq$%eah{|AAU;ipn8 zXI780)HBj$bF_X8v%)ilqQh=ww~n4?zYQI$&_paA+i?Vwv2-1mYvBmjaov$dy^2{h zNH~MlXQ@utWsGffT-S-h5L^QXcJHlb=TtGsD!tY#vJ$0vmH55Z4oj-u(INH9EQ01tuMf9458a;gQpIvv(s0gVg(HiQ5(YO{+q#SLxsvjDHH@r1WTyejyapmaOfcYz^1s+yb7BO6J;cahwJEkTktMAXM3WiztwQUfn*C>o^ zXsR?Q&DPj-Q5enE6k`47cJOu}Ml5@t;|7qij8|5cId7dYr0TxD>nwqj?c2N`@w72$^gK4CbN|Tqakjb$hGP+}{@_Pp`HZ~3w z#enNTX^G}_2f&1r_Z6hX*~h+$tIt1%o%j56Ozb{{UZ~PD)o>uegaaW2ICyHJmeZ6$ zV7IJ|q%HLxkYG!al<6gyq{7k)L#90iqYR))u_wu8G}RP^!MK^JNrSNw`?S#zbH0?> zr7Emg9F{8;n52~1obE`t1e^}5^2i`23$S!0`pM5zikw@lusHl9jhH}gB-B0oPn33e9ear*NgN3*+#cGLv} z!=^1;@TS{t&*r#Fa?i%sX^G)CNAcTP8984)pKlL#7zVid%;OH#O;M`*m;F(|hNkDkG z@bKSa(_6k9TW)(dS{t{+bsb2*2Ikk{a18+l&0T2CvfXmV2-CTq7)UbFn!r`eVu$Ix zNaIuqfJp8XLP;txne|gtJ|APCFd#`*G}jw?w>clqio%|h6ks5yOrqc7ZBUe_Dh5ib zD@Wrk*@Kw(4!d2v`p~D*>nww71!rU(#NIm*oO>R<_GtvQIy4iAye1kOwr977U`3A} zFJk#v>Hj9nPM9EiRtU1i zYKK)M3c9(na!R4n(G{Yd)^32=&kO4Vt66WLQGRdwTxAJK_fdAQO17-GSg>WGdO<%ymnHMng(ornD@**xh`eks(hf)&q$~m0<>)*rG z#~;GZ_x&4Och`FnG==~n(pbR?QvNOx%QS&HltuVRqpTW|NvIHS8J1QhbOwl%<1tc| z7&Or(3IK*lu23W*2y*FTNmNU84)IZ$7iQ7K4fD;sai%DZr2kP6`9x0Vj#UEhacdNW zii$2?I)~oLr!h4(h1uz=*!0hS9P9QT#+lDvfnUE2r4(jm4cqQ{KiqmVW7Ih$VVh%1 zRg8V6!gP6Y@Ye3%%P7RciKigqE*6*O!5PQc@F;G${)Y1Ar{v{?@&L28dyl$*4|A=> ztO}xE-;ySvzDm&*l~yG&psMn*Y1Vy?OYU?oIEF@tF+abED2fnw!-kYGA0(W?dVR%B z!VYV3#_C#W&K-ftu?e`YwP`U@8*8!A`bwzxfhy4FVbxZ{lRijfmL~{;bjOwaKzQXW zUi|al1ZK~}Qys)}m%so_)&fYSu+WRpId=jt{O&Jf>C6i_^uhlg&Gp-oz)J}MyeQA_ zu%yXvykI1V04A}CrmEAlYJkNE&mbS38>u1fKNw#fDrqAX&`Fb!!vINoN7~N$#zG)X zW}4?!O{N^ci^7{mE6!oXc81hROAJUEb*!kOpmdRjozcam#LTg8CP}*2LG#A9V(+^? z00v<3hTCy<;R?nkHpAU}8z%PM48~aU-YDu;GD|ZpR(WrMvx>12(vw#jm_K_8mydl5 zZf^x+qvMcKjLloOV)Ld=TxJuM@lb9_;wKV()V z2~~y0Rh6!=%$iC_Lyj3bu8YYH>+$l5m*EEiVh)BF+Xe||uwGxn31bdm$n||Dgn*I~ z^;)fbw=!+{bgU)!0hAh?npN+x3RBfpWxlohBayXPZ(w+09Mdy1h@uE#_X5ycfm^Gi z>v&K~!{r=zZCQ`IHm}2PJ^2F8Ew`cL2(NwV&(N8@jGKP^BN*AfADpp*j6SFmvNbDX z&RnGd&*kM$r7Uk8Q|)kFlz^c#Jxmv!Ld|91$RRNUCz(M~I{U+UK8GuKeyz00+g24I zcn_N!6%V^eID5WquGKwGgzy~ot zHfCXh6_ntr#G$WIt-ylS`fa%EEW!FH63}y~G8e59@938ZnN^nCpRHe|W*<*$gi(Yz zjv?Zh$yh>ZU3(+OzQN$czV85lBLuc?*-|d43ubAs+yhxL8CCUR3Yt&4 z55#8CJgc^H>26dhh2`ZX_#-3G=`pz@5HT0-?px8k^*!K@3(Yttd>404jpCPYy&k*9 zM&!%r%=mEYl2L6mqqt1_MyAgrh}N1z{@*^9S)t55a59x_G87xD^4@}xDvqUGK@hue%O=ckfQf0#&nPrTbl%?_bze5dETG|M433(`ET) z6lj9<{pr9otCSchS1)!Vv27Iko)#;8eg+01IJ(_7I-L$v&`jYC=`cO0aQ>+hJ!8yq z98u#!08xx>TeqX^k&d#EVL{s5;`YROFt3UIU~ zm}y`uZe2Q4U8p>$GlIO(N{qAWroYj7KDDVzjIp=Bo64vXz{#L5J?&G^yJ1 zq38n2iZEZPzd}n^0(EOvUGdnTg&Go32(D_hLFv-xXB_YXADwOo5L~kup(DidAmI$w z>nw5fDC1(BF)rdL!qCtVYC%vj7*-{x!K}!~id%Ws53Z=XvF+rA;>?=T==QpJ>ew^* zzyI*}ar&i~5XTW1=kOX$)OH@ifq(In*nI2T;rKN?_o?3n0MtAe6ZHflWLo38RvrJ- zE&Flbv6FcC{0yQv!qV}hc=R`Z7PtNUeHhz$1Egk!owKmyTueeSryeQd)QRGJix5e2 ziP8Z~L3%f#CE6*dR1BdjYgHrx8aj36xT%YhHw;A`GCD7$ReDY$Z@!xuAEt}LthBEp zl|pxM9#^0LHkPI@;qo^=hl4-yzhPp}%~|Yo7_ho91K;z}QynZe*JIm`gHYfZGzjj% z^_^mpV|j8nvo+@PQP!QZl&ej=b&ylIl?I|X#FZx=f?S$`>o{0mUc$zmdvN6Dn{z6( z&8VbhlwG;l1DbuEIM)jJeFmW`J#MUiOi9R77uhQPR1B^)oLxWxbsvR+A{|jH4HZjx zzK1xDb=2uOp6k6hNH~M_de=ej2<^CzrJ*#v z#@9w#+J@ZfZKv^%sdWsR+jL}x4wcCfBK)$x%eueCA3m_;m>~)w|@AeXs+Ly4#g^9Wgb%- zHlVlYU}WE&@Q21SC`?+jOR>sbIsJ5&CL)!fv=tq6v45lpi;gl3m(QMnym$&i_YedD zTp_T2{d#QOyago+KC75JDOJ~YbY|sy)h>B##%2}FR!A(YoK)5-qcqmma4R4aC|eTf zimX^j#?_KMN^5kxU1(_dwYp{svs|+agC5RcU2`2C8Dos|-Swd#T6&8B03ZNKL_t(m zOG(Azu!pgU3DoO#YaW3WLF`HcDofIx5r-^o)3sPR0reA3(;9R0v-sp+{ssQ(Q=i27 zv#)}s8fpp*<9ELgxBSenV`$gmbdCmCnZ69!UIvuJu*1+|3S7wqc}VRR#$17)KfD*; zzjs>(Rjr&lhNHjri&!{&JPm{lu3)ISsjP}6qhk6rk<2ZkDcX`XzFFSuU@0BC7w7b8 zQ%#slW9ZNdMK+ibvDCU~@th5!5^_T%t=wY#i28FTa%vSMG753w(XT+XuOjSqGP1y)efuypG+e#A zFZ1-P4FIcfDq3OK&x@`Q<)nX#&CNdjA@paZRi1x8WRF!Gw~ygr-~K6}wL+)Uf#>;< zN~*BelVR8!8B{ofbT9xnC zsv*)!b=~CQ&`ROlxpVl^m%oTFJ@{43O;3X{1}P<&<6+ki{48$z{trVmM>4pGGoU+n z397RKForSTL(NI`SkgB$H0p8B!M{DY2YW|{@oP`KfLZ{@D5q?lV>pW_Bhb(3vEck0X&QPy6cz0zL zaTpJ@)CTMIE#bORP_GLH2F?W<^*WSRMG>tm%CZVu+X~S`T`^7FuaQ}ptj>?AmBN`b zui$g{e-;ls{19d?UQFMH!QZe0`~KxGVAosU2jSPUUAlRBcXb&m?1C|dy5MkWm|zS% z#bPuAnw;UgwyeW^r-R=;_A(?iu6*NueC^q9CAP+XfXh#O4LAPCFQBzh53K8!>1LEVSZ8CwddrQ)bwo>lIq0x?#Q&7F=PNbEXpmHQbA^gK#pe; zp;0X8E==Ruzx*#a_t(D-tu>fZa2dn-ngUt;clOL2J`aFwXPO&WhDoM(0M(w4)W5*2Q*;w8#teZ>mEVElxu&%s}?K2ptEE zb2J+bY~Q+d?Q($=*x0^V*Bt7skZ59SIMo(BEm$9?PTMwGSLZ;zhCnFg_S=%P;SXIulECbIU`+!;nz~trVn` z6-dH0n*Um_@jzpZp*f2)jHF?5=FBVj{1@)W*B*HovzIPqZ{cr0fI~m>OW1bjdjZCC zxUJN2!sqDO2iqX3OzJJFioLg?={xg@LWQ^|2CFnSUQ)}SL6A$6h z*-N%#34eZ4${VcFHS2RnN$aoAe0`@ z8|DR;7B9Y+p+{%-2PFQOo{9PSg_n=vnLqj!EIj#l02l-`HnnQ_(4jrJd(#wr&cT($ z_Td(W1uz$dxboOn(Ru&Bh1VJZ59;un!*H7;g(SyG95lTwdM|K74X67LTY4uO05wCX z$o19$hUH7=5Ge&NW2|;oF*Y`a(a|xCjEwZV0g*Gm>`FoRpI@#r8Ke-iUENTH<}4|& zqT(tI{%Q@Gc6$z|Kx@>ba?748oe2fn=d3FdoZYV?pL=m>2@DL4MgyD+)>&O;;c9no z(8C$5Yc3s22f&+M*BjGX!*xAKDWSD08c-E>Kb90nrFt#v@1av^&#Mb_J@fN(c;LaW z;%kpQjOojX3VP=RDIGGXa&S81wXVk7f_sc;JIUC&67K>*XYW z@_K1AsFn}ly&_0r~1TBf2KefE3=jQU{XH0+R($f#)$>09>SUCDn zlF*pOk*N`UQND*%V)sD6*THijE#(=?gcn}(_sk4%hd**AA_zC zr|nazRP`J=L#29iLFpM*FH~2{<@cWuL8J-lVGye*SZFfYk*IO7=ID>WVgrl`9Tz9BBJXDK%A>uefP^-ZaZn4}3isFB| zLM9!S8C0eCAyA-CS3nR-Dm?q#v-rvbU%}<`=K%oqRts@$6u152Z((ZB;oSO|k#&6_s&y$4E`%r7{gTHr$1Xh=Oo!^^rXbEKKUXZ zzp{XddJSXs8sgP96c-rXcLclM^FcU5KnbtlbtP3E*kR{W()w(#ZVngg^e#hlG4Ob|9G+F?2lGHUT`<(GKiFH?&aru=O!4_sP+!}`CxoD1zU}Si>;(kWi{sLCNvDC}4 zHc};f0~AZ#s@{x3pQJ;M-Ph?BR&P~Tt4K3~6Bhh;`dEV4a5y8wrM&q0re|irz_8k0 z1vK>P>dLEv^TFWfV7;!jd-LvHldcf;g(w-KbR8#w)F`gpH< zhZkRZ5hqWcNM=tw4-o`*{j(p%mRsKjo({sO3OaE%JScp4Ba>OFA?-)rC>AgdrWG^; zX2S><+dVXEHOSF**!Yg`!*x4uz|iI$Xl>a8?f3=x8K!d>$}q|VE7F`5ap&|*AA++E>m(gF!9MqI3BSc+|$RKl)Yl zuAT>D921_v|8RIO?%KQ_u5h4{OA@e@8mCff`KFN(Y-sp6wbVs8e-+{UWwb_yljI=@ zK>_){&97XdsM3-Rb&|q5b5LdZwKT*CW6J=TxgCb(^Djen+t5*nc6$|DHtxjGPzyn= z)@Kf-qA0OTAL>hECn=g>)_Oil$)Lijn)EL#6AiDhG3b}{=njijZ1R}(T2oCUs+g4jK~;67)A(!ngw^U3aT@` zqp3wMD`V4|m7xQ2)Azz2zWAjt;M1S}6lShof#bR0j)&%+Td?aL-v`Z{3}R6PZ7|N9 zIt%g)-sG0-JQk#eW;q?!Bpk&BgjCQP7!3k!Yc()Gw~YEtcVpKFe*vx^gP@bz2k%8z;q2=&I2$iz*9nPSYgm4^i zA<%2Lao1gMf$I>6SXppzrD>TZJzeU@*mUYXiG{J^jar$3DxkY6^iNkQI`^YeTbZmM zv$QG+>WcG9zo)gv^xSNMfF#6*Rfjt>e|h4mHzcGsAPs!y*Sfi@S36NJ)JiG1zK`+o ziR1yb2?B)%qfM8#M#8I%r?8eT8^gJC=Wze$KZi4~oJP0XMRRBfqBV{KANUz~BU6bf znatIK9iBrehSVb!z&OL`-oxnwqA=SDp}0dT9G;>E5{fZ=?ZQ?3FONQl(~B$U#u9N9 z0$S-vL7Vq81DFOTqBC@K(319QBxz5@M3Gslyjh>=r=_oniD6r;i$tYZ<%!Bv3Q8Bh zCs=MbKvfE4Iy@OeU})VIAdaDhgA;%D8<>9Nb5JsdRuUfPcx5S>Ut%eArg`##nIV#} zWC z?!fm0I8L&m*uQ6AHSC3oB%^KDG;83Qo<#|D)_nrFuC7?L)h_{x=19DPv_SD(P0m^O zgQ_cpJF9H1nWd1xs^B~Z#u$cLEyPiTPP>iem8Ce65qqNszd?obonLJsgcJ695RQX- zy^i7GAvnTPql7N^akS06L7CH!ie@N?U$S{vIB@>LdAxf1Wpr2Da6A`U_}KKWAH=%r zZ!c(&nE-BYEVa&|m7qD4ig0S2TUY?FB@Z83r*N9YVlTpHU%iB*SLV}%CQ^2E#@1_qF#`!H;$Ksxg-A8R1W)iiGk;TXYtYGD4I|Z$U`4hTm9WV~H<8ZOs66SK z-g71*YJ$8fqM3sDb|q@k8dLj@z!{!^T%Ly*--y<}BUpdqow)qBzlUIH8j(~52^p=E zs*o!Q#YCEf@@(M?0nZT#g@>iH$H9@Z$W;D%K_q@Og~w8L>P-E>h99kAT3+nkYpKAD zink2gn0fA-sCQT42#&F_aqK^M5R5TQPfz3W)yvqoXCGYGMS(_Gn$l<0{R~tXlTk%y zRu%D4VJTe!U)k)peqK71{m~D4D&HR|AuqtR$!WOx)jVSz*!vIqxh>xt_0?`~s+-u`XH^Oo(F_Lzl6&LBQJ?VLLr2-2Y*!ljS z#N^HQps{{i2GeMgZ0PA`G-zIgb|}BXF~OBwwqBfnpC+EdoC%e2(Md%_(5%dg*~rtF z#7H40bDbi>{V?Wk!Nexq_8T#U?tB-*}VEb?rEZu-;&Y@G4 z?b5xIX3;=!xD4oNjipOx3lJBT0*X_$Y??^9CA_SVd)ydwmg{0G!dwHDu-P-=(XhRW zSW3*z&m-_%1dfXxyLREBhrfoeeD%wy`99wLuJ_>1JMV%cMDc*S&%IjGDYK2xY~jy7 ziHG%5Dj;J7Rl1-hDB5Psp}MD97&oX=frh1jSYdNrN`MSf#+bQs6`BEIuP0S3V3!uWMrzVQ`97MBCe}@@!_?Ficot>X;%8Pt1XZG(sIHK#q%}4+j!hdj!gU-t zo(I)l#>}Z>7~8%VjPrtBvuQXBtLtaz#KFioYUAs$JbMWe!W1kM?|IkDH38_kByIuLfKF;8)~$aoz0}YvbqISuu~u>MnLD# znLx4cqY1PqEruH-j-tYGu=U7Y87#<<^ggtdh`7Lr?_#MJq3Qb&re}kkEMPvAP9f23 zfIZc!GxThRM2aT+jGFYTR1wjkcG^JR-!Z>q1H-v2hcc<^i{OA8)ZhmVz^lRY1(YXn zMRxJU`#%HeaP*csn4X!2QZgwuE2{%momfih&=tI9%_<9=N=D{d%+E@yv?ks+=+oD! zYMfS)8tmj^DMXhxkNF8nC2{G}MfgDt%5~z->S{L#f=h4I_%|R8diQ$FsoJCt}xOxBHf7>M6pQ^M-ld#*2R8=p*9O^^Q(y^G~HX;&Xz{DpK6hDqK{^) z6BsfK~6noKm~*>%(` z`FcF?(ex4)m{bbAIH}~iaeiGwS@isId0wSafl-qz9S-=UaxG}BS@AHlrf#ob`QjP4 zE=Me5bi*D#`MbY|)u@L?qY1}xvF*C+;CX&A&?=2;dKg208+BBRx>a!uDsoxn;gw}g z&Dh%22dlt#i;q-)3m*Ibxb4J;a#;@`j$+Ks&7j?FBMw6$Jcl{_jcL9%NI2i=>Lv;P zzVG`%97QbbbyKLJ3c{ux+&(hujFrS~>CQ>Ym^3t4(d2{yTnOyjw;w}8!%#{=%Lof6 zp2o$e|GwaAX`0{6f{$XE2W>}ijBVKg#s!4q;@H(Wfbpysadx$Xk3RYgzHxCDF=OD( zQ4DY1nLqa_92BOO19aluBo)In^N3EFf%G^@8dO@w{M#JIyiTfOg+dXL0(*;CR@0`r zskCT6%SmNz%=yqHX%eETw1Uaa&`Ks%9}`$nz+a!fh@W`iG2D0b1*}9dQqEeMY*OYL zG?sf^^i(3YsHbdS)VY+#L-RQ)d4!Q9wK{VdTt@JHAB-`~T)u)Z zj35|Cdua(9*Kfd{ox3X_;69n5)WfK_xiw1e(pUpMDpoeox3R}qCFv{J5Dt1Hg={2<{B*6V5DfF)@Ch>RoXSVE-I|CPyv zGb>S`NQtI09u|FCNrgCy(xgKk97^ZZ8vFL_#q~EG2Im}FYs5=)c=^l!4YIwE>4|wF z5w;ARyb8!9`$?6te(z1FZ{7*v`FLev5szG)#^1em1uId6Z=S!5tDOiD)UmI+x((!{q}r!;Uf5)2CofJ*K&68wpBGIitRB%u_T<0vXAm;qCha6?&a z!qbo=tgjtHBFF|+6)?Wz0Nk-DK*kt)>-!N7O`+#E5j!4cx-mL3O*UzTOJikM0in`h zjKlGQ?EFpt$}s87|8YXywm0OQIFoX!oS<{Rp^W~`$ZV9>&@zVax)>TAK^(HbNr)Kj{K zDO(lqxvJ8lY?pLnc;)hCa3* z_vAx3`NcoMP2d0FO!vzW1USV@y(tKq66!O+1FaX9q;_lpFqUO~wGB%F!u?cM6v^g>ELp4&C{YObPgOvv(3)23yqChUk zZD)jj7VUH}l+v?pH{Cvm?c-a3Oi;9nMqRKkV8vu*ABL2hi_1$;TB6-v#q{)*>4lk@ zv)`P%+8IS5bYsIN8!T9(%f*`0)HuRoV_U@~y;h6lHpPR#%zVc=K z(Z~M~fA`RX=yki+j;^@vyXnx4*t>ThWE?{&1r>Ji^5;H*3r~L|Q#vraT{G{&t!ltw z02dtV4&IL89S0Bz2XSK*K@i}%nMEACG7G^NY8$tsIklN7RSeP~eG_wvoG{PqhP0daBB^S#t@W@!Ii?6W*w9OqSGwFv<6cW%V7wqlPPq@SXOZwIM7J+&}&1*MLB`U zXkv_H{yUb$-^bypYpJHM*U(W2uQi4!j1Wa3dfhI1y)G0a7FQS1GS$r*=ADJX9$VpA z>oxkBsA$aVGW=}!g0cj|D-;b-T`A#O5TGs&g_gW^?3d~SF`!&+CCAWOqrKWj6h?4d zS1r!Ytul>ozma3$pu+hME)|ACN%`ihmo7$H!_~1wyW2^4_Wf>9`$lIKWj@&{&Ck!{ z6My>0c>K|CAq+#bTCIeXK%=R}+?kx5#M|Hg4!r#GNt{1>7Fubj*$a65cm8MG^3xy1 z=9}+<)GP~RMw81Gqydja;$~vcjo5g{d(oY~l$dcfhJe`%Xt#S%oep;2{w_%2XVhkD zibkj4Bb6%ya3t@^IZz|=Hnb1ZO9Q<~2RMzYu}M~@h%|Q32L3X$uqNOt0)yF*lhP@Z zjujJ;V7WLZ*b6^Dqm!c3=w7iQ)lHQ>1}b__S+rCMs5alEqB zPGhEkQ)^_`&d{}ET_j!zOO!di#@Prkt;h_kjfp~#=AjtCITs9Uc93ue>-8=T##a5H#*4uFO z=#yAlT*T7CB4iXHK6e7ofAV8!jIBd!`~Kn{lFa6q_g4VGTsT8xiJy*?IPmlT87Dsd zUx9Ca2_5EQ?7-~+h*dHtW0I1?lkan4scbgR2C6gG$mU!* z&xc2=9(%g+HWeLMx!>dR^6RLurx6{sH}Vwv%U;I?WVeGT4)GVKR`97)=b^#i368y^ zEqwUKJ=i}s0uA8!{1TY!gK>fJ8{U-6?WD)?M3E;Fi2PcCY!1?%49`d()@+d^_S>{4 zl*eriuhv9kY70h!814BPjEriGjE|%42e|#sZ-Vc6`OQIRP&CS>Xfy#a6n%0EG-VX4 zgQ`l0^|K|=MOdtgRA4bZ%jyVK*`rD^z)q6(Y>>5U)MY)k`Gt8r`ps`(Wn~#by#Wo) zdSP$jjT`_6klJ@}G0rvPeA@GUS3@I?V|2US3d*J~V!iZXq|kt+z5n==PvG0%d<5-o z7pu!Fa6J#xSFd7qWx4DiYO!P{CMNOS-~Bzj>j8o!O=fMDgm z3qgLEBnidzdK6K|HzW*(T)BaI1F5KosME$2U%{vo;`H;+C62Jd!Iq7iaqm0c2|)=1 zHl#}-DaOi)p#UN4GJSb1shNI*nw9fwsFuOey8rvZGEgeOXjKIeD?WGh#hd z<`q2fl`o+e_MnswmlqeF*BbxrLfGvL63$?~t~CXAxV^f1yM}JaSYpG*jkxR1yAt96 zy-WKl%v#kAW4Lhf0`B{*-^8ht$5E@-5yvt7S}mbQhY`F+18=(Rc5sJRZ@AK3q~ILe zwrs=rG*jeRF*z#AF^xRXISj^N;F zvN3FL6UYUblQEE$VOvQuLPT}e1kb1AskDAZMb<@Hu=$(woVjH#r=fati|!qhLX)aX zE7BYBL@I7-LuNYn;t>|SZz?%S2pEn496!L&mOa>d&-Y^2dwvkx@BSWiq8JO$eH&a! z+`4`Y7gySNW@ZU780yy@!2b9BID{qtV3eApuF{1fG( z;dLFW@O@h>m-Uf|s%WBCz9+M0baZ*_q3|pgld)0?pS}OHIC1<%L~)GOm8B@^g%32F zt@~eXFRMWhXRuyhh8KIC9A#^S;P z+M7ehht(pu=%FO5( zazSLxz|bJBk?hQ=u(tVlhK|fgU&bVYP?BPj747X!axpNVEAlEVQu%bhOJ;wvDuEML z7df8MDl>UPDO$)fuyh#M8JYz5>(EL=YK2(Fcy)0(AyD`=Y=7quWQ`UxtZL5NVE}iM z&t^WKx!#gMl8W4RQsoYxu__Lcn%{>>x^cG)bXPDh7jW+MD+q!BN-E4wU&YkaBp9oZ z;x9nJx?Evo_aR%GwW-l(NR;)_Mk8Nn-M^{<%IvWn z=MeUK;I0cPwNhGNczF6kH0a?B*6Uur7c6V7g>Zy!wT93O!{WmU6qz$xq0*!&#fYAK z>L|YO+0URGh5+M;y(T8#{sF9ZV{|(m#8P7R@)aC=_E;{6R;2?)swT&Auz%lvY~HdN z&1M5p7^1t{#_IeWjL-?P)eb_x`IYf!}WU)R8$>Sqmo(8c3SZ; ztUV%NFWs;44y>tL)~|n4DVtNF7K=6B&K#FcB(EjIW367;L)7cRaXrW=(owJ5;fx)9 zqsPBNh4UR=?as<9V~h=tjLD%!i+4L6gkgwE8$;G_CzgD1@zO6! zON_tshq2}Mdolae!(jXh;;@H~*7(LZzK*xvbvH&wMl9A_F}h99T*c#0AH~zhp26ea z{CiB#PD4tWhmv*m{IyM z@tmRTI6-WX8R@GyylA|}%P6EwS zRn`Eys&v8Hk~oD*$cDbG`5ro}qpxL6-B(tiLPWGrHl!@mYbDQ$By{rSQ@DEh62d5i zlu~oX=ap7x1_@`d{-Ns_fZporO2{cNVE&B&)r2@;E;BvHNiT zwR4!BzKSr>Zvq&DEzN?G^B#S!7^U^h?bwjDcp%+)7M(9$AB) zTMRGqJjMi>5#&9y-U?}a^Sm;Wu|_Ny1Qp|?J9nc#O%o(1(mf{e2|{3!n#f{VqaH?` zfzyOyG1yFx$3TMg+yn>0b{pZ$C8(&E^y*d_98bd^;@EQU4?r^kkLN5rmy;d~^7AuQ zP9)RHFg+KCF#pUsfcAH+gq0;vt)OLyUNDNQQ4Px-1)dI}POe)AE<^8B9OI0ZShlrlc8 zw7xhMTZ}2jxK>I!OUnyrjg3HL((RVcQzd4d{Nu#QllY52{y5s*He3~e4^82~ z`#*%%x-C#zW9wW0JFJ{}5pnzfVeid@B)iV*&fmTFy_ak4)m6QqH=xnzZZsQ>eIp6( zyQqbdsMV&%k}YXuj~#N@GY-d0gu~&9iQtT^VaSpwQwu4IB0&NK2?7R0g4p-90rbA_ zwJ*89_wK#($IF*_^HmmTdn^$m>vb>yu&c5%v+{l4{mwbxIW+(l7Z-T-wO4rOop&aQ zjHdU_x_J2#|M%~GmZOIbQm@w&e>Z0Wvc{6zauw@tdLP4^cd%jKo#aO+K&qbDVPku# zCRbHvoWo|CC>pejWJ^1tW$8)Vbnivm2YxphZpYoJlU?Y_XwbeST?!#^-fm%g_x4YGVbekUWYZkH6`V$jPn~vxhFSXJ1CUcc+-2h_%~lcH>wDg#5=TR zFLLR*hZxzi6W!);d?&mvyFP;L6*DN=yN;zY(3=pMsU{N>VTD2!2a&?s=JJBjtvdN2 zM{Bvlm0PwY5OH6EyiaU^$Q-h=aPNOb59*aOrY8>5OUSL-<>(tdweovbQ?1#RuP2Kj zxTK={14V!6uU>EP{EN?b__?svY8Yc4_6mh2QFE=~tX=;Nm$i|_2tUYatR-x=5-+uW zxIw=YlYqsAMZSLbSBctIG|g7XRUi0y*6qEmO}Y!V-tl2hKKNzA`GQMjn2`tHk>Mf>-?{H-;dzw1lN zD%PIXQM5w{!Sd`3m#3%Muwf&sDqozNL>t4SPd(0o1FsQAA;wtz^;_6<`$xeGx_nA` zgl!-FW!^l0jJR2&)oOC^z^i=mD}Twli3#@Z*^97Huh;qN*YD<`2fsrYh6vdXOVcsy zZusUZ8xhTa_`Uv+JNbVd&% zGV51T^olq-9(nW{USRvg7f4AX4p};W0AnhIMl*5M)p%0%o9cVt@O}{1Y6QoMNx{*{ zyPS*7!U3y@53*@%{dksDq5`_jZ58NkwZ^&v`pZPf(|t|WSe}3BMdBzzDG$#x8n~#f zIrgJa{?;1K-!-BpWo+a4aummr6a+#j>a|)Xlqy56I;X#rCr|S2Z{EYw@*+YCY_7!4 zPyaIpHt!@hPJoycT>Y+(GV|&aTzvGK=td2L<;|C0;;--iD*xiQCmA0f=hW%bJn-#r zGCMtmwiYkQ;Z1B||8M*ckKG`gg2DUk5KfOzv zu9sQbBs%X-yAY>}vvk=p<&Xl^lLskP+x4YmWz&X`(xNX(ta4IVNoJI_DZwutl*pI8 zjseDj+agDGt_`$IX311^xfYX&%9ibFA2|)gSZLO2giG^y`4YL!SF``u{||<@?n-RO z9DzmT$g5>P4#qincT!VDFGrO&I@jS^X5{B7FBdM)m-qx zLPWm_!c8#!0esJ@foN~R(X5zC`ef{^_!)NPfp3Mer*JUYeuu<9vk|?zv}K zn4bqJiM3_xPyHjVeD}x61*((Nk`~U=&@lUc{cw3CwA`S7VsO|f0> zB#^z_NavQ!ZvE3$UpY0%3eh%KrmI>L8OUEb(^ED%G+i!@ofWCrbcy%02&b0*?mmkv zWUx+ivj&Vz8eCemQ(`|Y{y2uF>Gyj10y{qTt4!YfekS+cMldka!)bMX2knO8E}OP= zdqR;J1dHzEX)H8#cOljodiZhagw}QWAHm}JlPJ$43>UG+P%f3)(d3rL9)FZWN00EH zci+XHYj$UrE3-p~=@MV&EtFk7#oxa55~zE4GgAoD0V8sGg&zIT$ zqM&SZvvBOhan7DPiBKvr^%Y{V(WpQ0BLncPwSd3#3CBvM>V;AnKkyMka(Q~19;SPh zxZ2Za&+z!8k06wyI50r@s(oDZv0ouy8tAc{kDZ;Dkt_Fb-EaOoyz&U1=Od-0S*vl+ z-G9wref_JH3PnEhiJw5Vp^#Q}nT4aTaqh|Qptb4P?z=p&61_aZioG^xkE6$bi|)Qi z8t&%uH>o-;E2{aH$6FxCdgXxq|MYN;bmsJh)h?#)=}em9%ENu$L8BVN?~xMC*&>Vq_RyOde=TA zD_vDwZq0Kg<|?WE%88uTrZ?dfAU7~d)JlB&bR1JoJimDE#pn6PH}2-O*I(n_d%wZl z{9GT)nm(aq_AMw1S314ImQ3{LjHGnT?OZd^Ow`AUMreB%kL)T>C<~{#AFRHRVn)>l zD}Gt0&z+@StrEu(mCAB$X?FIgvF7kv!&$rjyRKK37p*nsNUKq=8e?gOEtacQjM0um zZpIA&2*Z$vANelRGt;0Xu~lq+|IaYAVRHw)aLq1TIysFaB-?IzA3J~Uw=jM#A)T$I z*=+LVFa8<-`HO$XZMWRU2R{9C-Takih2!`A3DZYk?KFzC3B=A4r*RsMTijgF##9E~ zS=bc4a$=MH_YBw3lK|?#ZDg8BCfh}%(?^d_8@-R|ni8k)VQO=eIzhYgVCX_(*-RYT zJeK=fTb%DKGWmRgQn`!;Cf7~S3|oBv(MPbk0;8L@fKW`&%=F*e-VS5M@%T^V;ERt{Xf&98gKvF5QS9#8L4+o;y6 zO8E~Th?>o{hO>74cVE`pmm0O2T$-OZjarRYUwVFM4Z#=X%j%+Aj8+*41HFPCZ5 z>xB6sw%qzbd?h=DGhIduR2NqBBq%?}H9!3uZ2PHyOn!8NAeTo7K^#Ur`0e}n{Xh9b zw(q)zO*^kf2$951FVA!EuYQl#!cNe`^;;%KGls?oIRHj`9>om0KOU^;`ObmY~dx0a=k>9-5G=X#Bq z-a~D_iAgxGCFMfWz_yk%MXJp1V#iu0)5u%_S1Lc(rnlj1Cl{yZ;O|15j-#|IW-tyM z7@L*Lwg6GsBx=?Pn+;-;?*7zs&oMo>#G*IM<(fcfOQo{R3ggNZc{CYP#%3ayS2P2s z^JPyUeKENeU(h>rtQJxIR$gzb!Dsy@AomxOoyyw(*SQPlIrRF0WRD4iR8quoc*k@H$xAqO#BDlC^wuF)ZC(@=X zGtb3MEY*&UOGn(0X$~o;l1@c}u1ZZi7XI3iNlUTP(@e=;s~)$Zx^ilh*^lfH`t(j8 ze!JPJ%v977GtiwTp%-#YRdk+o4kV&C#vy$_wDbE;4=}xOq@^&v5u;;-R74gIPF1+? zfd@HsX_iW*LbciA;_M8yMzcSM@>WVOAy$%Z{T3=+{Bz~WE)yfoN|)xh>Py*lQ6YZ7 zsMF_r`t8#7(Jf%%*vaF>%@#o}N2yr07?y#Tev}H|nrPsM(&ptf#vHcV+-Z~{jAEA4 zQ=n@?*dEPm)f&${^(0mZ%H<*A!HsO#eM7I6OeQitUF5TleOkUy;2l5v+mtu%;P}0N zjF~!3$yZb=OI$j65^W4dYjhkVrNqlcTzu?41~+VF@`m?OTDPIIq3$*W<@keXV=#!s z8Jey;b2^zAhbJ}8y>H#;FIRFmHp9`T0d3NBQZ_}}NYXtZ$nX`;;knZ&+7pWO+NPQg z!sAXzDKwTsYOXuBvAxzf?Vn|pL>NcKmhw5qSq^n!t4Obmx;7aT)AThc0YW+N501%b z=Dl*$R6A2jdRPF^898*OE-NSAzI?I?LRT45GFcPw>OI)+=8qA>Sv2*eQhYbA{#97U>9P;tYh6q-81wH}7C< z&n-kkQmIvm8#SU<6CH(#Td@#WYq8p3YD>%>e394~CU;#=KA*!=u8P}29Ad&I_4z3- zzx+7Oxy!^+NLZ^7H5=_oDB+HzlAV0Y)c?#S$lZ(>(YDhMbvLEb^(%9sGhcO8Rye4n zOMG{x1*hqW43}Xt;cz`}0Mdy^5ZyhO^f^VQ^SmtW%o&qCEIVeNu53JQWH_%soBx$U zWZL0pOh(X=<-pu59FgJN++c>7%Nyrc^%73%S5dP zaU3N05w5!HlXyyYtxTMWKed&Y z8PS{XxG%>>;%~j0pk<(YbWPZzuBM{E+igF!=aHfTf z?Td0e(b5Id$l*1t)9l-gK?vu7Zk&yNH}5Bf7TqS4PBhsx()2MxXLl#mT@Scax{lL? zQ!c5gyJ7`MnuQA!8s{~qzlZc^`m#IinM~vMN&^JrS1>H|OwHHG<%$#s$7n?@YBQIJ znsuy=sMM+*xVA5WVp-iv<_9KfivE+#sxbr^$a)3O(kI59|BB~eGat-;P+QODZ7;5N z^!PFI#S&p0631~fYPDt&;=4a;wQsHA{IE!zR^vIKVT>ABSe#d{Jo_wX&YofKp1nOs z@6%^abNa*ygp{;m!_YN1FtTlT*EZhv631y?i)((FLOR*#WEJEX*t~=C6<4wT`uDK- zso$bHbAg#dFHxB~NBztZ!jrG#jc?}GTi(IAs&V$^Cs;Ujkl8~oQN3`I>Y1aMW*wvB zc37M3#4X59kx3?|bdx&2aXUjh_p|Y)_pLduUQ zdM#X(von(3_MYP%)+xjpK~Z{gcU`B`y%R5eFEg1v9oW)#T@y$lN7rp7P5871+YZIU z#OPSJZG1E`$RVU^r)DOyG9zR%Ntpy^lzn*U*e36ddw_PQ9%EJ~YK7?KuiU8#x(lkV z@WNHG`GpcgTd!f^)H2f-4q|B!QW3SHhNt{T zP^$9x-8*fq;k->QA>}1&%#(2xeNuUzs?@7I@$A#=-E(cvDedsFqbx7X)Uo0`Ub3fzP>}SjEA4RtsMAZet zW|QW^3=4-IkI^`PjOoX} z$%(74W5-YZ23zjtl_s)GDw%KkMcCxlN@<7`}t>$`|xjt^% z+1S?xT4dZ1oTGVQJ7tL27srKfi3@CX2&C(){eREqlS8=QIM>&!j#9h!?X z$?04qe2i#okfkqB%7@q>^NRe&GDADp;pcspj?NM-HHm8x;c^RsK-e}%6vr$bdWnN) zk23Yj6YTlf-(>u%Yr*q+RB-8}iu62;RxS2M_w1#SJ2wP1WvzM4V{4&+UFVi}A6f=LBT6KwLy+XNALU}%|ut_Uy zQY@C4Hzx5%D-J@Tj1Uqp2*{U)7`l2tqc^{w!o(&-egF~VkXCeHKX(}x(YsIRF4P(_ z9+i#|!Ps6W7@e`nNexK5J2?ukG)SYm44#h_lJV=`jSPx3>lJj^guo*o1P=Y-nrvpR zZbYwepeN%Z(^Xa@;9W)9TP~D}jIhA=pR{0=iO2SV_Ev-8vaST(sVxjc?tl0}jI~H1 zP)fx{>jSLU)|HCdN)sxEe7xIOCA&w&dmB$|A(9a*?=Id_+5?%hxxeEkA zo>o+&v~?GQ8@KgXvvdxObZt$HlY`R3O*kjs^qx@}KPn4ot(iZ0h!fxYI+vfkpK#_p zI&3D(sRE=YQJ$nQT4K{Xx3O;5FAHQX001BWNklOk9DX?ZlF)P1aiCR+H+bGt8ekM*YGm zE~7)>0vs5h%bCc;-f)S3Bh>aMR?Ckw}O;mC{&gwBXMqyV85 z0~@wd+OU;{!!HqxOfq)m9wZvm&UmWUDpYDU8qEft?~@Ax`p>7ZsbE z>l=W!$^>Ou`TTlvFEZcnxUP5pJL|3HZWd>r+U)#1XO14l^F6fIEX>bW8_ShHekH6o zeiUooTEqEazWIJ|(f7Q+(Xsio1wXQu2OfHeeS7wj%jGzE<}|a@QxpdW@N*?5ue%fJ z=Q^g3?#a}c-i>t8rvSP4hdULxIa&4jDb79h0LSn9QyLdeV51h+#7RL8l8?)kKeLxB z-?fA7w{NF3Qp7L$NF|X{5fc*;A`lo1`H=z^i-*tVU0c}yvFn-o<(ql)Td(uV=bt8A zXwhgjkWvz!Il{5O{45IxpJ&HU{}U#5--r$honoIRTe<}`h2B!BvTI*MX4fU1TD*4A zFb-UV1E-~pE6Rkrr4XBIc4;kCXD(2`bcXqZFHk%D64NISBI?U{SV{uO<yo93> zMqniriUk4_5b20U)F8Goe$G#_hx{B%m1V5eL~)dCpIf;pm-O2d= zyC{yWhg=a)$sP-vbQ#o&-aMS1*l2opoO+aTP7qs4I~ZrW^aKRuVYYwZ=NP&AItI4w z#xIt!QHWA1aXc1+#l;1ldg@8mZ`i=Kd#+75suk&((ATngMUb0&3a~5FM0pGDr6-4G zm9G)2{rvP~vC5RQ8Wh+w{j3a)d-lZ_n4X#fCD2-%R-(Zo-0RP>`+xek?D)_xB!s#n4v6WB2aE;P#t21*3Xo1+FwR7mZse1lh}!R48V;Af zH&Oe&a-k~#(`qn({By|PGcZVD*`evqTkj$00b0OfgjMGrg(HstV! z0tzD~ilapu({(DBmXStcLQOauQXDK1HACj+=TOq4Tr3gA5h`kuJFB^H;&m2Z_%5T@ zy_1pa?xeha8?snNwcWj4dm$rw;|E;9MA~>wMsA%pc{eLaIZgwqvFN~-T?}m9O;8>} zBZo2o)%QnW>tz+Z*4J*|P+3Xm%IoMSu6qB{aS(#$H!i;m( zAhfrD=q#%zlv?_vcZ!v-MX+L-Y;kds#~*(b+wSa*dZVhN=wTK33q*~zhO>74ZKwx$ zL%<<{C@bF^XogMh{^r;D=m$T{YcIcqwH9M7gWGm7e&udF)#o55x>onb_U@S2c7aYM zx30h@y?E27dyposfVTc&8==QS^ z2owqkIy?;mJgbNWXkw!F4R3|T<_()}y@H?pPrtzH4Y|>IjhtBO+%S^4k9lb+>4`GU-m;a>#SnEg&t->68^# z7P$QUcRBQpKO(+(lAMynQc-WzST{C7D{NA4)@ZaW<-sz=$ugtYjx&18I>z2HjvSB_ zhe{N@BB5+ikL%dFAzF+;L2atW?DsBHJ+e%AEMj?XiAZR2%BR_C;58J!f!2uwOkF&| z)N_Bsy6f&_^!oQObmeYTq1@{r)5m1h6^^;}G(E()LW2}mb8BzXxF!DJXdAXvZHcu9 zet_}=T2aW<%p!r3oH=`%3+K-#4JT^@(>F1bUN`0JWYH5NkQvQZ9h&UQ24H#Xsb@uV zT!oYASt`vmL1R~NKrbJ7l>;w6Pi~-$@A=HuYE^6PpT1mMHfs~k+V!_19*CNY`@Gye zLVMSi2M5b>9E-;veu&!AGL1^5O&sUR4GfbjmOF&Db?p9Q(;FM??xdvmP3j+}+W9WY z=DVeO`5ea|{t8o1KS=Z9NzhT7Ac0^k&(2Tm<&NKY7sFQ#BRq-05R0h&;qi%yF=!MX z0RaYsMq`9QT8YIXkth_NRR|=~N*ba;EMpjl>pp!QqgRdb$p88vONZyskw)v7*5Wj8 ze)<1lXxlEvuD-5Q0HLdta+^?Rq0%@_hVBBWAhE%*j)$03-M>q%Z&V7^J0Wr1Dbf)yrIDW41w`*)kUhyu2YMv4H|b7%Ui&j-OzJMJdj8HzkU<{2 z0HJ*RiH#68D0vZ!3k!W3f$hb7t?70~cQMqisuJk$gSHx{L!XV@eu!9ScP0a!$!z#r zzn`0K?`Gh+_CKq&8sB;7K?X+00BANFp^l%pg@8Z-6m|DSK{H5BZo(u6U!;`Lj?x54luNLn2~Eo z8NPOm{CE*vjgXuc5Q}4uSyX|XaT+QYFCt^ zoxPk61e-#ZqBk}|2#6Y0X5V}c8#a+bB84CfTMT%L%~xz-_paTm@EkwDZiXKKef8x{ zR)NCgDhs1oH-Nsv0=o)q=kikh9$U2EFUup3Kg#zW`Y+@Q1+m{7B!Bp zHJr8UZ}ploy0K2k<8c^%q%cq(@O)p@tF^Y$2do!x&CmVM3~$|uwxR?1xb`Kk+AM3` zq)ji>9EB~W4m`uruYHciH=dy}cZsOc04%EPvFZKWc>lls2)p0E2e0U3rD>~{WJe4j zl34{|C9#NT(M%ePO$uo&Vq&2|SR@K*B@%%V7LCSsHh|hqObCY74KuW3gqg!r#FZEw zYJ4xiOkbdBEt9+Uqx@V?q(REVc(R+AnKp`aJ6<}n;EtF=tn_h{D;a>!>E6TJ>D+O2oWuZYtH}@L8JHYoY%t_@;39ATLMt!U>|qS^OYJ7=^_3 z3N~yoHag7p*X>72xk@v5i)rPpngjH4i*}6Z`Z^TYw|;zBnN-?ux%3B_duD$8a;3t5 z{M_$RtJMgj7Eu(n78m9pu$IqV)=|9HaMrHB{V7NJ^BA*pXl(3C&-aVf#U){}iHYXG z2wUF&vy|6w>yWxB42vQ=dc~HaY*yLmdCce`4v-3p5sIv05XQ#2XFR z__3|*{M0pUz41y)L1MFGM6$3M5uwqYX5-HDrnt19QYIq$(TX%v~9ZMUT;T|;IWlWuM8*x)Q zmFUfgag#Y+pSIY5i6Um-e2&9k`yADmz7LHm^?D6$G(kR3VWhyw_A$2q#7^Gvnf+|M z@e1&Xnl41sGOXR8f)Jlw)wu zFhe(tQN3J2RwPO!E(O)QQGh?$5Z~Zf}JX7Wij5XZ<&;vaDo%=D?&}=pJ%%#ioI*$M8Yf6?_UC%B=s$7RK+GAQqSw+!W&{N(bK=5CDHoO&&6MwmHH7sFa|fQ}^u2$~ z+-pw~)~ZBtL>RR|f-(JPNryMc+p(US{^- z%j8BUC{J#}DzE1~>)~j`TkkNYhZgDdh4ejjr)_H?j$*vQQ3ki|#z@KRYfqxX7I|NC z*L&W_z`#Iev2&GfAw;ihsO>*BtbE2cvs2Tr`G}RxWyMq_dK(=X{Fc=kpXmE|=PsP* z|N0OA9+X0BO|0X%Rd0OP!XHkVSg$plwd?N^TB}b8^?a+**l(@ft~{lLkPKe6m(6#6 zjN-_650~26Zl`MwDKm$^PS94X$>}E^;KaS3r+)S@VWXbdjpRIvyRP7xPhXFoXb_ne zL&L*t+`b9V^V_Fw+cVk7HUtI&V@+}z7n)FnXc42^g;9|@LkmI?64E01Tb(paVLPXP z5E!A+XrxdKZyW|+QF(n4&n7-Js#&8tKh4Ol8}W*RJtu4>`s80ZJWg6DwHq?#$fdKg zY6L{BCKq4$9>?$g1Hv<`8rm2yH$r7IEkOO5nESiJ2rsAb~L3f6`_qg(N?eXZ+>~GE&GiV&{s|)E0l;@Kh9Hp>s zBXP6J;;T=P%jL-n%k8({$@tiK|GbzL(3)!p<1SY^zo^y31g_FetTyGW(lN5TJF8u9 zu|kx6$X3|mFaGLF96fx9e4#)-Uo_RF<#Q78zrEb7p8RpBacd3d?bmb`C#AP6;KR8> zp(s303|(_0TWAPayuv4 z_B$V?K2RfS&Lfm%Y+{^^+pa*N@bTOCZgS(LCv%Cyw_dwwCjV`PO_n^ZmypFITt$Z% z7fJC9$aU__w1`F`-Qm&r1PCP<*)~F>(PHkvG`{rFaYS`~1~odt#5MboQgkD$of|w2 z8%sq(hFnQ1w3vvWOKxx|mdw=zM_WjC! zMmCLMrR|6SBoZYO*if{aM@-yl-k}H+=uC7QhYqd!O-(y>&Qmlj&J`d>>WzIeXX)ISdO zZ7uQh?Rj|vW8=;7b(7@s1qx#m1cgF}92c4U&X-+Vl63Lfv@=VDwagxVnbY6-3&Kms zX;qh#80nlxZr6IY{qDyYx@|M9<~&jfqBvx5Y#7NG`@ zmS_@-sH0jFLbQuw3)@}-_3#h~a^%U|JUN>qXY&L$=)h?bg-<|CgvF8{%(LfH?_l`0 z36|^2_@2*zFf2azFtrOO5|dKhl}Jl0BCJDLLkZi-bV*Y{U1-SV!L;)~Yr(`Zvj?8$ z{QZB9Jbwh=T67$PK-5VHt_jC0?^AhWiKrH$q#}qyTCrwu*9~atcc-~VjpY*u zh_%7w1`$%>O94@nD2`D|VG%v`$z6bz0*sECJNN=8@B0Fcqc5S1Mr%!@)kG-4mACEW zu7C9bw!dcw<-u}$N)w%ZBiVk1O_9VZ$884&D=bDBJnMI6M1W9)Aiz(X4KkT3(~>1j zlD{J&KqS*6l2|0lH!U=_-P9x!;R#CX$_!sILO9=|cD|AX>5-Us59R{&%vl;nQdqwQ z736!I{oO;qClfEal%tU{(}ddJ;*@E|Eg0DhA^@YdYg8x}1G_)^Ivl^7PLfjw-qmVEB`4@QN(eINl7O~da zsf!n9bQFF5*~PKFnF?X3bijbZ}7bI8x!w)cW>vp76RL-A5w`!0kMt*h^{~PciLk@l z7&I+fooPmkBpW+NzBBQe_WNxy_%?BI&>}HC)mTDW2$3`?WmPiq`aY94u1A$5A}3gS zY7VI-S{v%sC73=#_{@DsU$O3%5961II-egm3ft119*yneEbdM}BID#D6++77vl!&r z_@1BU_?JIRgyirWuQNM0$HomCdR+rL+>BV6Z_}4b$z;u}rX*Woo71O}^vO)?`TG?) zJhuPWbe==}0a+847ql^7{jq~?*2??a@>(#)L_!Fojg`gm z^%P3wPUvf-)^MxF;PVA(nXM# zSjUu0gAC*b@vMgyP@qVQW>S1~Ol7{pxf5rZpP6I1x{MWWnFIiI9JC!B}~B)fiUFSD;qGyU{M zqNqi4>MW-p`U=G>b`Upfyz%+}op|OfVq^nb-uE;3xdQdMY2N($=gCzrle^}2wi*bV zExdw9?b2z^KJ_5E;W4(~`4LpUOy%-9Eu7)ixzPk0`naBE16n!dn!zzNSRDHB<>3>(Mn<-VnM!;xD2Gu3_=JZ8xe~HR?OL4 zr}c-TmB`JlhY{V0DupX!5huSZVUfO~w5QC~zpvGFH=%yiC%b+ z1?4k-^9S%s2|wsMOn2;X+8;XOeCDL+bsHCst2clj1WClX1t}%tJFn-Ym&15Ia|_EH zKY4zZ9mOiW~A)@^@dOzRYVm!uQ2*XT35Khv{7WP2N$>1UZE z`b;(M^T>=ow%0}a#L1I9|NL{*!zTGck!r0H&t00jV2u8Y*P`a-wT82Hy&bNTz+$6T zyFz+CQ*S=c&X4?3!pS(FeV2pikl$^r1V#H|SHS!`r5QF1I65lEqu zPnCf}DF)!%#5*n~ZtoRI zjIqql%y4Pu0=0!IjY@;C)*=ieOwphM)%BW_l8UzsHH$I{pAj7h5jjDzT*420iun>U zCt0^?g7w22C>Dx@BI+!V`quA}N+(LWw!9pPkb?2+CfWHJ%j>NcOV7?z2ntlH%PcJ~ zQdnFh_6p=DwlZ|h4fuiIv)2U4VsUn0P?^HriTxH`0R}eV5|xB}A)cKjI4C}d%Nj??GPvUTe=`lfC6z;3J(r_+aQ$=YPQ587{f5~~OV zY#**>6<37iG7*I(wuv7|tPZpe?_RIe+>*&ph%ZQ|B(yYBs@3{S|Hd-s-{{ zGA|KIf(9!sVhxMbw2njlB5(Wc|?%11z%iZ?{ zR9H)tS30J4{unf>H0m`x&nF0y=zweg(GBeW_#X0weEZefbQ=kYBoYxJE$Y<%ivr{ZhFDCXSVVI1bKA13RzPcHs>zdfaNF|BFh){<_t%x{^Y1JCEq87`G z%Ph_>Fn3{&g{3*>=NF(|z;hz!C;(IIAk#DT1U3K#!9+lAILG4YMe3KUG#YiHFr?L} zW1CA<7v?Ch-;6&zL2O00HanIz?v&G^P@II(C=(NbBSMf;64jOo7pBp1$mOXiUVrUn z7Fw2iy+WWQQ^{V#001BWNklsP{H(re@DzuF`d*UolfBy*|?qMkqxrBF+ zLSWllltKt>(_*SIww1tc)_`bhldZIvSSOEV6XK~+sk3o>BRkh!!%d~zxk~NiD(^}V zmPNaW5gLUgKbq&{^QUNBtR|cJ%E!bSp#*{NQ7o0Io<2h5&x_?%an+X1eDH%GBA3qqu|Q70^=}CD$x_HRnXGF-kL+6XX-Y0^*5`p;t+?p- zb9%pATf_IBdcuDFp1bYh{JivhpIk0)=jZ0mRu&e%W{mmcrxs?yAJ@9J)^Ogw4W|zL zq*Q8BN~Nj`vuwTNBNPV)d+l4=r%qpjryCr(|BFn$@I9jHQgR=75`V)Gn?Ca%#%|q- z5J}o%V2gy63UkL^!bA;(gu&q<}A;C_X%p1D%Kc;2O^NzFzIr#sE{Rv z@C7zbWK7nA4K225uE-*Ga%+>q$C6iXIi6>NB;%h0EN(@~$!VSNBD}$3m zJ)DwhFJ3lmo#{(c9DU<3=TDqtZf=^$MwnKdG+qG&78xjr60Swd7+p`Aj4Vr}r=V^T zN+1UmN=sBrVU5Lj25l1#O$iT~YlqymLc(T9BWf@=KTE3?QXD9f5Aq!g94$0KThV6Q za(3VP_T^MHNi@yEoav*(2Gc?3a@LZN89V&Rp!OPBx8A4j3U zTEqE!a7`KAn3P@_xFg8t@^Q5y26kS@$kyG7D95pGN#V8mb0<0SjX$Pw@e~4!=lh6} zJmWuc6I(v@PDEZ|MV#;svPt#wd6rHcLRf>69z)|JTzTyd=H}*j>FMXGwwAGhMdW3B zdI2c~_^DI0z%&dtvTZeLqEZto{rB#cs*O!D33-VvPHNAue2<%MyNOMkHsMPT9mP~x z#`i5{hEW7eicJVCGkOXw3}L;=@rREb?X`1dn+4mem_=ge5shoN%ma5oh_TUtT1=(*+%WVQ{zlP zJrm9!I*xx$$8kydDmeGOd)RdST@;2#J6R;1gwHtQ!gJrH zcK$fhS|Vdnz9fJ3dNzFGCW3OAP}Hy(0$ZX*gT~TjKw~0<^(;zADhtaz_wZ9JF3%Cw zBZNvCh;5o=lF=Xnfh~Y-=P?Te!hrB4wq+1rV(e+PMX2Pa6@kR}eUwxfY0$AoD#g2Q ze>dB=Y(pA>jVxAL8oJJTbB3ZEV1)GuBsfXXRU-a{C$%3=bG ziaZDt@3=UO5hZ~%5*aEmmQaTYl$ZCAfy6X4CN$(qIbttHOPxrarGzFOQAON}h~pT& zqw;Z#WV#U7{EcR_ky-I;k=En4Ta*8)q2G9yjzN;LM~jI7me63 zx%bv25KVM%=*p!t9Qpbm(ztLUkp&BoLq*nq{1!I6{|2;*knLR&5Lg>iy?B&{hQvY0h#0{`;Gr0k>#0+vuIdvi6E?EvS`o})6iOxO&uDv1a+)+CTwY_PFO(o7Xy zR|ZdY+y!m*YxFsgypYqEw|SQKd>{y2@o+ORQLq6fKmBG)YO^ zM2e(H&J1S;Gr$Z6z+gwC*So*Vv*g2h@9hf=MwO4E*l!i4ryGsi-M8<1&U4Ox)49e+ znQU~f!N(@9fxRF%qgY)!#G|Jk<ASb@;{{)hJE2)WUWX zHeXn$9@Pzjqb3GNYBb}bWZ}fSFj;$VikYvucOCkt*iJInD*}7#xV_|6g>7hX!8vwb zf01hY7Kc>9(@#Imk;6ysoLugSm#BN^&)hxP+>QJz?ww`xHmv8bSjFANo(nPZ^6O{A zr~d3u)Xgi`lK$2vWm%AAIh}6z_SVM6AJ$d%ub!U_iv5bV@1^$F`uf&r5P;vUisG!R z-6R$RZhZ0M?7VfZ0VwzErPj}Uoyq!Dd|gqy8l^S)sY9%N|AwL=iRk+V6?Co8&U3nrWnp=tPJFDpQckN-efPwWVMx4Y7{0 zTG9f%V<6f{@M6gl>xl7|L?^QoZOA>_Y;lE)jkmNRp~WF{gj(0EK6#MEBTE<~r~Tn} zkKRu29#+S9Tgod2y#ddE{dulkx=f-iN7s&%=Q+BodHm;|VfE2fYE_}pWMs5shtbZ6 zmtJ_0>sPNa91aL32n#QTn1~$TREFq-So-*YuRZ0sWIP&EjthA%J;ZTM#E&hSS*6U9j3e(q%G%G|N3ZHJEC!%p)bF!(`7B#!zQWF>v$#gd zsnlKq=v3f23u>DS0i4EsPd|QlG&MjKrrAx3eC05QRu6LMzyVg5SGYG=!oBUw;ypje zo)PHWN-e$vS>Wy*p83bQXZ-ZuIxR$a;k7gF_y710hnL{c5&9;e5MbIopSo*$pwiztin57=6b!6SRX$K)z z=$6K0h8QE1vDw%t0Ya2ej+GHwI4;WM*fKH^Mq%-}!VyA1Y0#M_PJ%c&G#vQjhfna4 zAN*S!I(Sf~IVIxz0{Q(P;wYAc!|D*n%rUOUt5mU?#U)J5K6S1cUl>wvROD$!mgJo?L(I<(8c@biO-4MT2Rw585RX6c1lA^R zpF-Y=2XJ>D#oZ3kZ?m%btH=fX)q>KyE*}EtuUx8r>;L)f8{4Mm4i7k-XyO9&chZV-+As6H|%XGqpoUNSpv?Ht{kF!^nTEWrXP|TX(DylYc_gTh~M7pXSO%YiTaz*IiPC3|FGqu zlP6eMT#`f0y~4}XT`aP@2?Ouw?982pn)m89cVyzWuJ3s!83-TfbTiN58+ly?t@9 z+hY9k7kKRt{~eR9TWpkB}c< z*Pud)kQ;)IU_d7t(=yOiBsLL8XlDqu_`@YhicJ%Aq6tZ$DoVrhSbp(5%?ob5WAl^F_a4ZaA(GKkg9bdhfrCNqT66E#hPQAF1) zY>ccYVvvHYThf?95uu39Q~Ev+SLghr`5H6&2iwV0eLjZI{lvsr>N3TqNsDQaPo5hBKFOv^%ujYC1;=#l$*|NGuc zYBN+L@D%bvv+{(7nJ41ZHI6p%2{N^Hber@3d=rW%c05TdWwbeD=ao%*+dYbMf_I)Q zO$ia|_6n_I4`FPAZP;p43~@e?ZT=~rmg}k4U!22%hqdT`>vGymNZ~NtUUW5LAk~d(+g=}y1OS3lZa`xQ7R$Sfl!HBf7&=K1{~DmT8b&4GZA{K zM9FGRMWSuAIa4s)MJPl$A8WxXRMxEDGA$?du9+?J`>xN}7jpl5F#z2zZsPL>`rs3Qsw&;$ z!PP-;H2Tu+&Glb?Il2q`mFk_ZeI3rXU(;ECI>~=!=jP29UaG5~Xt!F=hN^nDuBrqe zB#Nc)eGJ=b&y?n3^eEVjZ}M28f%I^fayt^MAh<*wzzGQ;s7N$XikWX#VLqU&M(0XC zzYv{AsG~HUOwVu}!7dP#*$J3#jK$^_AH1COGl@=~j0#-8#%7k7ifQP@@CuJ7?_s@l zh2ChJ!Dz^Ty6{OJeEc-+c1Q4}OrsVJlgSvXQ>-;?ZfxS5$0o@g#ipDTTzcaY>(_2k zS2YKgR~Z#!TBkeQ|A7mlcyiz(MO-;FTeWdjOpXO zM|m*L;=Op7smkM1kLik;W~d@G;dGJ`2ii8GPp|Oa6QalYK+;NP9|ytD;#hF9NPxCt zWo;R}tRANYQYYdfO)gVdn20k(Wh$<4i9;Nkxo)>;(a{~z95vC^wW{cx?6UmcgY;kP zNi0H^p`wDaV0_~OgSXCb=;0?ICK!=lk(xOj*xcGlDlF+}YPYJG{02FJBJz{u*t+;rtE6bn0UAOn329qs^_aj^XFCcKevt`aV>|t}Su& z`=7;{3{NF?CsEcb5l?6`T#U+~HuzAY7B#wN7O11VB7|llY9bz^3&NJFBfdGhn$Tc4 zc*$o;G%Ay-62>wW#ZuNm0Kq8g7$nNTXz;@9dg1HOlYcnj@cnBTC0pK}+rlO(hs<%- z>ziCU{}$)Zzrn(RB~G4NXeN<3^BgND1$C_`%W{Sh&0pi;m|ItGFc=IdiUOkz3+*lk z-hG(P;|q8r`k``Ma{2X3O!{N=^m`<-cH%xBdgc+%{N;<>zIp|n8K{t-*COjeA5dM5 zFFc{F3C_#9#cDiGezp`*{wqU>4(|gbLM!*JfU4y=(@`)SOv51b0llC(aCAj*x3G6o zS7u6Bh>}G#&8;9(QM1lX?&AcJNJ_>j9^z$QqG|%-GIH`(i(?t*Ke~4sF{A=3@tWX#(in| z&OKs&0=avK$6de?+~qhB-|n$?&xSNNRYf`YeCw5$;_vK{Rw_XnQV81f$uhjm=Uf&w_15hfZ7OXL8k|dZkp>=wV?AW4g@S{aX zi-}pY8i<%^Nn#5d62j2o`ZaO3ly45HdT-q`rTJ#dEtJ;iR7+8;f=nvG35%+X#4-@Z zo>;`jIl5WVDWHmkv6mCR6Q-Kh!piDTcKPPZFL3_c1vYMNG9HZ?mqT{kCWqC1T)lLi zS6@5BaBINLb2m_4&0?tYR!*9x5MekR;C-CKd#55-81(y`J@YC%n_F0Gu-2jy#lcgD z$XhK81{D<-&s|`9eG3(vT$!L*Sw75rKlI(Wk>}#MR|QWQNb06mT1W&y7>R&3$r4m0 z6N=I@iKQmPMl1C!Q^XELD>gV*Tk%_)i0SE+DorAKCZ`;96~P5=T)8P^jskJr4ysA| z6zs<&2Hj|=>awDWB|Gfka3r*_v{^N)P05vbY>mbk!@*}C)w-bY-cubHmYU=P=sLVSMi{dw%(%$b2s4FO!7Hl%m3p}BMFiSM{(i*sYv zJ)71$G$*a@+zrOaZm-8DKlADMFaPz&{F!gPsQbO$%-V$e?mOv?GG{0K;THxwJ0El2 z{hJrd(SEbppK!hdetE`fVq7rBwtN&b|HI$=3|V^tg=vC`y_A%wi7GT;u@oEcO3;BY zYJ5o&h0<~ZD!`hA;6lTEs=-F$GVMpv9hMI3N-@V zG6KfeuyJ#p+n3kTT9bEMEH5u{_Q9(>^}hFVYioz`?uaB!*_mv!yS2-q!v}F%v9Pv) zNi8l2CNb^}C?*B%ZhKZdymxHexQ!cEWJ!kij!7}5))n@?lwjmJx^?XaH?Ll&7)^vW z92~8sF3)}N!{qHYZ+zo5Lg9tFtpdhaOll~{CBE;`m7<>1*yV(n$OsU0XjYFJuLE%s zusT605yfHwr$~|nH*wg^&cF|cBNj4Qszebg5tRbOR2r62;DSSGMX3DDeOnntOcawa zW@rX%vdK|0JSFG+xkl8Qit9D9Udy@}E6*Ijd^TZnaU{;)C~!6OHW_r25&NS zvmvz2k+lwc);D@Cp4#ZU8d2Ocn@#N=TkV_G5-mTo6GF#N0(av|Q z#Ns;=0N#!XDPjzaMk8K+^$cJ6%df`QUVF_=MkBk>Sy27ZBZM$nzj6Kb@nG;!?KX@>R<05eDC*1-%4Ypz5WS5RVPgmZRozKdnrc#F6$n}h?qeJK z7N^B`EP@NQ;6J5faHB`%4G%y2ZjPTm$@xpKP!0>m;}N%ex9A@3kf{u1HKW0hJj=1h zFxcub8V)&d@W8A{O@?D$f8{Kp4p?KQfx42;!;9p{T6pUziV;^YUtxE+gHOCD^ApQs zPdvxU+5raJeJ)%+gIdr+5+K139mQaRGD`GC2P~lyUpiGQ{6LzMZfjiW&{n3t-~z^L zDau8SRvP1FGN~uE2t0$rmjPuEH-{)Jv|BfC@$N?-#~5k!q4AEHx&<7*L&DkX=E zNi4XbuCc@t22BwQl9*7bh@&xcr6k(O>a&M9{=pNRzjc-{5iM3?6JjxB_u{Lpu3x2f z{B(2Rfl^*#ciNZD)%F3*n|W={RKs)5w@AtZ{IK32QFn_ud3)Em7vMkrM&dmp zpYQnpx04a7s^sg>f0NIB_D^~1(j|(zR#jCd##+ir5sISN-oCwYh8X`BZOoUxG2C_g z6==WVe5YNH7-LQvV@yyH@`UVQSM(VeOw0(XA`;Nipp>LR231k@c5%f3Y(zQ4nK;+f zrk=^kL)qM$i4r`+HoNU6>pFxWD< zUkXv75&>$ok>2hmbzO1z;8F6TL+wlIQBD75pY)+jCaVP)jQVu#5_#Uj*A7?JcNC}f z8)}_L3IG5g07*naRO_67^$l*^yg@xFsa-`-k;CsfO6z!wpaQ!ayYzNk1=Cxf&5 zWoN(Pe8*i+rL7e2KM-Pka55Tc-Ad`4ydTrbnh<11P&HoF*qG0>I0{V?+ql69KPnnv zTGm{VNUYd1!D@t`F+_rD)@65i(Th7WwKSh8?I^kizes@;RFD-EFmr^S3L)#FqNpcg zg%f3c7nA_sj*3Y+roGT%`N4yXF78r}3pTIbW;__tU0R^C(k0OeRb8UBVmun+Lzt^i zk}kP&T9ntvT_;DMIQQ^!r@Bab6}0 z?;GPySr^t(1e=udb0?id1gzr#bwj?$!E`Lq2K*r4M;>i7)+A!8S^3!*5G!z@l18K1 zGyNo>Cc;Sj(w~2cANlAH(`x6lKs@1wM6^;UWl&jchy>uH!zoXn9@~CrCjQN0La8L4 zpq!MXHX&_i9DMg-T1VRqF865X9enL^Rf+9yQtjMExe}dZd;VzZPC|fIcRBoXClPFl z0Zs88tL5)^qvfW{IhjuOz~y^l{Pz-oa!+&3#^&N}>^J8egTa6oUV4!)eE##Cef12r zuTk1yjIIW~-gq(^_3N_yrZvg0DW$&f{A4iRuRQw==R2?2*z5!${DMuApUTo~38io< z(s}X(=|WemX_^9`&RNsP2GxlkiryB1TB47msMTlptragY=WEf%Gz<9Xfe>YSQ3_?W zh&{DfsuWe@S$Wq1qKVvi z>pH_hAC(D~;+^NgQ;&1_&=EY2^{dxKb7W*y7Hg@P%1Ka+sz4_~=?ir)jU(ROJOq^>hQ|j}y=T9*lHO=2^SZjBL222u$vQysV})M2(>3qGK{!qrZ`MhG*GZPZYV$7pqk zcG3}Zo=k9u;XrI#5|v5{Xpm{5)9v1ot5p^y*Dqhi*Pd>>BYGhXN8WXmmD8(~Rms-+ z2IJ94nqtsoNzTdBr$y7^A{!TPq6QLgKv!%`f)b`!EI@a)fc8OFaVmnb2EC}o637XD z<5Z1GmC&hOBc3h9H9N+pk()gXLM6>nAxO0jrKcWvl+wI@@hqf@_dNG3?Y!NT*f3-7 zP5-MJykZdgym09SPCb4at7Ni*NGrCf`W3_ekil3nG!~UE5#Dv2Iz7j@7!zF-Cj?hB zS-;9?X9KgiI;-niL2mAfEMU*Ld%9AaHYct3CL9;`{ul-8D&h6I%Zk|+Sy#Mwf4Osv z;q9aWB@^@Q+v`04trxj+JQkyxlWuk>og#wq46ziu&E*tl~AIUm1(XVmh#*igKkSTPLhOR zWZ`KRjJOC)f+T>Vg=yE|XNrbaZdAhnj3UZSWXcS6;Oy&X7>ovd=z||%xx0jqB0fdw zW~h`gusI~qqA^h54Jp95db+Jzf*l#%B!7H>RZ^sb+ca-`$t#ubrF-Cj{WO$NG+A64Tl5P_<>j|0To&om0^F3p0f z6j`3*f~Qj4yT_y&6^j`^oK&2EL*u(a&V$-o#R^xpthSt5b< zaz`AZ2V+#Cm`o-R6?Nh8wIezSe9JS5Dxg^*?riNq2v}`#p~lsYPNz-lfeyh0`nx?s zKcJi@XopWd3u^~RvXn?<>-udfCx(_YOu{K*0xCtEuP0HkpG?WmYczJjqLjiJ!Duv% z$*GE>Wr_;r6(%>$x-w#&#pxPSJqxkZRs^6rYC;y!)`&yF#1li{`qmXb{pURnx7T>+ zp;Kf!MOlRlp56Wq>$f+#y?cvcF`&KBVK5l5wz`H^(&Q>vu`}6X5%s-LceX((ilQJW7?%ak)x>f_j4^C% z+^RuU!(M+B>iSh{>~99*A3a|T_w_XU4d*)^uX&Uh{}13JTB%GarD6!!yv4%OBIRh* z0QLrJGITVJD|BeQ)S8sZm|$WmyL-1o?RRADFg;#|NazJZA@=(wo1+#PS*z(p6Q(9v zP2NpZk;!-r4nh@3Y(f}&S~}0Q+s4^`Ri;JkJ@WF$PA{=#%vvvr0 zAJN*7wo-h@&oCJq4N7Z#?Pu9E&N*4Fm4PI+L?fgNqC8(D;-dyaxQI4FCev6P2L|OX z{jkfMSI)`D0z?Ef{ zPT*8B!tCB)x4%nWR`mLP{_^WzN(lm~S z!(!C$-_}uWjwhoxfZq!-e*TroU}wM5>^GeKW@EF*fZsL7K7x{b=orB+tZ@JL{sas* zS%3bE=tflOO$|*9fm0KqM@Nl{1``tuhEk33RY4efS(i2bX9UC%I)FA>^fi8O2al1{ z`IIId>gITwK&U-uUp&X!!NcV3jKTJRZl}Y;Cr@*I{Tk(@5L=uCNZIo=Q9|dH$w@rg zT;cYd4=mVLLL}1ZE|6s{dOMq_MBz&j2)eOjG#XOX70x+!cXzpY{RVj}qpoVUZf{Xc zCL|`Iww4eQ(uEwGSccm}%H0xIJ5(yx{%gxeSXn+GP1}`h@7xwoIVadk90x%Pa+z2Z z3f-|1+knu=btY3ts64SW&53>w+z*9Ed4*OAhm+@2L!zy$DXlazZJH=*C(ng8M%JOu z3%1r0{Ebp3t>6N*8p>GF{2h(ZB}W>$m-qsu6ee1V;e@hR;!2M-DbWO+E+9r0o;pgu z+eQr>-gzc|j4DQyx3A&LF=@L?n34tN1fTO$%={{CCi2&N!~s7QhlV)JP;O5}FP^g5M}U8AljH%dwMo*9U=mLL_&?(DANvQi*B-!+dMK01 zsaHenwlF3oC`Z7fNgA`xS`t1Hi+zPfSDIKTLe=POnrTI~v_uEQh>jxWL<405`cF@; zO~Pla8!VpeO3*vDTIKDS{w}&6uk*~mSc=<3Rz_9=53T#q&CGME(dj8qY_1uCafGifJqIb z^#PSDCH&1t+F6&w$Bs5ZV3A@};2SF$)l7b*aqsAE6k#0DjnhIL23e_1Lt|=jE6`Cx z2`ZQ8HmJ~K5(P|RzzSOrqcBr5&MIPpC0H-NBLgbYvz%w$thz*ODX!Ca2opbRKFUt> zal~R@oT(#4RgkrElvj)hSl1d&ell5Xo#B>)Fez| zifS+6>x%sTBNT&OY&k~bNxUPajnRstDhSTuW3cU3=c%F?|EsF5f6-U% z_^rv%?^m7uhVvbFJ-fJ~{CNChTI-J)Ywt7GniwN>)SUR?zt6k>_J75~krUKD5^XN< zicRoAXv}3jB&93BGzQROZ=)*t+?b*<*rZK#HM*^5IuVsNxiw8gQEfx?!n`8Hruj@m zO_~HxtB7S|dDGSulL=q^^Dps{ANm2ZcFOwg+g!hTU2p{@tDjIfp$o@mjpw5j&qS(L zD226^tfX{~E?_%iwWGAcC@ak;3hv>Zr$vh_&2g@#mACORkmi}>(I~}aJi#c9qn4yi zUvmHIDNJG*jfVs)yPV)72iJ~Z5;N1WjEW&uSxT(5iufu>0;-poVI4KmMZAib+z7Uk z%g>;k!iK5UjTmJ5krNCi2%@fyMk8x7?KEy&6Ox9Go=P*0qev*k;>Io}f_Y^SMh<+_ zh>VzshFlyEVvOR|<|5SKYcJDJS5d2q@pw$9)uFB`hQk5Q%lD*=MJGbIP*JGf-a;Kb zDTcBt>EvxF#?-Ym1|fLtv6eM^P6N|P=WcW7d4cGZ3lJhiZAe=!R7hotJaPi8WpHH? zT8AjN*D1zB7C-nHOG(1QkrQZRFo`ABHRJWG3~yYb8jl!XeS_YGGmLlE>9ksO+8rRO zZf9YE&f*i^yHiD3{M_dHt#3V@%gH>lh;>IVXSWpYhz!{0ovaYi?9UJ?t^Mbp`JoNn(j!;m4)e*pLVd3Pn(!fXBwP zG4%BKSVAzwvXs1*Du6M>NkC2Ho>Q5g`J=^J0BxlN6j!;HA{eB?#8L&|S_?3oB(#zQn_HZUjC(_R{ax}jBTZ6FH0Z=IDo1P#HyDnGLVO4jlNuJ*R%E(B zRQkL9Edkt9Dd?DpGMYFEm{d#nT%w5~ViO5gtF1%XNUR~0QKk~zZsJ|N~J(7Zt6%Z8!EJxk7;XWYa}#Hp^FXOS#&ij z)kF<9w{CEA16<&Lc5~BzQ1Ohj{d7f02hj`qN<3neNRs zH)d<)5S?Qu8NKmMV)TUCv;FD?vL}`?I>E-QxzSCNX&Q8yi8Yv394o6bl{R@e0m7s?;c5ZCb=ou+7iY|@=%(%l zkalvGj;#op03{}%S|$-4wPeVQ6vgg@qMR@;C!9F>08c*qB#%G+1VpgL@bIJWVzjY? zcQr*_P>v>aPAs5x{La&OT8vF~gL&!9kR~Y)J#d=-R-dcaFYkHz*AZjIGDu~LP&BR&PMB)35xgj) z(MsV1LWTAXp#Wyim^x{E-h!+_b=6EhQGYL!HU}hU`R-oJ1iVJ zK)f}HqS7 z;M7O|9?B-9YH#J>=WZ}#3?^%V60km&HSYEVN=0Z(k#?$*)ml`|$=#B~w$N@(7oBbs zLL}BA{*<*?6IWbELhYF@%SF=>iB6%@I1_^^t0fgQZFYSDe6JS#z^cYQK*{44kvQ=) z(de{UsYYZPD_PehdB*a>DxFrhDgKe|+uLI1sUxMWCsS+WE#rk$bon8c8p6l(*~NKR0QV)Xg=LyvRv;gi%wP0)dxw{PJ4HQp=i zQi?L*yvHbm>1bKO)u5~;YI(eRTsP0ZOqg03@xyZxZxCuP#Dl0LiL_ajsvxYrPge9pER9{G_$jiOq>zQhTJ%vdZw$lDJhV-iAl{Rk0yF=^#+jSZ4R7zoG1VB|IDrT z{Sa5a_;Ggs;u93b4xM(JPN!?z?T#IehONnD`~zh%d9OnKqxW}Le!bmlfBN$q*SGeo z*M7tKwz?W4&VQ!0`P*9Sws)RjHIM(yFYw5Z{sWY;d-foA0jmH(C*;eAz*vkiC}S9( zy@}tRkhT^jPC)!>WV%rv6%7hYY8P2tJ58~1MQ&27P>B!$!Xz|)T@9vEOPZ%jR5OV{ ztc4~lr+KyKG;PFQCN^XMd8}rw)5#axbgY7NNhq;POypP!~+kI7(3gsj0Z!iqGGYLNVQX9jYZd5j5BK` z%^}zL+Oc}zFnQ7@5b)GwDkIkttDTgFSV2q!IuV@`DiC~}RaH})naQTXfQN~j2)#M6 zU?RbaR!S>N=}WA(JpAy(Z1py9U5!b+*vEL8OhW0=i9u@!I*2$nv4j{2X6lzGcX0?F z-4?F74jP>rLK=vL(BWz5)5dB#oety85oNDHXI3y7uP|vYw0d36NGIAtg2pA5yxkI) zYad7#mS>yR=`~xcX>k^?T0RzP`q-$w&t>kO2u+hq z^_};bY3TPc?tPtU~ttYecz1vK~n z-T#Jn{oqfMC6>_KZ1a9o)3n_Ps3c+O%5*4a)M6F4cCeJgj-@1miR^ki1B$+zIT|jH0iyLk6cq&S)m^z>{i`Rk5 zSE!o367P<^{6VgmSlFX?dAgh|5E8 zL4w(0<7o%P7Bhwx+KUi35v!R<6uc;^nK(AOK!{%OCTAcjY+~gQP{n|LZU6uv07*na zR0R^B&{8edw{OjwWl$a!67r>0q6+Aci8xgSs{Rlkgq@Zcjl$xK5#!zt?c?G{J+-== zP8DHJb2E?2%&*T9wMP_~THP?WdW&K2F=uN#b?4Wajx;ged4Ei&tH@rXi_O^py=N9W zYl2yu?cQxaUEykNYx>1U^n00zFV~kc#tVnK5zO+i`$SHbny&%rb zwP;;bCW)MqgF`dHs9ES@EP^zn7@(HP#~2X}dM^$FDpSJNQcaU`O?qe|WCQdb)_{um zGRz9BH3_Gme4KP4qhL%%&aI2rD278!ZZK`j^70Cse|(dL_9A0OWNFH~pLv?oryrV$ zHdTxgm+b<1nzPf}0<9=?fggI}k{FM=#9_3dRwW)sLMqRhiY#=NNK8Vd3ZfAUAB`1O zUIUmLW$=~9=U#L&eIXJEkgr5=suj92P|FmzxxP;N#FJ!8!aT$)TyV|qNTIx(^=-E? zG>wX&BT<9Z;ykSjO;wa)Gn5NyI!1`TNfA|1A|I-T=B{PR^l6}qB^$llsE)>0o-l!Y zX^AB3qC!GYO$Uh@-O8wx!`cMz9M&jIlFF}6sXIiRwsD89e1pL>h27qj1nR?`6V5CV zbZ*_o+}cdfVKQoN9p{^WW130I%u%nW2eqm7PG+UiRQ^t)!t}nB@k5f+D*`};!U|h0ig3_VqDfdhae~3MSBa_;dTkk+lWlCG zq?KRSUk2u zcWnWU#$xGh?(o(dZ%|R9TAIc75*^#-(i=VMx+cqV+U*ukz5f}O4=nG2$syp1Qht7= zSX(_xZ?sFO1k(vwASMx07!sW@?)9OjvC?rkYK+w~)hI>MP6$rYNWJsuOwWSev}=T{ zxezZP3bCT0j)!qXC*l$voJ>H)s6++u&eL7!P=^u{Dd2AC(1~hHHydZ_<~SZ~AXH&4 zHCb3`qzyArDX1IEAuncl+8VJw$_=eni&1}suUymI6gY+HbY}Y~nT{o-O=lJp48y9x zYTbC=bubI7Gj`Y%Gnug{*~4p^w1t`1S_peg%Lrtq`8acv&|HG3xkt|0bOM@9KMj27 z!rtRr4T;$!A52$!)4#K7&(d#-2~7p47|Y?OKFG?uo~HNXKgU~t@*nyC*n96F%dY#r z^K;I*>7|Y{Jv})`GY`*R)k-$tsDK zWRbE=ij*iOF@pd>5OFY=0Wbqh?3tdi0Uumng4fvmeFqpPjVYOx3>)*}x6L$~cGBL!lBX3xAlo;MnQb07Zh3by| zv|m1n$YO{yn$MhL`w!iLH(qfpG8EE+M4@pc0)>wimdQOwXkVYB`SQ~sEz);8hX|bT z&~$SuZQ|SjY+@ZbAgAjJL2fE39gC`O9JM*`j7;CLvrK04q9XE4Vz)~;iyOBqDPIvr z5qobsLeL0MC^DUK?)lTKCo8CN=dwKA*h2eio9<%U2{xsqHre3TJ8#7cf)NX1sU%@7 zLYoX#n%$nttpk;)(A%9e~KMAv}Td6j?WQhMhJy8Wk=^!OEP1 zYz1aXBWiAd;YkmvT?b)JKDP=bo|5RUU}a(1>7y(pUIJCuJt8H(@8Y*LI8N}a5|eI zETqlOKul`2At9q)sS(u22ULB*Xf#2&8!xEMgHA{cA+$lH4UFJ!gV>wWG7LK)3GOQs4vGThNl;;x$%D5t{qsj?zV)YAI`bq~{^ECSyl~li zUMhS)5T57z)mq&Xh(mF|x3kl0zb?!4+^cF6AFFd#(Rp++uWkybMZ(0i?sfb+2r`JJAN3lM5;s~<^9XiKctW-peI#Jcxnj~>b7WWXI zq~8?&H)IWYG(p!^T=r<@RY}5^E#$xQ9_Zl5~Ql)SL=CKljVZp zJm6Wl_Ml#=V@yVpD5iEFBCJG%qEMdMY<3wa7lwiqpi04Oi5_*DrZoG3p{7P{g4Hae z^Bh@N@|of5Nc8CQ4(2wS;5kbFUr5rF943|;Kcc?(1Y>)Tv;Fo5#MLkUDf8d_9IeZ* zSWhXm&V=XrDvY9flBG52dsCR~uu09|^E~m~{o`ByR0{c51m5x^i`VRpf^%biSC-ZK zm=NNv_3`mBkz?IQfB5H_IdE*iMk_KziW{YT(+nul#c)2G550Hq8COa=ROx-3=l`H6o^t-^Su|O-#2Bsq01xE1RsNkrj!W@LZQu z6ddEi!K|@lt!%LCSe?&<0){U-cbLW(n4UAE#5z)eH5P@(w%xlpap&zcDq|=-R#%oe z_4pIC*4Hr74d{1o+e`DMRW3hz5#33tRBMdaC%OLz?x$AIW0niR-;+dRRd(#!L8V${ zy?Kp(+NWBrB0G|XYA_#Pb#s|aVlqvy)xn?j(HLU-Y~Qw<$%z@7ODh;bUUEMIv7tI zwAF|t*Z5@GMIU6j7hTa2SM$#|b+mR*d00PpIRI4PQ*BgfpKFu#Q?fKgOF>wlBB+mJ zut@71Dh#gh$S@#FGqO&bAP9)O2q6{Piji0XO5H&LvspXP)eh*%#n8Tx8V3qd=vGDP zk}1P-1!17b%&{e_oU%__xyvfgj66B)qy|I+hDAl^5!aONEp3JcQ|2=zfuO$gAcuea zABx>?cn|C6Pc#4UU)ZHHPq5xxCJaKtFcM)9`np>6(=@Fct+)4j-PiTv?wN|Mp8vs_ zT_28<_<02LQ(I>AC(oU>Hww;;_20j{2`Zj7=0~GiZFhZaEbPY#Qzsta@PqFLA%-Ww z5^Pr14;1i&95w|Q3w~ponVavSdHQi|mY{^9`RoOfr4H4d(^z!wFuI&-aD}>hr4pS6;k)O-FtSs)3{QE!Wn&bF0{yc8B*rGjPIz?owW`6x`(HH zLPAoNFg7y@7o7>FX=cPG!;5k+G+~h@*U1P6U)j__Y{poWZ&3|@NUG>)tkz_i%^i_* z=qq=7)_sl6465!rH<5wPT&#mN?lBmRPy!YBXwrNp=b%Y1@?8P)B)Y8~As}{CZY30z zTEyDw8mUfQ=PMoSFPfTmb30rIrG-oVR3;=@+Q4*Tj5f4Z*J;!}vb80I&IS;#&Wl3P z9Tc5OnITqCp>wCCfwQy%6DWkW1E1JI;Zpg0h0^Q=h6TctL-!@&Z2#~#cU@vszn0~O z45~aQlEY4HBtn5<@v)`iYW;v{$05Sm15DoW17hX*$GQ6OpL6x;uaY&F2;-Q*_drPL zhhZ3mQJ7`vtJ5U8s~7hl0xm~>c+Mv2C*QI6=vT)Z6HC=vP5Pf?ki8E6O!e7RuW6fJ$AhA^7Rzc*mo_yIGw+|R-{zr^b4 zZy>B8y|lvhuRhD}AHN?@1o%0(*5*BqJAu0tP;fe$slzuBuPxI$f7YEGQVB{;M@d+TXDh`uOS;r2N<4a2Vx@;p6TGoWq~{Gb<#OtHu{jsrfy#*jvNWp^ zGG}B>maICA|mm%jWldS{=dJNGh4yGbRgVy!{zRCt~j zhGDqPShH0kZwB#JrPOsP<@26Ur+@i%?|5mvF>z|^^vtzdwchUayE0AF?3e!dgZf6n zxv~DH>xcX>JS3&87$m{YL(Ckzb0CsarH;mmq0&0{#vH&2B4?`=vmOD|_a0;7=KENE z`4pzRjx=!XzkHpouRTtE_co-JXpv!r#*(=k%zC+9N{R)7aDvLjES>NIR%cFNn3&un z%~5)6*SO9^I|cjT)LrN~ihLdE0>jANq^US(1hjPqmnlT`1_urw=cZTPL3O-}wi)a5 zYdrI{$GCFwWv3y#j|boW1`ZrOgcYu{8d#xNoUX@0 zV``l7sR?}FM{9$U9=%qV-nlMQw@%Y(ww!=dK-}%Ic6F7;_*fp(4N)~B@++)oeN?D& zzcaUU87w+=A!$-c(m2DW?md&C3vn~XAbNSh@U$!BDfof!^fswM8HrUEU;0iflo&J` zmF0wt+`~m`iKw{joUE5&1t{YRx~w~bf|8y))oV9zlEp>k3BpQ+_-gECRKCE}1kvOa zwra6txfPPlvy(I?OG#E*#H}9vUJoS{o)m;zcOd+5&_xt`v@t`C%RuCrXW|$$oTHSx z2PyJ8Wl6Qg&xw^o%5>=;J0wXCSZ(EwXyC@7H)(ZBc7Zt|5^G&?5}O!t11fca9B=Bb zY(7^lAUh{H6R03$e9vKS|LI>L>u#_4qe6FR7I;-U9Y9h(?Ai4{dbnMz4b z%C$>Zc>2qa((0}v8Uj!GD67~%yB~8!bN&y{(OPb~A-$)#>%seY=BkNN6T%f5l}7<6=cX-uFcm@8GtNE10} zUx4u)%g^x%OJy{k3Z>tZj>ByeA04 ziUopgyV-gB1DKq+Ty!n6bn{?`UQ(*W#1T0LZc0Dv3D|n*1iN4VE;?7vVY}-n3=5xp zg8I#~Ox}MWAO5)QK?|4l?%N91S~Af`n;u9|(nl%CNYQcT<}sNg5vanBKeganRK-L0 zv{TpnBKJje$5|nAo0m`#dNrmSv+O;1klFpa7$2LUCL1I+p|!TbE9YM1`KP`~uhVu0 zmlcJSk^=`05iSHg|HseZHx-pIA_zjpcTDr1pMEd($?+l1VO`RuEnJ@S6LlDd+;qpy zJpScJNs`!6r-h(%u1#bEs!@$juT5s$Od^T;0(<=A|X|pA5PTrlUQqY^PoJOonAdscPR1`raMg^+_I(7cDehFFF4D*%etmRxD zQxdZ}IbAdQBWTsCT1k~ycEY{N|6LH#~ zsst6Lr)Pq-R#W%m{IS5CWBos_R%hd8&kuGfPkEV?!SUmG2tKax6BJCrtcewo9Un1N!Mq}^xoG4&1A{~kb zg>y$%CcyMEOp@aHf*m_{5vYJm7tf=mb^~rtV6;Ve;DrI>6I+OORMBX9oi@Er8&kD- z+k7IwMx!>t&cl0|-m#Ui5n`n!v=JsTTv@un-1*C#d+9ma{icg@&eiJ`TjAKT<9I!v zlfVBY$y^^ls1bMpwXr%s@pJEI-@$$Etuq#(4OR+Acap-9266&{5`v>QA7f_kPS#JJ zVP$2BAP880VU2h>VfW5GymanGtirhh^m|-*=|v{CPvKX61d3{-#;$z_INv?v)crzW ztU=;Lt=8tc6=epPja3fP62)$q{^JPcjm#5^jaXYZ7b!aS&5567>U3d4!w@nSO5kfd zVmvLZ{nl85;z^6QH`Swuv!WX@o4T3;PAl#;op)x%DjX1sd&{yZ9y>3h<;SX0F|#=R ztSO3Daeh<*lLv05vF~PfJ@9rq%L}xY7wEoxigalX)mp>a*lOKbX}4QriSPqkvrJD+ z;~hNFZnavEKmWqhFWx9PH`ZUfZm%|kNs@O9DQ4n+EVF2gU3dQgN{SI&x-9P048>;! zrkvZ8jrdaeDj&$^KiAm0gWKQ#%RJfHVB@(*kTxSdbDg>0dyGSW_ia>WCQ$fjn_`7Q z7anUi#M*3V^l2^I_wL}};r(nqH_h3X&e7?$T(q**=vWi$n5J1`@8nUo?wG}o6tzkn z&-Wk_L_wWMRY;SRjZTyK#U(l$ZMxkyt(7LLOV{bQd(Jdcy3JPvlJV&&j@)vDpc`=J zKb)lV!aAWB;dwq|ljHpG2j0W|uYI-K$a4aN&1ps!kUEc9_LVC_+Aujj&fWLj&6Tqk zzzUKiCTaIseEK>Ef9?qN>vc9(*Ik@)ny|65#@VNy=g56G6ZnoXwr$T&R+bm(^}Co< zqx%_YVjN>kdd|+KlKY>fIoU!Aml7&+Tb!KHX0;h~2TCf}2^FW}SR;gUwKkO;nlp|K z=Y|OmzRTjAO;~YPh0P2mb339?0x$4g2cQj&u}P}62Ay^bVIhfQRAZd#`1X9$PmtDg z(H2A)Tsmw2!aBW+tC&oaCJ90+cI-TWSDPR=`5A_x<(qxs6kTlSFl3Q!wz4TU`f@H# zK|LvwD+aTZ{P~MPesMDtvz)?JATEhO3l^I;!$!C$j9$r`&6M(Xu){f2asQP&n{sDV zKy`(oXqmt;=%h+HQ3Dvx^Qg`2B;2}_O&b;`w-~GLVqTZOb5>Xo) z>!nF@YR~pP|L!BtKfZ9I;M`b$Jv%MSJYYr$5ou#lV^cJ??;B_oj2%6hl_7~jLtxDi zPAC^0A%~q$mY+7qcOT}+yMKmfuV03hIh62deepCGDgg(7_923q1{Pfc^KfE6CGkQJ zDi$GKClo~yH=j7p{#|>xHg}yDUwDzVWDVoHJ1TDWc=S{8qcBMgXa=zS7aBW}6#7C!&U&k^@xf*?fejO%|n$KJQ> zXU~CsywExAATHy0+sg~rn7*`)U5EA|5QNo`!#AHG>GoK>a&2&47fL!&Yd#gX#?g}f zT7WQute@fqo{O38XtdSN-8^`9#J*#Pxb(uykXUT!ywIYFt*D^l;tH&n3qA$HN{r0N(k{tH zhqT!x3?g)zQK{7MYh$2OqQE25LzRA^bs6}ciP1cq&Dk}!G#^o=wo=FHl&3!Zr8~U952dF>lEmrS^73Vc{NUu|)K|Y3_Cs}}Bz$Mr zHlcO`@2S@7dy&eYy!Cz#KKO2sasUnJlHk*npo8)WaKtHBYzkx(g4DPIB1}eQY>IAk znRM|oz64)+bS})(U2ju4Font;j3ouN)natU>bcWo{U#`wXiTte_cjNn2a2gJQ|#KY zizujK4eq2b3Wl0M#~Iy?HtklE=ISyVYb$g*8>Ib&q}OvfGS=biDs=7#N=dfP%yRUu z3Qw&hj@FX@q0=e!#@^I6wOj{%2nIrq@x4BDW#g65djXILM1l z2~4-S3Ni3JUU~Ut&b{ywI?Je6>&QglP5A8lfrIomdTcbCNKc`qA+s5+)h6|cF)9r= z&+(&xAPiZ%wv6?l;z!ublEeu%wMZddw6g_m4b~XEz{e=p)mUTk0uP;J=rm9H6d(iT z9w!7|=%aG_w9y6`x?40Q#pK-^I!_<1xRg}Waghdip!<5@6dWfdKdE|1L%H_)~A2Z#%G`OJl|Jg5L$)2s*~)u z4CX`s@Wa3LJ)zOL(cygO*Fj*n=X=t46=sg#jgXRJ??1=d#ASYKNs&K!fw$~?p^ zbGM^TGko92W)>MqROC^as4_jVm7QC6uw!;RQ4q3lVS%T=@FeFyagKC8rWRHaLJ&qF zuYczoxa+Rl2>l>e;5#)@5osW-^?O(w0E~@rel@ddFM1@Zx!{U7SN3jZG}q z|8kD$yJy&Y^FCIa%XBtcE-?~pJue?FOl|pG%vis%DIy)KK(H9%5|*Huxae{ zGfE)@RH_kC7!iaat!5LY6hSqFXXd$jxk>MpRd&4PE-Kq6TnuuSqD|Kgu%#fXNBO1= zT3M8_SR*jP5{`u&xM@H8j_)PzB(zsMEG{jwxOAQMb`vArhFFj4G-~67m4L7kF+DlS zSYsR!NormNqrh}CE`IR>&;RL3R!%RIc2bm(WLh&dUFU~C@E+dq<_C!?QNEivT9VB< z;lf%(K}jp#NK;V0hdY*4k`uR{;FddXrMCN}J{PFV~ICPW)w;$%Yho5$v zcpwlyY_!&R@$|FY+PIg=`ZSTKviI;oYK=NCK6Z+gcpf7Rp7NX^G%p4U=Pn?HOW3rT zbq>uwkP0%9B7K!Zc!JcVd9;FZqD}YLAu|_BCj;lFMudBQt00DefHX{pnkWk*lT2-y z=J3r&S#Pehwy~N=LI{K~jLqyF2vaiAMF@$qA;L<8Rb<^Rty9acR!1cGA%M4Cw z787@AUB5zi=_(f<{VeO}PSIbvj!qMFuZ4|!4pPGA4#ol{1W{DM_kDseB#0vXD5Sg5 z!uKn9Mv+}vXX%f=!RljYnR>%#v2nKTogt17(T%&rX^bBRG=e(5@-fono3Mwp5@`iVJK@^XSGn-;dDhOZ(z?Du z&99MFGIW+Pwrzs<{p?Tjx*vQEQ6+K|aUn(~y~2_gn_`xeTibXFw8`Hau+r^J)moL; zzWG6(e(Xu&I0kFTQp?(-%d8%o;=uckU~jjae&!@vxH*>>DB7#*Jpag3+;i`1sZDzL zB4Bp=ZpPm{#mmpVz=d-!kR*xA2ErwOYMo(?#jgkXtSayPFwSAyX)xU2yYJpln;0^a z4GddlPQg;Xas@21F37E)kfj+y3TLw;m2-Q@Es17lcXIg7BjCZM7fxd`r!&Hr1Y=`F zGg}8DPa#~^gfI%vRLOLY&a?BhpPIvT6DpM&Nzx}yVs;$44OOY7(c)87hL{JSXRDuXB z#4^bP-*V{3Skg45x76j@6IWS1xlH@Y25CRTv@|taqq)9H6jiwIt*_@@?|Byo4jsT7 z42yH|V~)WLxUyDYMQ&*$fJ8f5v~o@X&O2pjM||kmA#T0zPM-MEW9T$&xldhW z^5``CU$vi&{yNt#&pD&f+)<&qxX8nw`V7bKxr-f#cY>5uYE^E!_jY#f-OK4`o@Q-r z0c8|v>^cr%1pQv$L1%$WNfoKYN+(_g(5XR13aObp(b7}S{ip3nj#jI+VX zog<`@c`%q`tNF%fYIY09?>tTrgm@V%Q}m+DVrD z5`J8;y^I;1TNbmJ5yDMjZzD!?ct%{IOA*($MCsl<6D_(MV@7@5lytANa8D?Hud$5qV$*)yR8sxo??3akli#aw&yDq+T@x0n zMG%XQmqPYK<&GgJKymEE`9y8EPms&WGnO3k{(KUWLHvF zo;!;P6XcY~*iDmkZ|;x;F)C1Q24gisDm?4x#?~4#;k><4;qqV<($RzS=wqE|bf)RI zddxj}mCIkhK!2@=OeFPM122rAl_0ewCE=D=-N{>i;QRX(B%;S(CZHIS2sZHtJ1XdokN6+3pRA17ZlVl_Q`<&PWSN z)-uJ-Q&`dzOifuL0}P6U82Yaj?Taiurg%9^Kt&R=2v`ERoH?APcnU8R5J946_L>(3pF31>|roNtCPS*DG(3D(BOm^t7v z;44zf$3UzYqffl@)HDBb>y8~!wOSQcDB@~`JKpp@s(X%dbao%p$L`|dmp)4C{24^I z37sY~wW!3SztX3DwaLRTeSv6Oh3c*byYAe@_>M8g_l+T{l0XLd(jQbS}17zuv@HLn9cY-|dl^jL?LP*C&v%z($7M$M$mM&g0yD?_D$|#+-6L zw^6WCV1*puAwuWkjNG@Rz~e=tbK!_nZZSB=O&wlYjZi0psejFo86X41_j*bZE~(ImM@3YZn?=V zpeQ7^q(U7b3fR$NRi5b-k12P9fq?GnB4_^OgDidJUL z6Q{qaZxoyx>uL};tqFZQ+Peam_J4+19lwU#htId zi$g~aQEAljp?O|Za<48S+zdwKTv@Bcuozp5OonldwuHwNxj3Tm6f=3wi~=tTxaax-+dvxqHgUsz=I=ps8G+QZSkN7y^FhsPgzoYm&Cv!yW-s|<0k%cb)# zFn|35jj0JHcWz;7>sIQMV{AXTli9sHxOVvpOY`$AEzOhkdzj2XnkRN*kfB6Ym1Cf3 z$A^%qD@Hob5T3w<7HM6gs_+Cl$(${Y0BHr0i8yxbCbrLRCsip@NK%t<>FNcfP;Tay zXb1zsi5aYN!Em1S-1V6|fhiNvzH*J^#RaNeMK6zOh$w0J*~^;ORQ(Z41>`Bg$C@Vo#|c{c9#dpgTL zg|ff?smo`(-^+XAje_$XT+jBq`c^MIuQT24v|7Zy9?yR8SMu0hH*PigEXL-?H7TVv zZe_-p80hINTL3--d=4R=5T4&X*=xIdFN?X=kH)Olzb2)6@8rzZnJ9{?LV9e(mOXEH zJ4fF5V@N+3dRPfTeR7)W-4C+u#69%8ZPqTlgpNB%rKmM(D5(g;5YG=V8JNsaRT1kE3_t}~>Rmb$3n`#xEc5p^Q=9oWaty}LMc{4fU(A0&*z zyfiaXENs3@*c?o^C0d`&i&C*OSt0Ug7~1`c5{_a^AihZ5Z>mW(c0wSWhH;5D2(b&hivUn1%C@O&Q`DXa%^(j#4oS!pd0 zUaT+{PB5``3sXC0*tT~kI}Yr|B!|*!CfC$R zr>T)SrlT|M=+8KhG(QNa`wjLS+{@I=6q%O|3U+s`OY`cgvwjH`whCTUr!u_*Wh*%C zP3F1=jB~Z_W^|vpLhs}?nk%c$$1U^zDLu5HQWuOdvB{aBcztQPYhehzmHp6) zkT%1&$`n%L{wZKNQHn{Rc5v{}0S+A6Pi?F|=;Dggp)fe(NhJo#a9hINhh|#2=LtC+ zBlB`K5P=REYzPOr4OxJr0DNO|jC)>lFRwg%ma}JGAQT~qPPqEmWprS;W$X^B$7*t2LdcuCns!lXTBrcif94 zMoP)#EqAi(zBdt$Pb0CU{SN*075v(G4)qE|P#er*B%^bg@<(Vl!C=~se!S)A0?W7Y zTr;^bxg3SXlmMAa*o*lLC5BrWtJ^F%rOZ6D!x@q;vC+ywa-PhgJ~=8prPMJMuPs!L z1;0Lbp4Gql1AY0ket1g8AOA7?U-LGs^adFEKyX*`X^S^j=+0jv@Faon@y;K9JMa9_ccQJOwbA0* z)p;(Rzeu;!rL*4R;>#DX#-KAzrZZ}_8nt?jYORVN1Wa$8#`8SJCdS#ne=m_AP_5Uf z)GB#bSK5JOX)`EEz~xAhVKDG74+li)zwICjm7(W--tAb1fVwQwK=Uq22*KfFhj{RZ z-pIn@b(XI$5~ne#Ntl0h4nOcY^!}ra-!zE`4ddhEyy~92Sy)}-!Yk)#uC5ZteWY-@ z8;wDynd?rHlsN7(zjU5C@iHi!t&Jy<+Ch+6nt`%-o{umBU%0uL=s81Bo8f%TtV;(S zAD?7u?=;)C&C(caV3iq|uzJ=*3r)A%j5a8#Trp`(F?H`-*mB|?EQ0xykFoOjXAyCW>DRxPu{}reYYns&g8_P0 ziV-L_XkGFuE4qcUd-Fh`X-Bm$Wq7M>#5o`Y%eFn?1*X75RL*UA;1%_c>kC(I0 z3b-uDi%X)1uXzxXD84oBK$mnwX=aEoKW|_D!~Y{&dEq&9lKD~ys?{3Cn0_zoebead zgAXrVzxMsI8Qv&3-{JLK7BAiEM}J=%y$HNl2oYFoW8fn2g}flG0M9>{#s9B(_xIGs zCz2%oWzYA2v0AS+tTn3BX``(mJaiZL{=&at=HLmCs+c{yZeY_5m>I*|sVC6O*O;B% zju$F!zvDJ%v7#iC(^E`tnc~E)C!7*KPU&~L7_D7ij#RlXm_&IB=}EjGaGjehSzwoU z4LNjlv}LedY&zbSx*sbYiNNOgi^!dzi{o;xw;y090|+gDPFI^mej+VlI#{Agg?nG~ zDlWc!oS!Kqmqwjqv_JC- zy_55Zo+Z;6S(Z_&kF)E+A7RgH-hqs2%s=}m=Rf}2OsbePnxMNp&(x9IkiPF|!lGnU zx#^=;CCEvYywprS%=gvkFn`cVY~HNo@SIX?^4g9Tm@*}Kkdsr={-~0nXu*n842?A@ z&5eq9_d)Jd$=bk*A%a_UcDB?F`8MPF*FR;>f9&5SS6({V;re zz0s&wYjq)npxJ6Oe*0^==kNSFV>=EEprirCRF)DE^XH1!7P<1JKf_l7-&efry+6$E z-8&teW%4RjKs8dKB8eY{uDeL}aOc?Ub~sXCN?`oJxiRNQ{kLKrO9SR|il;Otcjsip zT+0gKW;)nmaVpE$ZK)U)UMki)(u08Uu?Fvc-w&~}y2@An;de{es3!^hy@(0ZdrW30~Bz1vvsuCTGVL8sql?b-^xPS@G|Sb^;ulrK?QqEkK0 zm&n}^GLyPOVqBq~n3`gC>vqPc#|eS}uj<^W6*8yz8azDL)u2g5>MUuDq`A80{OY8S ziacakA=o;J2~1w5OdjB>v5BVh$R*Z4@jQAxp&C^Qg9sH=2=*Rh=Ds%(P0g@y>M<^U z{C7~@HByn#)*-w193qG+?sF=J@Fl&BEV0wdmqxiLl^vVqCQZvAh_E!bDK>mvx@?Na z>5~3Omy6dXSgc^Wm050o9rOQFJ zQk7r{{LuFMz141~{RySizj&(GzWn{R7v3m1-=%f3-_gL6gD+(9-?Y%&0{pZP;$2~- zvNw#PKu94h(Cc;C^CQ2&@wfjJjjh`UVmwpg|Ch7piyI`36Fh54g{3l9n z)`N^49?xS1EU`?n3dSeKnI4};SV49mWo@C!wX1XV`+bry#`HBN&K(zgAK&w+Ow`Fl zN*IMSYGX|9oFotde#J-Tvn*xZZtht%c)783A#e!igI0LRHJ6&KE-axFgH7FVKTr+) z+B6D(n1O`Bq#5leE_3xieud=1DuD{<^tyOnK(KWu+h6}~CJ&xq;i)fi{?C2~nWYHf z(M}Ds550@Yy+;Nl0#6KM&ne(3F={n4h)6G81G@BCm`!JvLSG{`<=d2vH49x!;l5nB z4V2G0g$0ky^*pB3a2LUBl%fPY0a^|_r{Zr@o903VG+2-d3ilAx?^t=}3(My|@u94} zvNBt*HALVCSgg&`YzfO}>ec!`{rvSyOW%+C;EjUww|w2|h1>H&^ZshRzCWl$6(yw9 zMw4nwYJG0{g@4Y$*S!Knix&) zV#t*pXmN_dSYjs#>-0JA`^on*JvGHA|Kt;_uQm}_Ojpxwb(s5DO8k6}EpOb$_SfyC zva34Sv7~kVgA$Uk7P8~O4rX`H((QFgvji&)z`*T+PDnb~%`z1Hotzu^1+ zuYY-QZt44VKf4i6@VB+l_)&FMs(&Jc_`5+R`teG&wo@q;`o1TGlw?-YxcPqW{d>Q` zo>#pA<@v)%-OWzArPHP?AyP7^(Aj7bUpj;7_o;80WdEW4$PpP{!2;VfSjoRHL}^DU zn`)JJ4a%}~JIXlQJhK^aV@t}tf*y?+p*s(MhZq!%!jYhSk{*gc#3s=NLxazPSrmmF zIJ}=N+qTeZZeUVPmS$A^h)_jzmOEU1>@q7aFJn9y-#!T;ZeD{RvJpC;r=TDRUB^*n^WhBZ#mm;f2I+Ak7#HJ>n{Zi-r$N$6C%a49`y4P;c zAW)w22!bHRvKaSzpG?#2pFG-Jy88XRuiYp(e+$H~iHTjPY-F)-k2eD5|G4A=E zj4D-}AuAZZ`$mmwz@V01SWp+t>%^Ul(OKru554UW*XJ z|F?DkH5EIa$jJjD&(9IX&Xya17Qf%~Ja+Eh#h!zEX|-B(T5YfbZ45#S$e??*!}_T; zHm=Ig3$58x!%jw)3lSx!W=g7j zQOc%a2)~u*E@~6(IbfQ}Z{J8~c61gr2wr2fJ1Q7$W$3P4^dzJA4x3P5waMU1<%vpEB}>z^)!JC-b-RB6{PRbei_72dd)tkI^S5yA3#zwm z+qLUoNyN^6w-cptBD96DK83&kX72f=-{i;}e-ytqF(OP8rEC$g>Fb4sdRZN<1O(L@ z*Isy<=Go`yx7)0>Hn{zsI|##YASA%9U4{SvAOJ~3K~!WD`ZZz@RnT<``Ws@j8kIS+ zj+v`nmHBEYmCdxDLOS4wtv0@ZQj)33Ne&-5 z#MJZ@i%W~FudlPdzD~E>#gjh14rrfUXZh43$$En4`-Ed5eh|1#)p~i(3}`YEnGVXg z&HwnqMZo=xrbzn{wwPZz2(9>DVS6O=kTokIvziO9T%dccgVh!pDunWo^*Ytvd$1Pb z3#+Vs@_AbS^(69opUAI}W*RFbwm#0xYv0Mix4oaBR_F8|{u??M&ft4KLQ0I3-177P zjGcGB4kg5p#wNDW6xwFt)=hU1x>6bD0HK z7GxG&kKw*6^_d(l)2f`r0q;SW!LJVJ58KxUxH)aGIu?%N5 z1$HAw1Q}xV?aIjKm-ijl95kC<5!}MJgvsNI{Y& zyz=Z>l738@rDQS#-={jY3u&utJaLibKYoVp*DfQP8NFVYUL2#5L^HcM{^P&Ifj9jR zSfM!c2mhLlQ;$)p*6{;pnSb~v{}BfsdLK%uQ3vGGbyeQr%aQ4uubXmkn%M+-vFYow zbdD)Q!IZB1Ebp4iE(Jv)DbJN`sd+B`h5{xlTm}Z?#?8=O87(fD_RE)>7e4lHpL_JT ze`AM8di#SQ^m}oSe%wcASqEdE`M=qF?`YYwtGxHO*4m-csXF=Q&{^H;APGq=sijs9 zfF&@Tdi)j)aiB(H}*~E zo*XOexYl}q?7gc19YNy0cGWpo4#W5XsyW6P_nf*_dso`)TXW8Dejk>fR*w3U2iKSM zcvTw%&Y#@m=R@svfaCf)R$B2RTp)J0kdK>~NW` zjLcFFRu>^qzQ^qTd9Jui1M>;4ReCuTmdISoGM5KL{0E@|~1ETL8Wc$FG^@fbdd~BprnFd_XaPGO&XxHHQ zE;h8N9fi3Fn}2$q?a!Y^U5rq%CP`xwZ7`0{s%OiN+Rt}*}_0vDBCoz{^EC9z6nz~N^1EtlrR$ZoS-l^b@=8vG0aSg{yM zkpUJXb_IP@kwdH9z!PUMe^UTVCbB99&vVEeMfT5Ksa808?J*8tbCik6W+n^N>t*)K zu7{G@Y>bK(ol6~7pIl=3sYQ~-h&YH54oD-AN|DMGiy;-d-|H!s=!}V${jKw<6P0H* zf-F-?WSXe~AlX{m;>!8UBpVU>QbPKCgnY(OIc-qq18Sj*i^Ow1wDeF@2igDHw{!U2 z-^2bp-;J?`7e4bJSoqw3L`j+H=V`;#TRy<;-}`rPDz#xUN+~0dV)w@eLwG&NB;;@j zt-tXZ>Ax`22P%E^NLeyOE1lmkO#PQ~Kt?QA@5)Z3MVVQfk=dORyOY7!(8j%P^33o5 za`gBw{j3hw7O!BG@28GOeSZSiM%CmN1OcRG(rp`ix@4*?LizHIneG3-OzDzqqTz1TFRiZ zEG%E;T$O>O>tvad9Bwd(qU2Y8V{(ACYPct*RA%3N{VT5L=rzajD|u~tBG`owHApawHaZ+ zCKeN0(ooa6&?a7pIQ`j^tUa&8+#1bW-oWAaem_Uv z^+8(4Z$>J``7i$wXMX!%q0@v~y_uDD@BRR{{@~xmpO_rDYBKDSIa)R=e;3L^oi)^xA4S&DOib+xqQ>%;TTDXk%!Bg&jzEXg+so8$v z>uV=|f`d4iVXxJn2~#jjc=WOt{%b6M;3pEv4_e}B)y+1_{++dIZ{IDcM4 z@SyMcKd6&*PA947DxWKle35-O-ij;TtYkLQ*c^Iz=^_>x6SrS#OY-u8E3@pP;()e- z>)!KytUUWD@yW*#Hs$_L-%GVxo4-s(=TxHnHRWnVS)9vb&@EiA}c86(p~D}R2;n2WA(}+D_^^U@Fdl0mHOcZ z-b@8G<>I$I>Qi;1I7Vqn8YW0jW=aE&C0q@N7b8R>NOuxC=iA6c(p~G|Mhejk zNd1&VrZ}#Pu@>1nM)NiA;?Nu4NBzJtgj6UAn`fWn^5=dN&#U6rJ(4&?%^u|V`#*#` zG1UhfR}M;3W#F;pg}nqedZfjc+Che+L^h!5lKoqr&#~+l#Z)ELg5tTjazK@~fYF2N zAl8E(ju<_j8MZ(e%8E+amIkD?#GUPE>8qc){QSrNnQ??X)T~zhPIpIWt?_+fJDr`j zusor4`pEEn3xq(io|d7rhoWR4%~L< zpiXXwr9Fy^#?9AMQz{ooF))-WF683EEEbOCXCL9julyr=mrfz2pgA|ihky1bc=xxz zC)Y>Ibj*rHSoC>SrCn~hRF*Tv>`1mUj6}l=e19h__MlP^@25v-*n2#Qq1%U^U0+_m z*hUMjccV6YG$F)@ncSF^@^D<2^^H|D2G4a7jwDC} z5*t&i)bXWeMX8uQGe|IM=`7tpxjT#XWQb%CGi6`(SI0CqD9Xv|o6XR%;q#G@&uv{<9xp z_J-S#Lf9e!EEaq_GV4-|Q|VIkUXGN<_WBsggq9&)*3zP1Cc2dOBSo97;Dro{i8Gin z79;&2r!@F!N^TZ?kSVnQb$8n)XCL2s^gsNI&Cb~uCnhGEGfA4rc4sHMW|WhLVXza0 z;R902pMEM1E{<2NaY^kjY%HgV-6z#k*4X#eCnly{&vi{4(2ZgazxM4ot}~cRVE1f6 zaZ$x)kc5;e(?vqHe(X^B1`7HFq#GH#1V@vco_!y0{dR z|3d7lknI}4EU7~eHKzBeX*$-y6S`eQ*D3_^)^)?(D%# zmAZI;)0pRf@0VD4_#Uj$w0Aa1qkwBZ_!Att^PLE(ip5=&RVs>>mK|Y+760$b0qd)b z?`0*V{&#jHvoRyRoB}LTyZ3BNzi%XmKI)>qI3x;M0GnZ$n9%S4*o<7_nuz$yndJG8 z{@l*f|M6dk-PNTz*L9mg(34RV;Yi0wAv<9ZJgL+4*AU`gJQW8E;}vTRIDg^eK&{?2 zN#e(G{My#!)PyxgY+pRhtZNQNG1*( zVS8nP&9l#=zxSt@zV%Mr$vIF?PO~n)7nYT0zs|)!{y1^3 zow<=F5wmZ2A2)x`Pl8t)yhIS??@sZ3b4qp)eW!^MGp*25Gj^C(?8x2iy-@||AvKH< zl8f47h`HSGmm^7OS$GqM6$L~|SI~rA)P)n~{5`+bd*HqZa zXkB|7)%m0SE7lRinrZzHxRe1EteTlBhJ9GQ6vSpj~vO1Ia=8biI&z^hf5nx3NK`Ru5lIRraRS>lXjpH{{oj)`r=3Nl;jDgOT^PKymkK!1^)bu<_*d=PtbNzRG2)EuW z&(T^-rqc?8&5LAskW*wy*^mvh*aK|r$oH+;MLAjI;+>+D z>^fzYYCG)SS%`<~m6z?M&wM=Hdgd#^%9SfN3d5#0+5theR?lXxjBXoap9zEDH(XDD z;zZcl;qn-3#(?veES?Oz!JUn%Uukb`U*}c)qe2QNUO3H(KlliDe(2{A^$DbuXa*z# zYllZ;7@6HHU#y0`%U7)fhq>juf1ISfMRM^)niEYHPF>_TKl1DR_|N_X`wr|IT)8(X^&)U zwya)I0x7E$!shbB)()d=p>j}~2a-bIISyW{NprHr@oSF}rwKaNL}6AMb38ZuJqW=< zty#wkLA74ZY@OqnW~+%YmP*w_8^g!`^{=9&pxK(nAn_cR$s2CproN7|d|cx4&`&R^QlMt@F+`t3&YgYyRtOeIky z+7Ex0XQ~bE_@19YO*AoN( z{u;^2$B`sF{nf|$nEx^U_Rst+rsrl#TiVh=OLhceWXIayYm0K$z)1aUB-1Hn7q*;f zlo)Mx$ZLn;6|vmwDJu+&rc`#g%xCRr(`lI9VBO4OIqR02oKRr~E1u1dDQpg;r6(i= zp5ub!QLR;&oS7OBCFF?4%T=g7Ns_R%utXF^7*bNLu@jT{t(l>r%>H;1meps!#?HwT zL_wFRvx#4=Gx5f6WA4`14?%X}N8^?HAbs&lyIZ-rETma3quLS7FFS1(pi!Kw{Bac@ zQ)-95m!(#E{J3zbq*YfUkE-EFKboM++LM+k!+ zmrlLRcBjkDx4wdEy*8X3eTGe z;AXza8z~T)60VlE4aIQ_AJ6ta{akuhRbrJ@;C)R zMp4Y><@0QwdWwsm{(sHp!=FkoKky;>Q_M3YRv>E=OkH;i>sKxkEM6oD11_IEPcM$R z@s^vYRx2aSg<_j4hb3l4*ZBToKEff}V-{wYd3?VMv&Z&dh~1#FM`>;NBn<1+j{=bp zL$6T`Wl5#!!CZ!?P+cGgSzBgz>;YCt$?kW8y~z>z`7gYDk>?+Knzfa69JhkDF!SoS zaO{omMk=TOnM(n2x6O;c{fqQYJ%y_zlM{7f9B%vZf5Oa-cMP!&OIFDye_P}(W|tkX zyWlDsWJV)orr=OyKu)gVm*|0tKv7`Tg^_Hj=Tek>%W_U-K~TzS8YNO8SlX*gY@dIT zEBF4EUH{^5>dW{2LArJJIk~ZT#nEx997jpVaRowPjj=+>t|Qd5Q5f8BQvJJ*w7kh|$l}|kMJncw1ZZyl;tK?+|NUk!q zf&zSo%!$g+Wu&VH?7l%c{zx&d*jiQ|aFZ~HE0Zh3uHwvq!iiRI@X z=h8jDgNb^$!q8|kBR7obUFK|f-1d1$Ll_vVFg*5D9 zlL(V0S-RA>I2W1dLy1CN0E;IFbmp?TTcHFkhsDPGw99-L6#m;IC2+zE`nAh%ip#b0$e2Z`NCX=(^rV9@<)YbRiCd z@v1ZK;e1nvs8n9mQS`sJH#YxSeX{ksR%>d)MuAKhPV&sJ{XC89Udhp0UxVLWCvYd2 zy5;owoMY5(gbtzVaEuKyl)}hxrvD_?e&k zKbYG$w+Ap3TVSzA^=e=bFgiFo5O*x~ZAPh1yRs3xt0yyP0ofsk&;ApU?6$_o`T4-iFvM#-#qlNzKD!yYh2n{3o*t^5`jnTPbM z5Os;#+vq4n8;fvUNW<(si9&>=xi*} zU0b4k@f5-7Cv3QK(S&OYqSmO%ouwtNY;B{HL;#{*pFqo$IEisx7o-Sw+S`{f=7luX zUlvk)E;Z@7FD+dhs|Ab!XB+@eW$Lq!-04+*ByM;AQ<-nl>duJk&k%qqA&l&RRQAYk`u{xeLe9vx-3 z4A<$(#Dl#WN>|I+`XmFvH+~YkTSc}&e!B-ai>x6cOi6=^eflpNGjq_jMTs8D|#*54taK_+si>=JN|?xYtuwq%O}}05tX==aql` zZ}UQLhxL>h02QrJjik6xk(7AVIF?J7t8D-sxQO5T0doYS3 zMYd3O4J)9qc#j0^?CkL5Q_pbng_j80U91J)^N>nqOznK(k2aQA`T9M?y*4#pVTHuZ z9OA&M?m{U!$Vv)bzJeT}_fT&xmC*_^URjY@j_eO4fa5@G4sWhaaQYd%?aRFBh{u)K za{1*;gexoTY;P0CDaIJOr%tfkn5I?pvSiPxFw^jfjAeU$1uGPu(8L;oxXb3!BCc}i z^|}C#PP5|(Au!dFjdamL(on&&EBRaeTwL z6a~Elwffg|94|ycc--}Uzust6k|dF?>k)?`dTWLF@+p>1K1;8?jW;ogQ>}yJVi7~u z24yp=!n~?KqiHQk*kfnu5~uI~6lXv6F%C>9q_uR~JLohe*lP3iV<+(HRrVj;kMCFZ zE)BtO|3k=Oy%F}JEDau~wdTIheTiTF7r)Hczw}kkpTEeF<40*U>d2hFG{jOHbs*hC zePA~M!P2+Z-unf7qr&G^e&36$H97-iclUPs;$)EOl*^YE_}IVu7@z&EPZG2{RQxKg z>*G~x9Ju>Csqa6MgN5btL!W2uu|Gu^jk20h`dt45e~+1KZy9!BD9QW}vA*np@x=&5 zSzC5#`t`*yDcKYuhF1}1NSfyfnLA$-Py|%RIbhdY-O;cgDMwk>=3WAL#g6%bu z_BK1q7ui@`U}NC|(e@_6<{IJ7Hl6hqx?5YMNs^cLB);zx$8jHIq?Cy*8xz_X53CSU z2(42Q1wE4X8o`B=y!_=)v$k-7owY@h&NfO)jMfN~V6?_+jj#rT!03cD=+axh%;FRG zbLvwc=hPqkGVSNSikox^J3DBd(rdR7Lf|+K+nZZ_>C<0eWn+_rhYv6{Gd*loJ*sQM zh{lQ7t#j4~k9=iI)08jY^Cg~p{3(QnwZ#>ldE#lpBVj4KTdD?62fX6*JI+9Z{Viy_)(-%!xAqg%`ry~!>Sx%ri+s3 zYgznq6s$xksf>3vxcty(nWoL!mCFyUt*rdy#@gD2&eqlzR){o-{Uk}8Fbr^%Bhok~ zNn+wCB25xJ&&y$Nn?WNK&!o}ww|mvUlP1ZXovp1O3zc(=U#rbb&P*SuR_mV8 zTJCIbQFjF0$L?Y6E1v>uP_-8IeMi}M;~i+ff>AD5i`$$g*jOgKbOzhoVQcjYRSB*R zK&M1u!0PrEQc7;U>kVwLt_N!iSGKiI_g8BTp1S|*Y%Q+xZGZg(-1YW1Q>)j9vkSWy z5ZIDm!2khlxdD&G^F2QJ!{0*~gq-;D*9pRag>x7A#IO7oiwleV(Es)~XfzrOSZ;PO z#5pQS4B61;5TMwR`-~XN4NX4)03ZNKL_t&v?bXW>d)QR(N!c!+U+fJqyY6pUT3qJ$ ze)IRa_qYELq@-G_Gc`5K*4i3YNu+R4QX&z#sE?eHNX@C~FX{X^y`2130;x zXE_Nh078upbQUV(X19%i*<&uJTrt!|F9?v13sHbORmHfT^V!dS=ER&|e^?25*jn=~ zsZReIaL{$V=G4CVeL=5llPGo^*Qr!%HP81dvfJw#C6zWp242+{QYjgRfvVLTa%*E_ z&0rUjFnSunSAi`dBpy zG4z5ij&krkpJO-O#Qfa8_S)j|r{f^JT&>sM>v`U--EPl);fZIsaOy12Jozl&@z=hC zYj3zVD|s>EK`hD(F$~!>mQ|WdK0CAXvwY|$|0Ywj(|qZZpC^eE;w0hzPksT%_xOPy z`F?7(TKOQueVAP$pQAvrM}Nk06_4byh}=luYSdz3sC{L3XI&+viQ|aVC(rQM*B|Fk zKXVTkpFe}|`FO5F636H$rqRk&Cbrsb!m!82rI(qy>9x??qI3F59OaP45x5?$Tkd4` zm3Lz7pnz!1u-I$CQY5#$DEwx8H7KL?=Wucx3`-_qLrGv0O8z+lgyRrJAsbsU@%Gwu z3z|z1G_4B#DnLU%GUwqnsP-^+rQ$eW2Uz9u1?&OX37asqHun zuONsV<*XZPzus%N7aH~ERyXKA|8yF5$1A}YaK`b!FrJFKUEt~0Rs3gl7=6J+LDO2k z)$_c!W9%K$b(+>1C8bo>Sm}7i#9`nn1OiRk*(7nYUGhw;h41<#I>DqGV+{slE47+d zj?Kl|*jwVG4U#l7&P)%N>p)%v$6rDhb8ul>pWTt4|S@BQGn^TxNm ziPmI`y{G-{-c$9vhCZ`@o0Ba*_(OjkDHVVE$v!Gr5E&w z;*dCra6`rWKKx_c@#WK%|7xi0b5B#U?&5gCA2X6iu8K>7r zQFt_oW6nHxl1pdL^OgG^k|#W`-eWr)bteh zf977+mRFHR^7-HS6s^fg-uB+N;d<^Ua_vDkw!`Nd%|68LGS_Z3gKvOo{r@YUXD}My z>jtc@u5$XN)7cz$=J*uzBT>+P7;!YUADGDl$UrGbv@{ZulC*j^70i6B*1F^GmbIfjN>m-d~IjF z2Rs8j^J>3dHAYWixn4R>cYbHj_cMNlamuu2M_q$+gm%Hasw%(?RGm0sZJLi zr4nt-w&%K$fG>B`aQXQ(?(Z}oSX+$Wvj5QM&OCWyp*21G;l@PsRmyR0ZMCKvYwN3g z^>bh3+|w`e!4G{uZ+*|(n4X_O6=gO`Wvab0ggxCqEwy@`_k73uIC}kYKL4LT&GMxy zY;SCH{-rZ)zy8(C?wcF-;ohw%I#TnxTA7PIKuYZGt8mrZhR?IOu*l}-HlO_6Pw?DB zkD+6Y5dyDLqh4te+K4hf&rzz?5b((5U*qCEi`$# z(WEimuE9$Zm5kM5#(*=9Z=QHE>IOhS!WDqmHK)F41r^6}o!<6NWA(}<&-ZJ#-l|^X zdw$|M-t&n`wvE=}fwe`=Hz4^uxU^t_C-02HUyQ@>-NI4t^DC9R8uj{&)mn76+kD~o zKFte{Kg(<1ekZSc)9aYoH-{8*@6ndsRi}|G#o|>wZhOt`96WM}wdEB&&!b+iGc`NC z8$j${qMYR^RJ*tIL2+)eXUfCXjNwL+Gl^qPojk++_ukJF4?V)_g#}V=P>#c7YYM3x zq>vaahi|-|cYOC>UEPf}KsaUw(lGhOp5>n-r@Y znm4{0B}HFl!j4d%WvM??*pm;rWilA(ER>)n17YhjU@?+42|zjyQf6~NQd;ZaG%^>R zaf|_H9N&E9x1CK5bn^kYZ2+P!taQ)+j~?@p_U73)P0n8I^}3IqJ9qlSp6A_t@X*og zq!8ZL#x|ReJi+r%KFy#0$-TV&+uzNb-|-fvrYDCN>8mdEBeDefT0c8K!)#%jaJ7=z zsJq)}NLlLn*l$QW+kN~!KrKIIr`_SfFF(Y6_k4*{PdTsWhP&^kJ~4q(3TrLK7?L<9?Dg2*+~Vl5<6L_26lw0f=k&G-7S7UJUnYt} zP!6rRLwJo=-_NG_w@?s(!8}cwCarTIapXY$O`DR3QVH|RS&}IU#TKBZlNf6(y|B$z zFEmz)gRgJRibprs?D*;%1I{?c@joRV-dan56Sq75FF1bnuCUj8k8+)NO2o0r)|3KR zJad_Qe(%#xHZKEeF#KHA+jVHDDupXCkjeGhMV z``dZNZMPz&WZ~jPdc7VO&z@s#X^HmMHfK)0giSO$2v}QMChGPO$|W|2_W6^TI3|uG zl;cvJnrHUacZ2I^<+H+iTvPUn>kGV+m(q$JjGGI3rY2*L%iRJIeN5GMm|k7bsSDfrmNy>q)yTiiSvxJ=vCQh^diR62(Js=E}Ndw4XinL@|pd=a0%SE{=sV$AB}Aar_y?{VSJ~Z<(5Th*+G!+6Pycm)=+Lt8W#SSNgT; zd=jVXp+9|q$G-Lmx4rJwy!-v{;ilZ=&GWp>1uO@ZtIbIcXw72JO4m@iYq(a&)#q>+ zP6tZ&v)0hrY4g(aFY)+;kI?RPc<%A1S-reSuiMMi8?d;Zj|e4k65}WbM>+@tQ53R% zWsxua!6#W>xQvcslv2b|h;m)xD8g}F;v~jei`FTg?`5=PzsAH=gZ54vool{rtzMzh z4Dt3Kp%+KYzxu6I_a7UQpeYCdGS5WPQa?wQ)S*?WWL8!XaKu1YbeGNa$h)R(-tQ5h zT$eNsXiYa~4_tTM{MN#v1H|JiV+=Us7{{MUJh8cMf$lqxT>FHLl9Rsc{eiH2XV~q2 zYg@{jmE+Ev*vOY2e~#17y~y3)`VQW3_nSC;{0NQa1S;=R+aZ7%EuV?2t1R!8`G`@W z(NQ|wFvr(gOBBWIY;E(zBTw*^d++DsOJ~_wT_;UbAd}Wt%E`>fw82#lQ4nU{HUcVM zg-)+S7zMO};7^cvhs6>YMK|3dbxEMGGOyS6m=S=Kil;ZdlP%Cs5r@iF

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 072/199] 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 87e709355..dc16e7889 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 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�`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 z%y$)d8-2?h)x9w%A;-*Mjue}3H{{3Ldj!tSAc^aYnw^bSIJLHf@!AS*zx5Uv;}Ali z%yU#_0WJiDu$#!TEFh~2N-Hcbu0U{6!#NE-jZ${=Wwlof%ggHkHeiggqEOgw$LH4& z;su!Xd~h-x;Ol?(Bwl~!X$*F@JtY~&7{skM*qIaPojV6A#9RVs5CQwmPE`gwoZ;=; z7`*%nmX?;#>-NA_8P42&9?Q#%bhtk_JK8@0ZZ3<&c zV6gBYPm(3sN=d09Q>m0vIYmUq@a8+7p?{on@4N5Ci?lI@DrCR4az#eOix)TUyZ3zO z+k1a|uWy1vAq)jbEkI*x8l83=h zB3oNq=yW^qvkYNS!?DL7$Ns&0F*P-XAPB&@fbR!rHkw#iSj6Egud;}c%t@MMq}S`w zmDN@L_!Gx&{OE`O;M#Vp^)4>>Px+po7??+I?wYT`;Fy7Vf^fd|L^4o+qe7##3bj_d z<1{8=SObKH7Y0^|l;lt^waY76`U27u*xa{dZCtAI&K>4jNFp8ueZihW4+?ZA3bVQg?+^C#FaR!zR@eQ3D5RNi7mj)a<)1XR& zrn~K%;ARDzXhEapGVy9z!M&lEJJeP8Ib1dKq!acbR>t}60SpdfY9M$ zOG;;3gR;C8-c*%1079)iyYMB`j${;D9&ENy^u=gNHHK;EW)gk&RG;R zLM$&MrJMGrWW=GGIev$Nf;yo^Q6!btpc9m8sJa)pzir5GnUYHof?I(_cOA5I5w+$t zgb-*nrf~B=dLIs4d*cW<16-aPAwU`MC$bz&DJaIk(;P~07{MTgRpC)%z_J`7$)Q|6 zQZ6{a7__tKp=5IRRLS`oDI}MbS#KAIjb7^ogv&jb{{x^JWHXCL7qzs zQ#ot?Oy5i=&oa=-ii;K%JZiYq5yp*ID9kDHh21*mO)>iQ3 zmma{SlPBN_uVTDvfO?48>#s$)xB$X=8Od#7I996T%3)EZp|Tw7r=CN$x`tXUL^2qF zQ;L6h_YdNSf9yx#1wPU^#@1#FwOWW)t8KmhvJB^5cnPPTeGaP^S8?{)Q}!C8w2Y4? zgre8&G>so7TPZ3Igfz@ld5CVZ0AjvZHx*cYWp+@T|01AXeS(eqZEZYOX2QW(r z@f0)%Ox+lx^DOfK%m9c1@HmR5LC748nFFxPu7H>Z;PpCuiZaS&o_he6j4@gpokoL! zpp2=#2M$gpX)2>`Uko~3*4^65e)WU@f^)&S=XqG(vkzBacOCZb-%sY}=CNnjZY(V> zqF%4V_k1)O4Kx}}>{?vHi4)IKP~tVFo4D@C%?odR(_8W6@hA5@e(W1>Jb(JMGzMKG z<$XuEf3M);#5a=u+P76`93IOlp_CE=5r#H9T^p#gZ5zHsY&VhB0(6(>kv78Za!!oG z3gfT<#_T@4IE9E)$5mMMJ`Ffn-G-g1L2hGh87mY9dzs^e45%^N7K8DmYC;I^`2HWj#ix$p?4u7NNg^1dq1r3pNgt@q z4B?CNhOW+~(2C;9lrqoKiMb$XEAG9}Ln2+dxtxNqEEee@+%z<*9Cn4HMK6~{_}Wk_ z&KQBr?MzK^!;kvSIUq+y7u``2s0OEKGq4sW#o#F% z?;|BZj7Gfj9B6+N;q(kz?G|R1_u-XC-)d4mpP)r0J+SZp{^;@U$rvWxNsid`0QuUe(4N2<4BUY z^f#;T+l!^^u7jx6M&bvCybngg@BkobwJ3tKabkyyqXk7qz-| zs^*Ob>dgiKg88Mz@(kX5>k$~O5l0EmpSyrdYnSk)`@et_Uw;D0AO`n5_(2f(esD72s}@CyjBbSU3>P@Yi@i6*|TpS z_U+z-<>h63^`WnVQVP!tu)KFa`A2vCAin=CZ)3mqoBwe>?)Oc%(^=qzzMla9ZLZXd zN4WoG08bg1M+u>8Pv%McTjOXbtx?KCzz~^B)az4d)*Db;5%k8OT$QuXM;3VKHR}_* zi0UV#vV${Y)*0pefzsAiroj0*1E~o&NNAvXS$TsSM`}yYE4vA0M%88IPHzGP0&Euw z0M;+6baAfu*3d%574k}T1VoYzT?NKxAu0#bn3=_{8;;=V2kr+k8iEU?oef~+6vC@+ zcm8HW`*l+(CWZX7;7hbo<)T{%1q!aD7+}-}rK&8o>4SD&!F2#qsS^{1Tq~;V~e0Nz#4#RQ*j$Evv@3L zB$X})MwK?m*3ZJMoOYA|U-r;S0cvxPK?kYyR#?Ja~_mnN-%fe->x$&u_NDHR5TK2}yQ zVArla@I4=@2%&SEj_S}QAq4e$6Iq^Fs4dpzCdb+VDV#a+EFS;!`;ZR$pp@Ep0Hq;e zF#n1hF?aAFDCZ-EWC$gzk=Y!TW$0gALI0(*n46ga7ar0yhLRF*`)}^TkN?zVHP>H_Yp%N*cYNP%xO8a)Pd$Dd_x#51!6O0yq0MGaU z{@UZmpO}02YY$zs^1=%y*GffEbn=KX_W}4cfB^_ue=>Ij-ZqX#8xBUMJmJ$Iv{9>` zW{_vwKJj1#N*46$NhDLX zvM`nEL$e)1GZoAn*dcARlvEE(k9Zy4`a=Qa#Za@&@SpC@fV2t6W*S;Bl z@tgk+ac2wlTGQ^lS5G52coV1yMqoXatJ3YcYII2}gQ`Vnm~~Pnl_XC%xQe^R%%y1R zisGx_eJB}G63lwX45P=1Bz~iR*fI+=q_Qltu{&nsRWdP z$rVhL4XZ;cL8S(yQ)D^ui%2r7a3loN>n%u6Agx=OZWj7TY5}s)M;drAhK%q<;~dbq zB@SZ@iw6%QT0H}#G1Am3q0-oP)||6)=&`)k9V}@%kQ6H`=dif28(~;0&$-pAkP2UA zk85sr9=`9RwZ+iwb&zKgn-?$O!pRdj@!(gGCNaVwL=^QcpV+Kp*G;cLV{UFDJXjQ% z>UA1zZDI4d=U{pR)M_d#Fy${9^_(4F!Mx!Z{#H(~M|5ZJn%#+l0Q=0&u8~^?-Q!_-xNj*!`eOa1L z0ayTVL&unX1ZaSe?*p(1fE|DM;p_vC9jjB1^Gj=MA@@Au3xTz@74rH!UrWC4_S^j* zea{d3Cr+NkUwq~-@YEB>7W#2~cx=q*+3Y zG4L6OT#uj-TD1{?Xr_tQ?s*&jMJ8KrjCTz?X@QIPIPCPxLSoH1EmT$bo9QSUz8JMd zL2@=qe43qy<#yzWDnFnEE8S?VfoEl_sI46hVFV7m7Yk(yYBQn{kx3Ld3&C`=%>$YohxriR+N3*%e?XxTLkc^xwx;v}QoO|=Uu zIy(>w3T@p9q$;y=fYI3wt%`hk1Cl4;Tj$VdG%(eiLJ}vy)D*70_9jfPw-EGWcu5K` zNh8f6Z>fP? z0g1GZ=SdEeDVRh8uA5>~!5=u&N=i#*XFOd;>igCeol+!$U(Quk@2Bx$2=4ia(*zr9 zYY4*{wC>^DiD#jd!s7BCaL)ttJZmH4lwW=jI9F=|DRZ1Ve+KjOiCz-u&0p+FHiQSdTV z$WN@&^ z2%q}+C-LBYe^qvnlrq(AZEg9rT8zJ^4<}jf12|Fn@~3Z|UnH{I^I^;$00ALnU$@n| zp{3N6fmtMk9La%}#-|TDN@>aTk?>>I*l~=PnU;P@Kc-jxTw z?P{E~SV=u1A)=gua0;R_5D)~t2xMv2LV38%-*2of&0Bu>$FceJlZZDjL2HF}uZw*B z3>pi2Z71PUF$+VlR&0HjP=X2oubP*%3Yz+1}4sdT)`v? zh%?Ykf~GmVBty`Tpc%*fg-ghN4{1Gs3_ZlnI+DP*^K68U#8)V#u=e69Ts(gP{cZ>S zUKfN>c%3cuFRkJkmt!P^kNVU!Kq*8mM7`NWZE7034qbt%xq0}t8d|LlD5bEpw0k7k zw5l#60JK!NbnYx3`^=x?%(G8}!C-c77X5Ayxl%|7u=|eN(O6srbCdnmVbCgUW~J{M zNw0^Ck39wxMG%aEGKL@sk;D;R{{!ETA9>#oU zAe7+gV^3kT)kcEn&;?PQ=(`xmMF@O3!2H? zc)@+?836YKz|DS*8>Qv|95%+x#BsDNrQCDh$3Aw5F?P5fh6j|^)4_Dp_uz+GYj7d7 z(Ryln*U}hwJUNiX{6wI z7;(dFZh(!BjvXqa%S2FP2T`$TDWglZQh6RK|ECn?-va~aEJgCt6POL{ zE_|!CiKWFoc-1RzMV%V(fPprx3pn)|NFYFK0>o#QYvI=8ZJ5zKOp6^M|D@|ol(v;I zch4_)CMRhuc|KMw!W@L@a*~)BTWpY7n2iJ+L@=P~m$0QRRt6@OFi{2?=Rhn$qy~#q z1aX4Q_t4nrLV6r&EkJK>8c7&Hdc2&Y*>~eDc=q8h!wY=Wf*OS9A<0u&3j&&F8BLNH zc{)IMJ%(`{9v1>Z7~;hz9>>(e0u~P*!txb|ktYd^R@l9JPYKT!u;!rK!JyZ}ndhFx zV-I{5oz)AV(C~xM3SL0K78kI0-(K)AEV;)rbZ?X^cAGtk_Qi|XJaGa6DB$^!F1RgM z8Gia#egW_Nfp=Kae-fO5;pXJrnRB@BPwvADPoG40vxQ!}i!@DbB!yGF)*1~+DN$dT zEpv{fk-FFGwc}pzV*oyZ@3O}eX(Rz$a{o$R=!h3`WN9;s`pW=$HEp+V7h!O<(Pn+1 zq*h6iWoi1k$<6?AG`HvA!C6q^DJfB(nS%}jWPy*tY!gaw;4-pHLUtY=GdsvLYn^^+ zVPAZa*0>uAGf;^v2ShBxRdsH2=sQ7#CqikA}<))>T*?xeJzA*IZhl#mx8tmKzeB?}%v88k*+8)$_EdPwXS zM71(y9u28Tpv!C;6x@?8yD(Fo$tmQl1!U;@H*GYi9!kjxCE&2fpp1szIESY1gENBO zpa(`dZn)((EFa#3pcaBPJkU_sj>S1p5Jqf~Daeiz-cmrSY$1sAVL?E}w_gu+-sSbi z>8M7TS*R794Lc@}4L3iG!f*zJ?wU~=@R;q0q_Wo+BOt;9X?QTo@jC7}O(GF)MKEaw znGoUcdYD;QfbO@zI7iT^>(2T1x@<_5=h=)>$|rP5VG;u%aY?ZVz`uEG4SWh^Z2#-0O*EN7P{SUvj^HZPpV#_C0= zG)3#;ImA&PFhIB0gAoGr*I$QlW@c=|K+A#4@j02e+r_05Cy=bHBCOTG7z3pYjB)I{ z;VOLKpZ_!5a>p(98Y7eZeuiH=@$?D&A0PY>)=s|!#;vG_2@cK#I5P;QLJS5yz!+FF zVXfQkc9g`Clv(~l8pr<^2>IhDWg6pqk3y&*owBY4;F&ubGat^=EYwD4t+VH905UN> zHP~Tb(_GbRZOX7#BSHx5+II!0Z)KVN+2+nr)R??-oQ&UTa-R@@C%KDl9^SkxU=D+~ z{z!&VXFKDjw8!8ovO|HH#NAAiTo;dct_kl#VquR|!{j!MtZ*Zuf`SSLo#&Q3o={cC z%?+!?@cLK30gWrKL3H}Z%-a3{03ZNKL_t&}(lkXJ_o2=mN3iQ42=_{*&`=d-p{de~ zj8b@xDRu-}yYRKjgny}ab3=NRQm|qW)BQf} zUPDSXjDvQzB-9nYq1DWAen(0*g#9Y&qH<=Y;D}IKtGeGi^flAQax3MgK}neP(`W=f zI44j_Vt#QqUir#bp)ofN8Vc7jxUQur;{6=Dmp~74Aj)7`F?6QvbtbKQIMHh8tH#LC z1g$dvWh~}geuS{xB@xV;VNznd73$QKp6#ND&+YSB#bO*9MmqZ)pO>!%Wk7NTq!dOf z5Jo|oJ}_GYv7P=`kL+)0FjLU^A2`#C!o4F;d;pE~uIAb5j; zxsp(NPzZ0A)@qIr;&aBi=lL|t($d9zt<%QZsgodtB4{+RuxmHsejkH=7kQR}In`z% zN1mHsu$w^wG!GttSFcxQwy2o(;712&S$^5$G-DjE#9=X>CcBkr`Ij7HuFOQ;HC z65gssx|vXYc1kQH1YlBu=edQJ5VRZly|f zLgCTl#_bL|A%&Z<&r8^b7z3VUkPW^KrW?8gjGl|;>>R%DZSTexUO0tLdkc9kfvpQL zQ5$r6m&3j$Mn(SX$hT-Fx=o#yf5WZ~EoJV^Rf`BrvTQX1xb=5^L1zxDujm znrrbnK%fZ)A&zg6rb4w39TTjRgyqEy(I8O<5DMiWG-tX~5+|lAb)*FFsO5!xI|NDs z0W>@dMQYbc2?}@|2!s^?q|yotJlhEokHaVpkt^_6B1;sq#NJz;)>vNJg|yv<(i(fO zIJ8F_GC2F($@Rm09Sim-m_dGud8 zi@@_BA(3Y}w348;06+D=`~rU9hu(!C^taz&WE^7|jw#&ro_FKg8?VEczwl)|c>fm> zt#2V74B!O;re{|zPcuPd#NgOJl@2ea|LM&@t&7%4P?A5PU9jf61{ zKL`=l8vwK23>nK)AMYY2y3kG#1XK%7(2uRW)Gg?ST1bQX%RDX}vSL7XCO)<)#|X4258DL5^H!#CfK z&kF$%J13IM1pRX-&}htqGFGt_aW?+tu3nY%GbOy`rt3Mk;ZrtYvqGpE1IBs9*Gk(P z*YO)gs9EuvMW#(LWGRcp!%8}4mdSFKMJiS1;c%A&Q%J3aU?VVGF?eZHImt*d*4l~= zEYzmqdwitpA)J*%q1Cq7Xbsw1$DGfhXXcURDM&`K`-&?tziZJR%OHd9B+#7%XvMZ$ zfpG)B!`;y~DAXw{h-=e}f?+^FwFV4v*+@h3nJFiU=hPK1Jpu_ECx^Db+rqLWSAfY} zMht-_t~2sENGL$+0;J{vbW1u(;LSA8{6nVFUa}Pd<+TtQ+4;?}{ zGgI<2z;He)?sRbRv13rZ9(><ER`x@i-H^_ZkEM3h^%k~D6Cit@3ArpnE<(z z?r+y6}5@wrTEB&aMh z7BVTgC~G%QO20JY)NzE`j;x3UmQo=bw4u*FjWlV&7ap`$*mvkKZoc_;h?pW;YXQ9k zL=GJiD5rqb)@nrSauZW@NmOeW5l%-S9z%)Ud{Ba%+lY!{$deT3PuUZyDJk}wC< zG2*<#%GB1Piy~&IX48&~iF07n@qyfSJ)|Lkg$7V?PBjmxc_1FQ(&=2m^imLygEu%D z3)5H-8DwfrFT0(#YHe<^+vgU^*SFTogM75LVcdMk7LxuWcU^hIqgkH)MjS`Avu94< zsH*&u(~&3t(YTZa~`S&!c_vIUvi*IV+`NByobb-u)hY;Fmst`MIglJKPwU5jkpc zLsRTzuqZr%tFOBTKl@7`z%6&&f{*<6A7K653ird{6?vL{NN0-NQfq$t*x#~c{&#($ zVU)h;2Z3ocnn=?OiN_JmG~iWs|0Bht@+O)cc#si(WNbRW*6D*5Zmh5iJa0OuXljDfHgIrY_6gy7>ZEbUk&&NHh#smn3 z+H!W+G7j8u1m~VUhJXtMeu#dWL2jG{pI#~rCK;vR{xE24=x$6vj9uu3vve?%N>8iexxz(AOFfLKW^OZ~ zq%LiA3Vfbcq|ZYLP1$0phciIhDGrs=R^%X4;NmlwWeO>^$~ne40uf?)`T&yE$QBaq zT$fADueKW%;|c&VFoas)GD1L=wuY^g!W7o+1$4*AFe?c*)|)sh98yu~f!MBv7MzrW z=7`dEc1P=SNYub~MuxGnV3piYB@~Awxt+V|#?Y4rmTw6;$Xp#{Islmptk}TkR%p0d+X7Xdu|H(6BI-}hz$V<@3iFZ|)>zQ^rgn*|Nqlxfy8#x&D3 z#))GOA*8R-t-PU zeDB8*_1cd3r^qgz0GZwefAJusGG)r-s8rcy#yG#J0sD5&l+B5;P7FCf3HpQn zdS`vJ`}s@fzO9Jg&!2m-IAs}tRr|lvYDWXWW4(^PWoq^hWfZ@pl>C=@mc5=)7J`7( zYjxytjP>JBLp18vL(UCo=1y)uv$%k}e(b&Y!S}oyeh^^tRJ0j|EGpNNjnvRc({yAP zS|~X?TP<8TcLB#9Iff`2KnM?92vKV^cBN7L-XlT%*(a01x7^jN-+j~rAOKteU>-&v z2JjlA)i>7fK6)<*SzY|d7tHqxP4RwF+E+^Tq9{_7Q6}RAgKiu3>6szFhS9}+5?nR1 z;E&m#7ti(5tn6kAXo66HdJa0t6ilvMGhsczlwe3ge@FG$`0s?7-0f^De3j>EN=Ljr zhVi*h4aO;GAfQugT|$k92HjD#q@uVK>a!x?jaGP=Qcz+{VUrF`&4@*f5TbP7mV*{# z(nmMxW4bW~#yIxxJ%pQXyd8pacg+y{-VkC9E;$FyrGvZ_3?a~*LHYu^h`pc`xhIf& z-sp8gw!g0gfb#7zNndQwVsiiQO*icGorfAzmUj$$)@zODId~?)i+h3)@LYoBa=1xx zQf%T99tlAS&lydiw-V4+3^N;o?4AN$sDssf)aRz*ty*{WLB9{-`<-5|oBeGsEG{be zORw~6j}YLeI_=g6JkLAK8K+4S6G9BKZrAaB0+do?j3Kqz83-=$)j#_jv{ZQWJKu^! zR~&%n`xAc4@cKD-_9DLWz*o?1cc7HQpx4LK4?hM@xbBfRH+X-uQ3Z_{VP{Ml0W~n}-4H0dNq&y8zSyyaGTIK;VAlp91(f81t$1 zyN})j;N;>x59HtVg~s>%tu&6GilV_4VXel3S`DY3_&OH%9dr`Z?QipV!7)48&5Zpl zQ_3Y(ni8I%p=TwX#z6JbN;(+WlN`A!*ua*MM_b4ZtSVlZ)&H5wm40KZnp)Z7A=_wK`>-9f!pM`L;h7kg_+ zyBpxm1?N?!ovhhOjVlGM*xgeHSN!q?Go!nR63xCy5d(bSPm)YOQSwOPZ1!DdpIO z8?`BOQSxMfkQmUQhF;W%?*;I}8p1t$5Y5^-tD&WlgTDxn*8(Wckog{z$00c%Cc}=& z)5!!Oiy7zqCZueRH`S##8Kux3g@iR@O=>~;`aG8qxkSCwN2Aq+EX_To?SQlaA=)LF zPzTjv#30gW18hbX3FxO#CWl8jr~#yu2pI(vd^(Byn}1ge%;Ra40QmLW!sfkMn*C}V zuf0bIFA$y(;DqFPX7W65!RYSQx7@NtlzL^q)1gu-eDPEF2@9ROwlTnXz7M#sRpsyq<@t^;sAfS&~L)%ClN{uY3*EZ*}# z|GT!(NQk#oEfJHif&7fnY`*TPrfSjT$&CD4kb8}T7d4tG(?=Cxz)A) zY)NW6Kr<8{u(IPJLy=r@T_!nnKZA)P$ZiCc=Orh$zP2_XoSlPIf2Rx0H{#xzo4oKR zIU#>Q2>BPpklVD;ROUHVO5FqC!zUko^fs^A_{Dm?zCTT3K`14D5aKf*y%!JN|3$pv z-EYI|-~4*)-M1HE7?daZiN}wlf2oBq3_(a?+$xZ!iIe_wFvgWJhDaqf)0}cHh?1Em zAX-V8CsA?{+C1(F|DLbM-AN~B06Uy`HGpd>#YFFnmyGN`=K$OV;B5e&S-<<}9{~6Z zfV{AH&lmJ}RiSyL*Gi9Y|09Eb|1Qt>gCGp)OAmhut7o3a{%dX+-SwHtklCc}Wt_I7 z#vtrPBdQw^0^0B#>|&szVKe|>qXg2>2l*D{%R31WCe#2FF7U;op%`-{@~~a1!b+4= zD|03Qq}_2=v*W7}@=kHSAOzRn@mhT4BfkTa=b(%tmpQ1Gm?kmW{SC0$J&v5WJJs9; ziB(UiR@!-lSzf~)u9XPG3}G$K7$_rPppeRT4#tu6)bS_AU?k{_J6=ahRS_6q7-B{V za;YFVE2Yj>&TI=pajcA1tY=(DQmD`bw}zThspUSQ{KsDsR0q=j~$S38WIF4B=9V@$T?9}h_@HeuC|BO94vb3PGr zt`?z`3uIFQ{bn6ZYt*6`QyU$Goqm~HBt1TiG*HSGAeX6Q3}8A5bebXU^s%yX9#Z8P z^m=(~eIq8I7ye5uHpjDA0eIlfu>J^V{9S{nf2UIFVZzuSKaocAhzOoadVQKj@lOcP z+Z%=<3H)$|a^YQCS;HrP_mA+GAN~Yxctbk!kTcjI-Sl;TT&@i_<~Q1?QlcK9;a z+GK-%Um9%$AtZsp24{@LaqI(7lu;@399pT2GjUdH^{CS7&rCf$g$ZLbi}yUx1MutX zcOU%}fL{f0H|!+S77t&8 zQ!8h|g@DX+uZqv9u%jg@vEuCeecAd zO)5H75ep^T)r%M^!nbkM*dZm8?BG#Uz6RNztE3r4zgb6Auc6WHqt+XszSRZM8d3;A z7*K5>vJ5O&R$ybLg~-zc?Oq3-=ff8vdcAHu*xEV^LR$YN7o7*&>k)vDyMLwW{*!qU z0r-t0@c*+>`uhaw`*eN38OV5!0Qd)JALd~`s~9`;J)mS;rRiAFhG_|2w^Z7 z^rNjy>tB;u_9!95Xrud_i4D*5o>sY>&9gM&j7z1|mez_IW71q^EiiWJNtr5i(#szE z;yn+%w0`%|54dCh5P+MpV+O&trKozTXvgvV&j9`h01vL;ef0MM{5fH~yYS)9nwNE< z@oM2()aiXmLf_rYa;QUkHK(o<6 z7AF`)eJI&Ro&b3gg9Wvc8!I}5(%Z~&FNK+A!R2V>Y+M+I1iVX6gj>W5RhpD~Wct6f z191)jh5T77?SisU8fx0Yf%3Ik1yj<_cAdFlP#7zD=I(f<^YT*K`kHahMhajNS1km0 zhMmJna&2261cJkFOoI@DC>mgP-vP{CcRk{IFrxCq1cH8~>l}~uu5>(AxA|&#-$;D- z7&K~jT4BiKj=Cs78r!3sM--Bjpf}S*Z>9;cYaX+gwou>ffVzE78u&l}P;KBPDVS8) z=xjn64bBCmQIJ`lG0M&=V_N?;FG^3!bPY1S_fF6Mb4uu8t<}*uiC*2PH?QBw@++jw zIrl_^Qbt3gdAHZeWuCKkt0f2B?ircqzxt$1zdC%y7$aSZoKQ^u8VNJ5^Z3mA-AA7Q z@bduP4;#I_({#tAWuV$H*BzwxCIHVFCI5K+?xPf*0{=hqO_8o2VvZD2xNwp3qEsZ%J<3W;;Eg(LnjUq&TzM3T7P+QeBe zM{n~2Ivbaul|;X_iLIAjK(E(9mPE*+K7v{uL9LD~&mp6Mm5|n&01T3D2P6o=xInWp zh1&EC4&8JcPJaDC%zlNza!=Tf~?A(HNYhI|qR1sN9z&r{Q=kNxx z&66j5A~3u2xgj`o{Y{u#-jB_*r;*DXf_so@0+M&32Q9Fm=3piVClWIpu&e|^6{LT$ z$QRu~u{alYF3hc%29}^7DMD^#E~LlG&7cUu0ZQNrZogJsIF58Bgo0CBEXoeVZAK`o z#t+z{BVAVn_ct*YjZK{UfK&?5D3x$U#*TG8u*-3nbU=+p6c3OjF~Xn*>Ia~nH!=Un zjCCd00Sc?edlND?e>En6sq#40!nBjoteH^c;9O`f_q!$y{^p**`o1N!7UwW~X$xU* z0G8!eeobxBVOnD|OVA#4p`no`G2$qS!XVuIN_+k9h4Sga_PPRa3c#s1E-e2iUj#E- z?akNad3Fn-WIuq{cDbkE-1D#g zC*1n3_u@70d=D1(9smd}nJ`KwpS^MDj7bHWtKkn6Mw(#(n`y{a43+2Tc3XJv>?u5a z{A*ardr(mi#3)?6xQ3t>!t(=|JVg@sK`Dda0#P(TSg(OGZU?}Ofzft4KFu61Dp9uKX+_uKT$JfhNk(8j%xERQ-3kxfERF&c zB@n`zdKyRMTYDSgyw8eE8q4!AXH}!5?PhYX**(zH;ZtYQu-gK=I7oP1VM9pdYVLWj7%ytmk;2`oBja~-+Vi!W*0EOycb?j z8`;5D{kEzJia{o^>-lqbv4ov105PD4npo?uxhZ{865C-|!&Gw`X_}zjX<3+uaS%%22SMqRY}7#*h13!r_aRe>SKjt&9GE+R`I$vThxa3%nR1Q+mmzy6oabI^ z6QB6EAH?QMC$YJ55qXwD${a!1Kt>zT%|(#K!$5r>5}anO4jQwVRkPGh>RZ77PtF zEu?0TMHaUIl$0|p!ezyyB+Q8m&oZT7SrG+YbVI{kHz|YKj0;+$j6kU&{xl3qQ%;#S z%-t#C4>+f#X=f>G*4DqRoI}zY>f~3T*G{1>7(8JGB7}4N_y6}tap;C4m)$;2h$7k% z^2G$$tDI}S3>UdmC$BXwn<)Iv80BiH*`(qaBqR?*$qL^g<_ zv_h_OG^b}U81zbsW~1JOfkB?<7UMTIV?`@%2&F_y2?D?md?UDTG&CiIAWc)`X$qq? z2&FD%^f+s82QFdjYgoFj|2V z0TW9!o3mKB<5ig1e-Mq?c^tm+7I5#9ZB(YK7CskKv(@{~j)!d;*uw zokJ4Gn3p)|_DP+bhI>x&mOsvRza>LY`-z4<)=9|s|uiywJ;NuK#PA?42B?)g90Yj=J&ilQG4!(c9` z*QbLZU^3596AaPXImpfiHcma|0<;LyBmw8Vv?yJ1&j90Mff+-dX9#LF zgkcR?mLeMT!97tbqOvqWtyV{tBuLZ5wA(G?1wILCwLy1%y|uh|9}K6&XhWOLW^?Vr zg)~W$I8M^YpRMP6_wPGct2aClu(L0|*jYV$wzafpc{d?6AOPi_K-?c_!8woO2#j%L zae|rYS-==l5liionAU3(DjIf&K!?*k_kGRx3uZGj68-xHYf1(K|f&C_2)Um1{~ z0e{yOpv@(a#vBOaHZHm7APeGL7m)#^!f_P54Rv3~NEd1ed6b<I7*-mfyWtGa~}1!z{LiqTGN%pt#2 zsZolR!~5V}SOH@U)6+8;M13UvDB_HtPqU=`GHWD`0Qe2qA?>_nvfL{UdURcL~aeyNNa16lj z0{G8_@#y8IHv5)3n)00#!5oZvV^D9rZ(+~!t4uC^A^5!8nbIVVNTbn2mZivD)M^;k zQ5ZXVo{uDrA%uq{iAw{$Bu${C)Iks$!3C8{nk0@4rOenF5^ZEIbH*9b+6;^~J)P$r z!zfMqgB7FIdgzCnnap33GBp)70G)+)%PGf#<5owkojUyn&r3R}t(^qvtbn;JlyayiHfmp6>X z4&BkV&%a5R^4Gm-s|8Gt`Twmya~#in&d`Nt*n`Z$I++hhA22)tNU@ z!2BG5{~bf?m+!@s_4j#OH{oFY)&ZjBoVPk5F6GA+#l~Gb=jph_hl9+0xr5}V`YbA^^ z$g(t-xzq+mdcGeIx;?4$Jm$U+mFJQW%CwYv9RZKL(N z(yC=(QX+WzWE_3V_wCJI$YIQWW6VCmMYq|Q^4i^&ucQnaWv|?Q#i45{rHeCj^H&G8 z;DAxO-S2d_I2T@hs(C2t_GoK!GpCgJoC{ls7)P2U@Phz^QVa%tWLXMh3>x(&gb>!R z$w5dPo0lN-9E{kmE=?1JwK}Gzrj^cRFHMrivn=ZpO0I5BPfz<{IOlo37X%@tlp;xE zgkcR@8*Blg6JG3g2FSpX7!FmN1@Qui*<}#F22FVh>=!Uv0gDyW`-ORDkwe3oO?J$( z;Vca$q~vsL8Ye9!&4n7QAnT2D4X}lzFbFlJ*I79f%7-F{0#4%-b8#Wd)3e3BMF^QAc3{ji#?)XH?1lGI1gj~( z6!>A9)x0Zb`JUlZ!8{~OjU%vM0 zJu1(qdzUr>Y1Go8k}|Dzt=sMJLAQ$_2pH!AS}7t`PK`Ds2x~-ZMH3yLxu1pCFHD>`Td;n4|V%p#TdIb^uxDvA#Nj-Ud1?Hn3e)2I}rXmbtn_JzGl>#=9>vXFbvAnmB42+9quywN=|h^r1FV zrK%ECRi#Q*6BMaQN?JiNW&;!mHeI~&0}QsY*WTIP+1KpM+}FA1oc?j|nBDR2Vg~~* zzMr((+1Z`hnLFp+@BCihhj461|Iv5zfL$jXW^=38ah!{#lmnsPylQ0Y<-?3Id#S$E z-I%R8lreObyI@<+Kr3jLj4=)fK_uf|%Ba;eO4FqI(?%Y&w=$s0d4^a&_JVJL-VG?a`_bqj*=)Rod0(9DR95efnWi!pG^ zg3=m66eEg#L`yXoB_SAv%|$vem`J#`10ihmcJ-jk$)@^KEge87V1yu+60u4U20jv{ zVNt+!959;BSuvs!#sOMOH8kd?5cthgDM3GIK^l#l?)w7n-tqgXKHnM_?^SgZy0$p% zx~VE)RrlAr&aNFPxicZVmCxVVioNpwQxOh1UGacug_BP{j@^$wf|n2O0b>k)qjj>o zSozdwqHZyB3Jj`xme-U;0HaE)jKE6bH$n&mgQ z0k{$A6s`l{cNrnOp8R6pdtN~MP_aUbj%U@Emn^Nd0SqW1uIm0A*{JM>fnuTX*zHXPf9HVm<*nW&~tNr!TP+fc@UCJ|)V0<$exu7^aY!=MxzLMPya z!u1LeoPkmXDjX~pMtt~|?!##wfy{k;WXDa*Dj1Ni6uEn|=7l<__w zn(YtOHX~I=y%E4LQi<&Wq?$k#q-}{n+BtIX*pB{P7xIU9?=?5NgpdIMSAmdQdIkpW zgj8i?OfR9dY=~iD6wzX-L@e7zsZaupfn__;+8~Z2Xl+m`mEpQY#EC)}_+SL!j6&EJ z;zWWFdaA?DbDV4lHZ@i|F+GXdxfz6Ugqis&luF*T3Xn2u2C+~ol%bS@?YM9)2g}WR zD!HT6p-#p*5|w~+0mpVy!!XywK;IB5r3$($J?QD`h3mLTq(l$}n47C%`s4|-usBD; zC#8U~n+~B%toAWlOXlCKk+$mmgHEf8^#ol{Gk_$g z6VAFS8=Z1G9W140XR-X;GkEd2C-BsB{|zZ)(`+=WG7P^okwpJ-A)FI)$L=7;BzFM# zHvmPX?ah8hAR&NiMjTb7MfrO6^Hg?SnL=9UX}3lP0K)z)@BezrZ4cK-CMrUn+R=Yb z-|u@yG+7GlR-|S@{Qz!8D#2X>VB>U;hxTi&0{9J1$gZ8|K_qj&;48P>#VyOa*mVn+ z#WMPpIF7H;Mtd@m6~}g*zPlX5BNQYvLhJ7vM9Xgli;Hu@ zaXseytptR`oO4?%#Z;0c1|~^lVqDj=%jGUJJh;j9l)9U}rS7=5tB)$Joj8f?+Vphb zhraYfzb^~}$T$WF;1o+3-u6LUefK@+zxdL1TZt)Izl9m=-QKXZ>@4nIr`W$%1k)LD zttLGKxwr)S>VDMUID&uwuSf9O;r)nX*{aXaKM6wapO9hwLOB<*PPA=VdVMM*6P*!F zloAMu0Ak*5k^g+e?GMipWlURH+RirUJR{Jy6KFqD>w*f>c5pBI-4)s2?aoq$uK-vc z+tL5FN+a(&(PSyH+X4ImfSa-d(b*}YJ2rGQ`#J&OS2-om?)*YdGV9?_fAKyyNs=B) z*$4r)3So_wOO^hD>vc26M-!FwgP_O+FG`iziwpC*Qtr~8R{$vCf(sa9sM3m3&Kw!V zV2sH^p-7cd(NuLZSZdbIzC-)s#rlG#lz0ZvaTrF4ltGdt62?%*Sb+&qkWmz9r6gr6 zQO3Y_+$ae{6UUJ!TsM$GXtYWU%4O&Sr~!B;Bb~

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 075/199] 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 ff34a168d..581e51aed 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 7884227a1..cd90088ed 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 8b5c63ffb..a3520e722 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 3fcb2110a..5bb448778 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 19ddc8995..63833832c 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 b07a874c0..7385d24b0 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 e913849d5..1d2e44e52 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 f96f641d0..6bf5f4288 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 e89026569..84b692730 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 000000000..c9e3f956c --- /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 20cb91770..cc3f4ec11 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 8356f204f..000000000 --- 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 076/199] 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 1d2e44e52..6412728a2 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 077/199] 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 83096aef4..e63a25b5a 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 078/199] 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 b6e179e19..51b5a81d3 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 079/199] 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 51b5a81d3..406d079c6 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 080/199] 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 51b5a81d3..3c2ede8ea 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 081/199] 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 ff34a168d..9d528d7a5 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 8b5c63ffb..57480671b 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 eac68e4f6..000000000 --- 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 5b567c6b0..000000000 --- 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 01a38a864..5a6124727 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 082/199] 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 581e51aed..19d6d3c29 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 6412728a2..d5aa4c1a0 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 083/199] 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 63833832c..4d760df2b 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 c9e3f956c..5f2f7a92e 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 084/199] 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 9d75a4574..e0a80f20e 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 085/199] 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 9d528d7a5..3e64b74fe 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 7884227a1..a79386293 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 57480671b..74b7b212d 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 e913849d5..331ca0e1f 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 bd7cec6a2..69a8e3df4 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 ded333aa4..65f4a9d51 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 000000000..6f0411349 --- /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 000000000..10913d1b1 --- /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 000000000..b743644f9 --- /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 8bfc466d2..22043e441 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 c2347cf61..bb5c8afe9 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 a63d17411..d6ef442b3 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 fe450600c..5c9216eb6 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 5a6124727..a6cebc6fc 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 b85cb7423..0a70ddfc3 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 000000000..0830a030a --- /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 000000000..e17286e16 --- /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 000000000..c184b8f3f --- /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 086/199] 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 331ca0e1f..a921e3781 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 6f0411349..4e1eb4889 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 10913d1b1..6562ad2ea 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 b743644f9..95b0f8053 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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‰t1L=zfgZ(P0nn&YGu2KTlx+%dL7yF!_(DNJ{TM!GVIJrCU+v{;y zLc_Zr!?D%`1D5QT>y|4xMfH)N@>y6<7q&$c3jtC z#6PzRl$FmXx0JCyJWUWG%LTIljCQYy{3KRTj&pl|Aq#xTO=VT5fD)H+&d-%qr14>G zxlSdJ#r0vB5ln5z;KW0a)?hH`V>}jEi3Jj^(eMrpAc7VWUwiHZHU}feq7xFeS_9Md z7&XsQLmsyvs2mn7gOV{sZxgmXQL+WAlHIr(?U1p8 ztjQN%|Ge00Y+n<#+a@N)U)iJ!!%^~)W2eHuchj{Ld>x@=nYB368{ku?7xCd|P9kwZ zgG?Z_!@$|j`RRCU4O=eex}3{S+Op}VnM#B~tfbSfF`zB;%5coT`S3IJvEygyN3XvU z*G^4fd!zn>lMXZE$<+dd=pLinaszaUyz)RO-qg45P`}nMkLp4C4rsjcH_Q zf^m}gH3-mbwUH(X>Tw;7X2T}q@t!nIjLGzyOr{rait4{*jQOocb<+LY&}a^+=!zsw zzQ5UQM?v6j@B{4_N4Cus9#of3G(~Ix0UU%d+jl~!0Hg63pMToQ`dGyPHd~F<4O9%of-_$I_T3t*X$`@j) z<39=T~Pc-qfZ+jEo_xpd0p*8t*>Mgh(9fh6e zOFTNd5wA`Mn7n-sGLG?$?43j)&^a!zRf}sunxzM!w zuYjD|4h}-FWye|)t{DtQiMNrF$TO8(adWFA5{}Iz;PYBUIiZlJ2UCSrm9yAfoUtq{ zYut%kd5oj9WXYe)QKdz(qT+)7T;e8qoyk{a!k_-~Gn5cwOT>Z@ROoDSJjPUT8!x=^ z@ds4kOZoHVf>ZL?P|l#sn}C##ww+H}c?5v9ZrTzRft|JPAR&v!VT*}R6qBEyb_f-w z8gC6)6DWM<9Al0^GJtGO!3qU}Yxcb)MP`g=GYV)G;IT7jaCU7S{oxRSQqacfsO~&; zJ&x?#1Hi&UK(Iz0BkkV3k5ZbBU!^fF4G7zCVIr7xts{-JZ| zer_4zzB<}RrqQ`^3aTBvVg~n~Cg}CLfQnIH*n?pH3SedzAf3Cf_4tF7ZcNXMXHanTsc05e zjKdXJ7yHc22*`~IS*FVsQvTeY;tb@1Oj|Uz+=3++ifZp5E&b%e&Oi-S;d>#-l%CDT z3|C{RoI2RA{Bp8*5j-Y}^_p7=qNEN>^6vx1=aR=(GcyDP0;N1&VheB|?3!gEEVW!v zs$Fw)r+rZos^(22&!NfJZMjy-dhJnOE##J|M!1Gu(IEv&mO@r0 z05%zc`#o^G?LhsC$!2a{!$K57PXWOA$(O+1pworkA)k~9Wdf)qD9G~6z!qX-4X@d| z2mj-DeFr}H7oWk!b(gVLJJ`n5YiH3tx{kF!UnG5YM0RA^oKI!u8(2MX9__nk z(KygTV^6bOAu%vMJH*OgoX6;RANu%+NrUDZG2MG$xBP==PKN8_wExJ`DiKj^m}P3L zw#-%#2}w}7zl9(|Ckp9@uGx=U=V!3JS;Iq1tMnTWKZS0Zab|33ekqBgs~h3Wy*tr9 zGzpS~xp(csMLH#hU)!{LEw!v!Zsh5~& zCWVmikLtD8#kIH@h7kb5I870?I#}3u0P_c~!NJ?^#?16QX1YT}n|;Lf2HJaO5Kh;z zytIhT?j}|iFXH2W^kJ;5uV8(75dkc;PDzA9NhK$eaq{gtO~1gI{ZEI(+P^uPj@JJs zHJe&oyMY-8K|mBmXhkvZz3xhM;&4mCX1>zRn~rA2R~MJ@(dSO#*oBK2WtkTt3XGF6 z2E)GFXHx*tnnrEsHIO?Gf}=(uD9qz$r~t2Bv5LZ=ELw&OyA;QM;)0cx7uLKPk%D=HcsRmd56!;#(+CTEbjAxRVrK4uG>H&1`$m7)ax zSvcFE{5gtMTakQ9Tc|ry2uLN}q+rX}OJ49vu!yF%A)Vd@Tzt+k+5kMcx{fQiE#P8* zfPeGpFW`|==V0Lu1NBCPSPFda?YCmv)D&PecszC%J)GMcxrRXC!tt5326Z=JqX-m5 zUYE0#^_FXOs%zn50*dRf%@)K)R^UB3JB>;c>NdxD815|b*Je8&z<-9GD9SpF>rod65}!(Oa! zUlJRix`^!D*sx);%%m-_I!@^MjUN8#ZHMsV*I$Jv*EaCT@){nzup&B9C_*8yG#umE zwM`gix?{&2-ne@^X1pEFo^})OzGgqJp6cLto_wAjxwy*OGTD*k%|E$7-9KM)P@UPb z+T^XPG2dVs0&+G)^Oh-eZk|SMPXj`Ut!NEj);stWH=Dy@O@a9z)LX6B2Vu~*)?hpy zqcgVy2XDL;x4ij#am5Ws5Y<}{N+HNJW==05YK;H_!s!VFQ!%Edr`LK5`ayoEs50kUU zv^IsMiBtmCxF#TlC{||_S1=D>nR9z5H(gMHvsjBlbXmOj9IR%y zHFIX3?zf!5CwYwy+-vmfJn~bOYjWNq$IsGgOAJ8iDxf`!E?+4+4UnU8r~dQK@fSe^ zsU);EuodAP!L7$~YPS%jOGECf$KLtyU@DT(7U<}^=ZMWmVwt3eOq=fBEK&+%LW+2z+NaY`BfY)X*eZw4-99j+T8%PI%w1y?z7V50Be6u z$fm-?tuyG{JdN@BA$pH((B>DHq0f!MBMa6HeQboKUq9w(yB6Tg9qVQq5xr&KPWt|< z_7;9+2PWG1vFom)m0=Hi%Y$l@FbkW+P+P49m=^vHZ0QtK!!mdIJCG z)*EnSZW=evPT}udd!YFJN0-;}zOOtYmj+|~vFFbwCpP=RPaHZZBoVq40O7s{n%7UDzNd-W_Buo)%E*bAW?&1C+q|-7!`2=k zA-*pNR8vYxMr*X@x8vJ?Sj>?*5kXjd(r9u{F#BQHy7_o}bo@nEy zefx0TZFl48hwsP7{?ot1xd*=tsRW1w)#~+$G)aCi8z;B+H#h#_;ac+(M@N18t=wo{ zKesc;vg~`c$)f&fKwyJw=I1fnsFyxw{@d2tV0k#gho3r*FP%Dvv1N>%R}_LUL@ll( z3=D>v(M61K*M7vadl5`7K&qfp@VF?7c@fQ5Iikp?FJo?u$5NzUIl*gMjJ1xjMFc5O zDA}dpaz*3|sm@G6^UVt;2|^&HyMBG6vX&v0UyT*)FI!Nh^Cp;wv*mb*!;Rc)a*ou# znJUNh%BO`~SW2{&+E-v8z5y|npu3Q=pm^u(G~*);3_CWPBS5&xPYO{HubSes21;^3 z-U!*pXQ!E4FNYpxGr8veD0zJfW`iZZkn5ZN$pKy3_g8&(Yq^KW(g7OuT$slSxnwJ3)OjPK%N4Dsk_|N zv;K)i8a>s69U556oLOtWa(-F<$^(z#AK!94CgZL3``}a?cWj@-$Bv&>i8lPy$_D-X z=f3I=RhIoN$yLJO3(p}IBo9OoodAu4ZM1HfL_AkRFcDt!&ksPVpcDrBe>+IC!ugeDX+O{2MuQU#nQ55N?aHHi z9fx20dhEL5AU^URejShf=^r{xhR(S_Sfrd&=*Zq-sLdik=|ot9-`3zO&ES6!DSS6q~Bxq?z2Ak7(Sj+IrqE$0PMXJ*S< zD_7(TPl?GX#DSD0rMSAv@{^MkR9SnKiuuCffSee%OkbP zX_u^$}LjX*U{no+T| zX-|OqJr_f53odlgkS^1f83VDpf?zZ%>yLQpGqq|dw62{%^I!+b#WB{ud=VSJe;%HD zoS$>~lPAv6XHT8Sb+eQBo-20a#@Q*XjK=u=r%oU>Mh8-e0~2jhFX88(7=sT#cMAXc z@#lH0jYdc^*cH>xzjxJE@#NQ0soOVBBHmfY>c`Hb_rMyNiz%#SjJ45UId@V1D|&){ z_Q-XZj-#Tc*OkIg96E@rr#ko#k3JKu3=>X`*UmZ5Fs^YG$UPRDi*tkG*%?gFD}RFG9~Pub$SyjBq%H z&<4V2fTW*PPy|BRehQHr4s!XLrx&*4-9P^?@a3y-z^8xvH!xZ{=Wh5YR6!6N7!3x$ zlx5le!*S#Pe7VlB-|UCAW}8&O_9%+TutC6#`9>X52=vkvPpz!uV^2SaFCIGu6-F2) zV+2v;#Q0v_KTI`7EFouhL(S}hn%z}2lX0SprY@y)fI5|yGFqDg*p|hb2t@<{Y*~CQ z+XCNkc`^o4sA4KmN)^DmM(HP3>Y=wLRObg;C8hH#Gqy5`hr>*8Mv<5HDVR zpWEo-gO49WFU_=-u)#za%)fgt;`!Rksu$r*gy}bJLpT*-basI8$pNwxBas+uzIbLy zJ+-=t|M;DEV!ATnw8H>z*|!suQHX!{$WwG`qtBOU)+n=#ND5p#JBb66ZOqmpeDLvO z$QV5Jx&`W7Kl$nuyb2K$#RB?eHk;9SG%uvs-tYBn7)7$tXyY~CeJ|pAv(QLX4ulA_ zK{Oack9)2z3KS(a|ZwK{r(9@^WtAtl0}KJ^?PJbw`uS3x$XoE}4${$Qb!gdv>DHoS_y4 zffGV{JR~6D*K-ySvUqOc-J;p@mRRLnq-qRK{BK1S6weB zzS#^lpqT;^_}L@ZVls~K6$9T`w8pmxMCC%T}91REv>WMT}T>kjDSC(q(t zSMEb7Me!Xo8W8zufDCHniXxehnaO+yf2CXUdFLOi{;iU0D{3CfJ7 z@tFZSN2b0hXYE(iY?M?H1T{zywAR?%SjYbBZ}RcKz@_QM(I|ni79#(_*#U7Yo(Nvf+QYZ+rWL#pM-`0Nr_s$fsoxvCCxGfaT6l$K+f((bI-L9 zQ3HlixEf25PcN?Uhhb1Q6Hp=8bSaRk)LZ$}v9XLWkcC(=pP=%!la$Ia%{*37r9u}) zxiX{Y(-8crTkA@OO~u%fDu70nB}U}1om`+<)!F2Y%z@?(6wmxhulRkhGnAELDhhiQ zTTBQ-5|T74^e%am%Gq7gHxI3Cv37*d;gPaz-qNr4a@HREm9X{LkT%vgPT>G)YY_+_ z8x5exL!>%`&JtMT9jmQdm)KDvD8Q`;uR=WvE1r9PLg8FATsBTLn&2!0gmaP3vug4$*OJ(>HWQ;E?4S>7 zwkkn=wlEXUW26CsC0RBkrNH$DNN0|efC}SU%&pGE%`BLh*Buc+3kSVGJy*j1e$9l}@C zY_!gHHzy{VN-3$0L1S(k!YIzKqb;bDU`9L`=KCZFV(O z*#^QI#)BSAVvuGjv@xhRr=ZC~E?kNF&a0ss9cQ9gvA?I{P|8~s{c09WWj53cW$B>N9UDaHCGyn@H8L)sc zuwL*S*XmG876gDmDI|%r;~y~~WC%IE7qT&dtT_d$bx>u5bT$QBcL<8<0g z(k=C+6$WU90rq#A=%*Pz+#6~D;_&>GymF%LVlkMZJtHtD3Y_l`@!@Arp`T^$V--Tg z`x-EbeQ8@50ywou*Afg*^|ATT8b)8;0Iz5uu6zj53p5)4v1Hcr2OfQfrsEJFKY0$H zKDnp?w#NF-?&`K6fTbY$(8X0;(P?d|rwjEOln{W{*7P!IGyBS$O*C8fmG7Wy@mkZFfV35$u*x#QiZCr-*u^ViyaqOh=(DA(*wHOY;j9vM>NwFz71f zp2_9UU;CQ7p?=|i!$<%1`*7~bhe1S8K_F#I9US!s|M-xK`j6`5tAE{1=AN1D6G0H( z-tBMxaF(TS=&i5MMsW>NDI{rvaWX~_1TYCBs&x=gY=d050-e?@L>RjQ;Hjskhzj1i zf^vnpq%`NJZW3@_t5ljr5{qJr$k$OtjtVm(r3%3%;Ha`H_a56y>?)aG-YqiMl~gC6 zYFJrG7NLSvV6BDG3>IFikrygaPD_r#MV0&+cK8ga$eby1Wkj_JSdhm6^XaMp(`D(0 zmb1S~!ht*T2w8@>8n0B{5Z3_j{$aIG`^?v41=?KXe?p`w z%xDOxzKs9oO%Mi$+VEs^Q!baj-7 zfA*zEvZ2M@uJ1%@357`VL+5vzu zd%R}wikgkJ<_;l9812&EmY#hQT4xBts02^_hgRzi%L~|aNdxhZPI z#_0cPFzmlA2%`O#c_9d+x)1_FN+_i;7!Hu88JHw$du~9q`)Z&w2Lur~_ZZ{S6f$2I z^By@ zE^H9OHk;nSIh zyMGOhnaXXd|{0NVDCgz{m6A!;>e4{UFWx)UDeP7{f&uYUg-H0A)1YmqQ|aP;=(Ii zqZz#~43r`wvJAB6w>faFI6t{uE^j@d_YLY)N(5vE-1mM z3L%*aa6V0S(SUM~u-pNlDw33*ImE~(AZskFU-O`#GozT0OZs$) zv*h$^@|0$hvxyt=DwszZ(LPf)Enx%ggwx&Oqr9`xj<0(QK5Ru>?cC^^7NY_=W_f0I5L6Kv>=q z(3j`v<-%JKKt@AsuEPWY5ZAFqc9TmUat)3kpG3;w(5>xY49Muq_2&MyMnQmgAHEhh zF3jP}r_bT&`HRq}$JjW%h|P~)Ky;*qsW)yzeQy)t%;hDM$K`Y`UxdZnOAGY`S!S|e z10uoR2Pv~w46;lv4LJSJPdtxNrc(wY(U3uOeG}3CCi-7mfmuqy1~BUx`d?V(!I#!c z3S-S5xD*j7g4!0Leq;jSwi@Eu8seQbkQ5N1xE%NKn}V6wFTf5p)_?zeWXF!HPa#DU zubDyXstz_DSi$hVb%JHwJU@ds?B4c*xD$b}tKGoLXzcK;%~!4-(Y*}Ojyn2EFlW}{ zm5Mr*RN9@=1qg)JlvAx&DbJN$r2^z5%MgtKq81c4u3j1X(~>~itZnhex@3~x`rQ8H ze(0*}@s78@7k~7T-$sAb1p!oVG+JXJ-kFR>n}>ti`;KO#(Kox%91h}LN(Mj4us=RI zF+CYYHFa@ik+jyR)f%Y9HLR?yKqElB?=Yg>*FiR?5Qd>orQ}j`;cZXKYm8wniUKKw zK$ht(puLi^pz-Fa`6jV|`Ddk0xw8hUWs9*-UvOb@*iqJ73fchki zP{=Nxf>}P}91K*1V17T;#9Wc7VyZfyTn`~+u_DaZbB?K1vgz^$83n2^$z=8-MJc!F z22j!Z2vMHcbCXf!mHYX6PHc&4E*n+O+EYqgGSxdlYJo`#0;eOytIuNh1aNVbpgZoO z-E1OIAx7gNvNXl?^c;dP1T3If;)#|9lF$RMk(sYdBppOei<11h!BEVhy zcVpkeEW#it?UBe$q3p>NlqZ;y0BUux&Y){`P&#%R9M8ERQ-<5+YY34h;LUYd6@ZoU z!VZ`I2@+uG^L2>85)ei^1gF^qx*MoOepX!1Q8*YJo}0xDb2E6ywO8W5KJhF*aQ`Dd z$&@kvS{I8)dkC*;VCuWKp|+=qcw5aqmtk=nv4BSu_fJZ0$)HyQs^%>0M80WXO^9s{n?9wZRa&K84OTcOshI<19idw(-IzI+ngr}If>%!9$Ti#X+=-_#*th_-{U9KN!i~9V#o~mNw!(1Z!hg+j?nfqo!nUSlspX=` zk`0DJJeuP|g@95x5oEr~3w=;s5eVm-R8EyfMQ4;xLS;m@`EfSGV& zW0yoA3h}70reebB{*J;36xYGSzQb<$61_&%(guWBDqKB1g{yYW+bu3ZyFogh167*tvR6&H@?Ir|WBts07m=|v>E@ctp3nL^O3DU(8 zHt)L#vyy>_7CbQ3*d>MwO#2WidEYnEZnk_ohoibQMt8C-5shX8){ec~E{c78p+7JN zkr#6oPO!}YmG%zAb~lTZs^VshEkZ;F7Z&)r zrX&Zx*?S8Gfh^VVg{qy3Rh;oFDJeV<%+CMJl0-PSA6)O??D8TG?A?LQULU7VJp!?C z6+~mgPe}RubgKTm0a2cgSx}Lwh;Yt<`C^qvrSDmB?Q!W=5O`Y~=dz3s}ToLff*r>L;zr=03`fs$(`h-@g8Ney!b_?0U}JCTLxLcC`U=&7ffS#<4(Nc z&>eXC$)kASOP|DG?L5|3ma)0MiXaG)rU`1T2G#}x3_te;y!Z95L%UJ;xC&Pk{I+NU z5E9&IKy0kL38h#uaD^yP*+MWoFT>#g90cHI+v7B{_&xRvRq}@@D+3r6z)nsH>0XabU9bqEzPP(HNR7vjJL%CMcLHv7PB}o2Q^1&Mp7(nXW!HCeD~eBtz8>sPmf3 zUGz<6!DBZGnxO$H`i3%gzMe;IX9JBpr^cgWyHH!UPpR<1hbxjHw?C{UCKuWU=zSc0sH|; z@wtZwee<#>8`W;wY<<&4agCE?1QO~6(^i>*`sM&K&D_ox&i_X?%M~7X6OduB6op3^ zDtJQCRWA{F=3W!zoe4ldJ*wdc-ufOaUtGik4}Bg&3Pf=f2q|tF^#;GhqvYoRJn*X1 znv?NeK^Xl|5J&r%nUXYxR52PeyPP!J7i>ww$uO$d6VYKo35-z}g2S#t* ztrl4^eqEO1F?lpV4r&|EAQR;BR;(@QzSSdD?(=1=b$0q(iW+kcK2^NMbK_0rU5~4i zrl8U~p8-&F8XIYh{9!5a_-QRS0oY*_(Ni3b&HDL>tiWp~mCN}rzm z+P4ihjCK}!v8Q?G)ze*b?pwYnn`^Hq|9#s&vYzWC1G#G$Y0K_$KUsLr>ao=?j zwZH&JHE>*WUTFEh!3<)j399BjDJjuS5~SS}yf}gg1*Ts+kIoHKSbumG>z_Ce{oEKZ zjO^GDE6<-ULa(o$o_JAF=!M}3h5^c$f|>ZT)m7)n6g94Bb-GWiBU??O&y7*Lu7%pR z`sK~0SK=Sp>UitM=T&-$P+`n4ZSi-#0Jzh@?Ou38^L0>nWB$ zy@;%rqWj5<;DG^;>tyj^wuP_7f0RW!|@L3Ust6%GGzAV4WEzQO^j>uX=QU*xV z#GgR7W|0WXsPzUA#zJJCX`+;4q5+Q5 zwIR9ZH-*B#=ojp$&bA`lbZ!AvDsdiYMFobDV<~wfw3QR75CS4@LG3w&vnP&XM=UWn z*~Hnik0Py4BiwyGIEab{XS_Hw&#c*!r6b7U2dJ>%7?8p_8>Fc&Xx2F$o4sKr8x19; zqmaWC(-4vKXtVxxlc3TLCXap2qnUGJf_JfoF|Ze&gIs-1`p05O2QoyKvo}tMQTl_WO{qICthGvMhy6BoYgZCuZ@*C!WRK*BwMH zkPv<{-Ae!TQQn;ntTiA?T*>xkokgR_S&BjwxC^BTXfy3Q+^kyILJh&IvOm$FKs+czWnJ(imnlG$3Q5_d+0CxJF7t^J%u4vLm-$WO!ikQ0E z7=yhvn8$iZa!abb2{L1(4K|Y$kFT!dYZq5BOf?KMdY@cI?~}_fj9qAkSO}o@*AR9h zG!9K*_*@r*PprZw#sGNmk!17-?gakwA#!yI6^#Umo)m(aEmRP?(?_L95o zPj?I}H(Nge5?!2zwNkKtFMf&XHMSi5Dn*sRP+10{Eriz4afky~T#X<3segdq{Ec75 z=E_Bl3ca^4)xt2v1VOuV7vf8KFuY+=5F;VPZbMLe*B05=8 zsa?#mfn|*#Pz9SU%`(`e5{Q;-Rfvj6=3D^D3?Qr{-ggsL&OQM->|?sYSnr;NIc-7P zuYnHhThchCauZp;GRu9#q}RM$hS?CHvkW#FgROzG33#{(N=6Rj=r3c;K(ufLFtHs{ z1;q-m%(5wc*HF<2k=HHdxLB@5N-jK)^trEMs-aM8v=GKIHaFHWy)ch!Zn*_-f6sfc zd*42&Ab5dP_R45ph70c_Y|Hip5eflV}QVt_=$j5F9?3Qi0-Gj8&+?Bcm|I*K zY-`jpcF#!^mO|&35@x=X0J6YMfUn1n|5cxO;_KQNKh^^SSQ(A++(s7{hC?h3#`vRW zPGX$t7u-k{e~~iOwKW7gYX~|KI=4+DXa0)P$BVRt7=2JWTS=(6(7>C z8my89ol?dDUjODdBc5pDSAOB=kR@XXA*k7IP4+f6-#1uWKMmj$FXw}}F|0Qj97-Vr zFo)LK8)XdgiPBEa zWygx)L|$7mZFyBFQWS@jhNB^;&sT4I+bQ|ul=r>&yB{?i4*dbd42Ji+<-PAY&v~9- z3lv;}(SYFyG+vL{SX&t$7*YO0cng|9UsiesG`0CLa{^;yoOKWdYkEJP;w^oSk}BXt zHfGs`f<%lEk^Qcuv%O(1{nDGK`dH_tO`?E9dC@4#y3i-zj_a>{6{q?PN7oZ+U9K2Xh(FdY+>GpdE~a3zfEjIrPj?YbHc=Njcod=6%P^jl)_}B{ zU|ed{6d_HQpu0;j#*WyU7-$Nl6OUaft*s!|vGrhps;WVgZJ4VsVURHlmMj3z8OQO{ zXYi4K=g0AZkADm+8|(Iz?mBMYN!df=9y|LtG>8j`F79z`goQ(S!UJn-NHP?r=A zuMF_e$rGVukS`+b#8^N2bUG-Qx)GPm6rE*q&tCe0$=Y~_S3L{o>cbcR`#x|(`2Tq%y%=?J_NeQC6gS?hjlsr zKFcDASm5dReIGvlw|^GD`)mIbnBV{rOS5dPE~|h1WSsoUUr+Y_G>9;>4kK_T>*|PQ?XB(*vm$LX}!>7E`>$IE4rZ!pkl19S;U(_SxKT3f5nJ@n9{LhwE*!p*B!(Cu`= zguwRJCDbo{5$fDih}VwUxxdqhXk{R!&8TVIfSWyt*sOUa84lz8#{8G8v;mkC$|=*b z;J4!+;T7$s@g^}^LxG(4p$Q|o$hkpFSA5z!l8`Io)cR4}xIWwHWNJl8+fEJ!ftpQZ z5O@0!=N`w_#lJ>s#(*x-V-mv`{}lY*_kxxV!#EdcqphzPak%l8-C(PH7i#A!XtW8l z^(s~eJrqTO$#jIO%#mhY6uH5)NKx@F*zytZJNbL`L>t zB^3-KOH1{JpPYdNfw^Q*!wD2|P{&FHDCS@>2RSVeI9Wfmgad;FTy>yZBREqjKx)+6 zIrOLmYK>FJ&){c&ax`Y$o_kR5O3oqgRqenvAb(``z zlOo&gf_5wD$gOj&(9lQQ8<``ONC{RJFyo=kz9Dj=PS@EGb4HtPadQUKA6RcTt=--* z4-{DXaxbyrY3f>q>Q;&D^ua7M7BQ@S-y!twAK>z9=OM2YWNH^iXACBFjo^l>abz6#zE5Iq6j6$i9C`(NMZE4iP}-7hu(GY*LrLcqpf6i&qI`PYsdAX{|j zOaN{(Q9FeNBsmXu8AT_xxX%J%sA*xUtW}WWqO$G2ZgNAkJ6T6@jL-h`-$ikCi2wT! ze-n%mI=!BdsXSMg)vv!x_y4I7(U;;leJGBim7>g#6u^CJXOEv9Zf)1Rs`?_1BNhpM z_~@}NaW1kfBVvRs>k<>3k~HO6nu(O9IJ|rm_n&mY7r;f;XciA)2m)h`{}g0OQ8lj;&@i zm^T#w5Jyq)D)YWy7Q<*9q`7CUg_OuBw^3dBLj^2mTM4@<0_NbcFn+XCTJc z5cT`eN?~Vv3mdCP@bf?Ri+JM1J5j$fw6-PM%=1ed(@Reo*G=||XNoFNa78C=ptM67;5#q8fD>7Q&CwV~R+cRo?^XV+g_RIldU9F-MPW0Pl-gHlm{BdUCxNO8U>q!( zz;t_!3x#nJ1dO%zHUh=5WvluaPufqh26HK_^yz5szhe!t-KrVEUkNYvvR zyIlMRirMV#}u$0Dl=ixP+?01n0j^BFm6`Cf%;E@${&h+Lg z(QU|VYJ>V_(f)4Ul42tObmBOnrKKh8?Cheyeh7>=GfHkW((x2g<=SBa#L*R7DWrzk znFjj`uOBG-odC{Z0UuyC0M4Gc(s3RDQsd_Qq8q^rL=r^!^v6Df%U52-`EUJO8;J`< zjIq-o{AbM2mJm!6XVmTW(yZU_Cros*ELAFw5XTAQoRcydS)9TEsH+NPQKH-LgA{>; z!I^Vs@z4X0;OOadSo_e2@h|`3uVQrlRouLB1$8A+UB8IVL+{7(gHJ&jpms@&7ME!p zmo3oV2v);MyH!lRb-yu`TV4c40 zZYFyuZd^dJeF>|*80_#0HhJX=F)%I-uyhFV@zdz997WXWTg5vVq}GUtK`Ui9V%l$e zB%B9XRms3a3wb+k3}|3oMXGD(f!!;Jvlw|k2ImpZ9J>cU`>9{RBM-gP>Np%_+1}4~ zv-GaX)QYh(MDDcgPXEeNjV-gxAHe+YQ?2c2ESJE@YSIufK)9ll2_2g^OaK5N07*na zRIpwQ5|)ULy}rhGAxlu>x@p zlcu1u1Z3S}2Q>rrN@)Z$O=D1PmPn6(@4Ze-13glxlvaWf|GBr_hfm*s1~2Ul@y)Fr z{N78Kuxp>!s2eqne(yR)61WvHbU(3%rFX6)J(?leNWb@{*^9u|1oVz0Q``w|Lx7%I z!DN0l$LRbH>Z=9HFYZ}4Xg@hj=fdXPxbyT<7wHaN#^JxlI-vEhu9)=E@mVMZgnS&cVuLP;5{?2~DMsHm(FB4eP4f%V<& zc~Zel>u>`ZA{2uTR4n%4HSNt3bBr`D6}VD%0cba>BS7?GYw4m5>YW0sORM;MKlRJ_ z_5YLOn=gI^!seh*mStU~RHgvRIw@JOontZ?!x+PvQ?~m|kuopbhRv~Y^cYT_yB|-z z`~5iCJ%+;@$B<+hOeApMeGlNL|BL??zx@yXKI+LHsn1oDgov;^p5W`3F5|p|eR0;w#7|>q^m6g>RIlYmMji#_6rK}nG z-F~X1fvGhV4ByO42qLU!Db}+T?>%-1pSt%nzI62_{@FLbP1nb{T|C0-+s1!*3*-Om zCis~I*<(xSKemeA**>C`#Xf7q2^723pxTfwHF)Rs9hHqT!K`y~eu(_a1ZulN`Q;%@ zrG4DMtTVKKEk=k=rAQwh0MbBxVTayx^bmgK(Fc)mHq)?_N?QT9CREpQ;DNy%j0p|& zM7ETWcjsrO*eI!PmhiFr8OLlh_ANe5CXL}`Y#?h=wH!F~5Vj5DY2MQ^!}d!Kwej-5D#L~#@s zcI?}f8j9Hng>#QQfgk-({ww^!Kl(4xEekNV1I{?cFFpgF_CZTWW;S?VU_wf!*RQzz zdVj5IDO(m*wC@@P9f=oQdLJ>>iXYG>XgsuA;GPvHnB8O@0ov_(6fh+J&ZtGPk>J+U(-8m|GtTxy0}*TPUwhA#YZwUzpm1 zOPli$)9mMn2-y=$Nbl_-Ih3JyU*GC$B97gEyNS&&Y}HrB)2Pxq1gy=FB8|m-)KuP4 zQ83z4zx`M490Trdu_0m#FxQhbJ-fEHfzdQaJ)K%>5dgfBSh}{=($;YBBYpefN(sGN zgvIFF@6l2(bed+76UG8ons6gJGR7`kB73rPj?%i)7J6g^CKa-^4gBaY{VMKxMBrV4^y~^q zFzCF75e%qY0;$qS$2sJ3ACJ86gNWk@|Hps&?_)9^gAvdZ67g%#Lq`crZ`Ha1aBJ&s zH%$r*4;=~-HJ16XIvvy6@I#!kHbIZ#vu!-i!n}^e7(^lpj4{tt^AW|$;b&ZM2*Dtw z4!&$&Ph($`W}OR^nqdwGyM3qCU=9rT*8WOsXcmE=eFEsMVt4DinB-gF%sLq}&apen zF`4Eld&eQ}y$`0d3?0S63oRsQx{ir6D}+@_TWpD1MaHyKIqha$*Ou1pF|No7Zhfc1 zjDorPA`(4Dx4UMqsR33_-G_5O_zCPDI)bRS_n;CSMK^U@*)9?aKbA3P24@KyXku0`JA(senl; zuug<`zvq2;>AB}H+}%Z8S5S&$OKUv$%0)bR?o4ow2+qy6eQ4aF8H4Hd!S;466OjmH zTVwz}YsP&pAG~G^sH(te3@rq7zaQ4Wmq*x+$`)1A?Zb@rKvfAtDmWMzr{jvi z!*6r;<7vB&=DeRwKtRel7^k>%Zy)O)K8osN$1%P*!t~`4#$Vcm8QShwUML_h6v+S0 z4#a~Q(oI}N*QLtAcBkqsw_FM8Jjoa z^#2y;g>#g)K&Bx(Dj>HYRZ%-LItI;wI*O3?x_I)VKY~Z!{}H@;{sol96y0tYTtqO& zVD0D$B;6jkh`|L1!JxOfhtBmao55iWbmivojKEU?mI&*m<=z>0G+_|Ky^lVLr+(t+ z@P+^Ve}Gaomb*PvlUo>{e+IpGd=M<@h7TP)!+fMbIE9-g2+YMM`!Sc&mZ7HSGH#mj zSrgGrpkV#)CpA7JCfM=2&nR1gX}&x0d3(7!F7S z;K+TbdTW?$UIMR6kkp8H3M?H1P8~*+^uV|X1x&Y2_R9Ha8Lc7VM1!v5<1UuKXt;z; z9mb5_5kaaZ|Hl`7FJi4WFN3bX46(L?-Q8^zWr@S*-iD9-@_&f*o^zNUNC({yN*mK` za>3HS<@eRTx8+iAUuVC4blpk^YKiV>f^?K48%~1uhkQh?1JjN3AEa7A?gCf49FuGKP~u8n{g?b%4^ zQaT(*x6(jqrt1SVP*=co3`8*~O=k|B%=n6)ydc=WVfhWde0$ZeN~j%xH;&x*Xa4ePj;}5Us@M z-#Y+HcoQ4`x?P8XE>&?&PUA}HeKft`0Ah?bwPtrz6&QgpMFCC+u=-fmO`gps(*o_#0|F= zT-z&+j6sxF&WDPc-_3;8vA{r0WHW+b)N&^vq@6DAfBeaPMQu((W>xr{!Eokin8tmP z61)@HO{5pY9{B-#N_4o8 z@$4}Nw70tsAH(af2jkZ6hMZG?_63C~*uf~JtcWpK_P1`hRuS^!H}9ioRO2V+!A9aN z(5Y=qbW>#7z`GAxvnHK7Sn`9cUWkSGY40M~)o;RNY;YST4+ zoP(#rJC>lX6wKxnY;yvh37qU7!J`kq3!lIAyS7^p5iZ}ng=^c}ICt!5z*CxWLbDKk z5)O4cKwUx`4Xy-8t5(n5A`7(9Yo%<5Gu(xyWt;BlTtp0GU`m0Ef?^z|(*YYv=d|Dn zMONwm?OWU7up>vecc?bnx<4CZkSm4lqQvrCwvE)rk`I&}aT#fvg95i?oTW!rFnIei z)I?(X>KM~Y6AZm6<&HA)<(zh2z8qj8Jypp6a@Q`=R;XFnw@<|29gf~RSCO9Up>woj zV-m>guiiL#4>J=7HuE#8Di~u77?`Q`omGW^*);sEMW5}>ve60vo1I=?uB@&Cj3G)h zfHCxjV90uM2M}MGiYj9=z!FW z2b#S|+?ifVrNCXvmy054sH1Lvqm&g>a$+#d1S+UjR@sjk?=(BMlTulKv2Yf(&m)zM zmi8fO%rOPM@u&B5V_|b;U?PFxc0o&*pTfwE2Yts8A_Q0{p5a=!F@^7twcEV~Z*Udg z?b-jw8mhNL8?48h1ix_shnG5tgtf{sAe?#6_v7p%Pc+s!3qt)cS0soIxXBjf(nJV9eEJH zKP^!z>n}L1YV1rVVGKx4!0Sh8hFXs84r?*J0eDeZ;J>b|L$qH`EuSglDBmy!DD6~& z0nn>!_IF)z(~LPwEQ0PY0n;fciohxz0*Z(cl9qe3eghk(cP${mGtc&I0n8fL&# z*vxlHVh~~Q@CpWRU%}yzA4hRzik+`)()i0;(A%}q+lt(YO|!K~@g)(n0f*7{u$aAl z3E4e8bWit!dFI^_f)YS5g6(&KGXS=W%7%LzK-YJM?Q3OFU6{6?wkFQ1yIpKFj2UX9 zq*4;l4Ab2mKx=gO#`bigZ7a-=4SXvJeSK(YcC9dz@!AsdUbaBiiv7&AHf>c(Fk_%1 z5rUmau%Ki&wE_wiPIb&!aVcVf8SlXPx;B?GN&r+6L33{N)(yb4hRN%uEsxlYK_aUa zvPq64A|`^aCrh)6_;8OPqUe}c|CKMB!avHFyj!?QP(G}?p| zOJkagM6F~?<2oRp;u$nA&b>JDc{Oh1Z9m2{-)bqrw4-A)f@aQO(=pK6p=`c*Xk)+) zg=ERLiKgwa+~b^O4-^^)1Bcy^1C!z63TkPB!KO_UZSa@i-DWfd@wR?F7<76AP45Zk z3$Qk`Y!mH_NUiy2Yectu9sOblQ4(V^8KNvptUdH7-v5)IMcV1M*`*7|W`RL@fbn9> ze{-I#I@9U2NNF>V(Q{ptf4od$>~6#ut}mf8oPbM(&TxX%9e$uq(D}fTB+ec^i>KfD zethMHXQ0yzdrINiS1#h+=k5Wa0r0~iOe-hEY`rJWp$AJ~J6o1z=DVvlKP{whwnVg5 zWe*JF5zOj_bDj3M4FfaEYK^iksIC!WQQQG!SQO3Z`Zfsyp)=cWkk9}RTI1??YCGlB zC9&Sl-cE$mBkao|YhF8vs-VKjIy z70Cr3a%vB+WN`Z<$VE4@x?cP0U;rF8O7~!0pYQl*p30y>Gy$m4S|jarAUH#|vxmWr zZ97f^f%*y9YR7)5m4ezUo7P@iYUOBc2@Zaou@TzWxntH@fV<9tX`5SaIJbHOSJIqm&jlG3m93Wu2lfHwPPgtgJDyET(_mHcbQV!xi{;8^91!fqn7C=3wTT| z6{N#pd=aw+iHB32F@TgBMQJJO!nq#{!Nc=`EapRuP}Ox{d2vQak|>M@1Fl09x}Sjk zxGy<`o$;1Uh;uCPkU z;JOC8`E78jp;e9Pbb`8;`2L^xES5Kp&bpG%Y7A!z2+g_46D>H{EF|VOWWhy`T|}PI ztq66Hja?ou_p!IWjO%BQ;rhKNuzB(@ihjqgKixbD(*|$9?;R+nQ)sP_b$WQ|@^wtg zvSmACLf2uOpPp&4)L;ydBtYcSiP@Z)u4#hMv?mJ$dSwk$mY}+tNed;d-QNkO*8@0* z;oMsLz|!1xOFYYaw+C{wh=C34_bs(XsrHiuQo(Wm>Jp%#?F{#gHx;}qke%#e`LQ(| z`OF!_r#rXIR2ft2ma{kBQac-Bh-$6bqPvZ?5?94Wa)-}Ph#m}$f&?(aVt?p6FE&J^ zjn-F^EF%#`MoS4@l;~f)Y#CyX3B?cf!l*+JOCYc9$%ff>zRnU%2VE?jnl;+Peg6SK zRFpxF;M;uOivo?e-p(C$)a9OV2Gyef?E`VKc;0Ru2^gww<^ryq>RSUw+JZoWfkn(Q zgsg!fU%+&U;so#h!5_o1``-?L5Jw3*X$mpAf%3-1;7umAf^zxN(Div7ezJ{ZiG$U7f64>YiGDAI7gbqh@#mJ)MG>53EQA?q9)b=p6$#X@60aZBx=K{Un01tlT6L`maKQK$Zr@6xly`Hk;pf-Ac z5uurr@@x^CFJO8O4_Z`+3=#5fhP~A#T)p=sZk{;?6-Ul!lb{8|^71M=Nf&i(yY=yC zik+=3a4mzohd*Q)&Y_80vP{b&iilvAmVh|1vC^KVO)XZND^NF;?taIBgn-F90B>wmcBi==wYq;$V|m+!pka*@^y`&<2BwDfjC=E)PP9ZvY^aeW zIgxkYw&tMjO&Mj+^tA619Jf;!Lj=fMcZTO1ZBV}wG*-$L9CKHTjfi7Sj18~8^2+5r z&&%;>h{^S9&^x=(-02MZ3D`;&Fb;in&l{45V^PuXKoMI=arU?gYcIOiB8#R~u&D95 z=DZVyu14;awh*}!yr*ooBqk06m@Q(9(m@RX8M&LES=F=ACYUM`v!gW6j3cel*&XfQ zNC-G`<{o_D=l&i{k~$wMgOz?4y{!x2x^4^#9cwFCD}^r2B}Y0zsJB}QjJ6g?AEK56 zj-s5d2Lb2tkq5O>AxlRq=TLyg6V1RnF#G599guyz^+=o9DIL|=Gvk6I0X0UQ)>*nC zHjuOFscIQUd|nSDm2$pqXz1ZYh~vl>8sG6rWt|GNL0(kub?xW$4$^fkgD0G&Y}+v) zHww6M0jG{1!_r_0MUf-!4)DQG|2#VVrG*ETMea0yI0`R+E_-NrGPeA=`KM;2}YwKc6T;$b7vDTTzCbht{uyh z+~&_*#*O2o`J%uC6c^A*2DxzrD(gVU@l0i;*2VGRXv`NfDM6zlD7UU6`mOAOoMKVg?<{oS>m;oRd!*aY|kSVpnZczaST3#-p-)yl_R?DKN3f1)@ zbo+1VkB{~};H3m3e;FVS-C1X2lmV)CdjRQ*&fP0EUmfjg0B6=u9Pe?)VkIR~CZMYt zE8qoo#)A^HX>ojTF?Y|Ra>Y@%C8FzkEhTBCbY6;)ych5O z;ZLKeC8qfl)9D0}%%QG*yUC3)4InNAgAiV%soiLaT$Yv{52+Sw&0`^L5u?XItR}^Y zNL@$cBcuai0JFkJ-%a>VA-skwJ~o2Ddft&6xfytWHyy04bZdm?uSFzy7;pJRQsxSh z7i9Y1_r=v1Zyaj1GF9!mz7WD@J3rw9>4IOJ*TA$kdf#C&5cKYKWc3cRG-BgZ^L%-To53ef<{l?JZEAyO(dV4z;%Qfpsr;nhqqY-8jLVZeP964olQ;k%v0{G0kv5Pryeoxvo zS|iMMXKAO4r#|^99C_#wR8^4(^f-5MyX8g(Uu z@RS^OWDB{C$5z!8(@Wn#7Tbbc6a{E$1&{x&Pa;Z^nF9lx!(|o`h-ik{)KHq|?=?2( za^5aLiwau16F09zv&JH*VGgU*jA8rG8nz$22g#ugbkZ)etcxV;ppqJU(;PINSO;c! zypo#8Xjeo+R+TS4Ndm-C0AES_{2Lv}DP+uyO99gwEFXi0d%$#JKObW-#GOkBvnHN` z+bD|p?5x|gt6>p=We&U48kyh=cTs=u@f8awxXv^G=U?T>SyIyRPIK7^aCdJ!Ka>nr7GxGZ9T6FbV?0_5-wf9fiFSxB^N|F`HK zr3DqdOk$`wvexLjp$Tc1Z_hgRrX+1FIly;Am^H2SbH#O>FOM;~jtNhp}<&w4(d*Z(R70GWQ3jJo})h- zu&M+f?#<|D{Fs|*1*UC;vuCXNL!BC|Ku0ljoHl%J>p0tTmT2Cn$8=bkgAI29DH|r7 z-x)d`fJXoqxH=loT%X%Dlhl=lo~Y0%#+)x4qurbq_MXbs{yFIz<_O!?(A1jbw0;9V zZf_R0A@Y-p`J46GTiehu1Ww=?ztiUjKN7WTMCC?lcemKovKknINRt#`4AWw2PmY5G zw462K%qx39tpbRzgh0{FkayD-HgPc9rhQuTUr~Q^ufYX;gs&T8S?2)d%nRYP0f36c zzHyc@`@0S^cd`uYLNJuQG>Aw68&U+<6|k6x6N2V00Iikr!f1AuPQ-BUV^8AHnR}7v zQ)gSEF}?HxsI{8%R5Fl4$by81-D}=yVjkFp|k(@LP?K< z`1k_fx!A{vCK3S+4cd>llv2TES~)7QWr{T;2|FL>VMGQ4$l87eVhvU!l@6zM-`zE1 zH439V3dP0aJpdQBBTCa4LRiNFKgI)qC=!Sw;Y>07*naRHyUg0-{;`W~R%* ztliW6!Kyu~YZp5aMOZs}9BUg#(9OE&^ag+dhSS{I;M6r}JaU#aHhMY`23l@;KBC$% z4vHdRU|rr#l-NRpCiloN-kLU;nb|p&B`_L>B4&vO3`RuIOG`i$Vb=+#{q+%DaeEu2 zVAer^EM_AXTzE!=ZX^H#)fcArK=~#K0^U49?bNp|Kz-BQ4Ga>_1M_UYP!Mz0Nq1QL zm>@*=WHTJ7yD>(a`6hqh#2MnTxTlk4M`c+eB7siUMb_zppIo*#mlAzuh^iW19t$^zXT8{-z-t9PF2iZbTjJDdd>{>Vej}MV zF}3F9T5xF21IxygGGyHo0X*f2~k;rISmnErn!RwaN^tpc=RKmfZEx{_SOyL z`2^bJD6W17*|{g6wF*%IN-@v+1ZOKm08dv|Xh-)VsKiF@7r&-SkE`$8S%&^Op(Lqzx?pO!}2{Yp!;PZ8e zczdJ^B{dWQ#>jB`_x^AK2%pK1_H-Bv67hbpaXMxQ4Tq$#kv4(x1$>eA7oc^IjCVXBu4 z7=J|+z@`)EB!TI48|zmBDC=OF=6G#VppbH=W4WyjSZbKUCCxgw??vGGirX(tp%297 z-bk_W-HhIoP`N_%H&ARefQYmGpOJv2II{Nqr)|_CJ3)aF2 znp8^0oB$I|(3+GAWmVgP*Ya;;)CeU*qNnS++BlNOb-agL=u{k8A&!N^W9;AKI0~JV ze;x!T8wI~MUwGO{qrS6JF0s^7yAgCp{&eRb>p!=a6>#$c>T-;t$WfLhOm7)aeCXo{ zDw74G;&w5Z$5{?M)GX+TXb#6=wBLYs5z%1&G4sg<^BuK+Ax5)fKIX=oryh71gQZmv z2*dFZ)o>5Pva&ATQh~`DluuzM4o@Kl%{VmUvz9j0(#jYEL=iApf=*f|U~e4i*Cr+50G_uex(h*b>$lV(HFjmFTD7j8IkVo3KLnv!TR>>EQl@qjw+>bb)4Hh zsl!2cVd|ybXxJ$8=8M<&d@*B3g+v&Z71V+k^#5jbbBKK7Re`{|+@C)6-58ujdQ zRCm+3ilZbILZC7QdMhi4Pp<+F*w(`W$YrqBsN)Dqa17U%q2@(j*l)IZkVuZW%f8S} zPC83nNE88PQU_lsa$SdtIJ6Tint4VL&7L%0kC;$<=8AWv)xHX87|y_ZF-*j5)`-?% zwVFBoF8Vog<{a*Mf8(n|dz7 zA%h7;>pk;0!P@FFcCTJURaLlpb=-;MBAR~w@Ny<5$g&hE#h90N1*=Dc@;*;sa;Q);uj zRd8mC4P#u$=`_yL42&^U(*m87YoNYusf^N)7k3?huMVtlS!*nD;D+E&g!3m+0;Ho)`{-V)M{(L3 z4r}4gDI_zBvYKTD&822qtRcepf9B_L?ZSDy{MTQE(He}8!HWvn`e~S~@47L28kf$f zlBjWPCfLoB+}uyK9GmUfi8<8AIF_9HVV_qZG&#AV+JSHRyDX8H_nEH>>HPBFD6>@F$n9;5YxxmvC!)2O^61 zwR&`$?S~vo$eX1bOc*Sp21{*lYiffMDY`peW$DQd>I>61Pd2#CITUq;lOh}p?h8h! z*_%XJ*ulK2yHae7*0GYbJR0s9r4>|OVP#`2+yH8G0-ejCF0Qf!N(fArdM+ze6F&Hy=P9~uU>f(1_lwg(b~F}RyQT0ke@@P0+iJ? z%@iD?Tb;k2dGjWYfz?%DdmAWoFo(Ao)=rS5#%bCefxdfl6TkV5^LY01b!?6HoC`+b zQJjY6ewzdV#;uQve9cOJuhyPb_Rs4h)ywipqn zQtmsvhXWuO=*`p#BL7vzjMzD))PL}3lz!P5^E?T$^NnJn-c+&CTBTa6BYBxqmUVzg zBhFHrM_trVmv+NykZ}YRN7y>CfhtR89Re22pJzZdGrv0<#NMUVwLzqfO(Jx96jMkS zMQvMF147=<7A|%(M+#`4{Pd!KXB4bHyogQLHZKLr3d>k00>(9%HlRvc+nQu{Q5ghZ#D)@qvcplj(IjSbP40B zroh@?h=hPKmU0Yo<19Bev-UO&f@8lTKS5fp3wH&_GRBCk zKbbF{Uh8ALb2J*aY2~62{5Uaqt`UW33IAGLun>K%TnHRJlZE3?b3oP-)7t*0G~%Ly65N`;!X9>H5{0oY8H0FQf+>wUjZg~#a1PB` z(-qQwMjFxLVmCLTTNraNf3MHHhI{@b>wsrVh?5lgWP*6@5O}|fN+}2eNGY3X`+B%+tUT8wYp zQL!Q0vkD@{7|2E^udUn8;28jl=ByGnxZN+tah0sbp5I}vOkE4}=F*-N_c%p@Ni`TjU%=)02mT0ocU@_p#QklJ-f2~x&Wu#PL zeCDaqM0PaRxE?g3QBSJ|5JaEe3$39R>wV#GQmGJ{=8LV8Iz&X97Mo3Q207fsvEi(%Y`!S>Wx%xpQ0WDf{X*7*PShOj(++v3#M0o}DyZcU|^E34*NpR|s$MILc z`x~gM3Q3$m*9G#+FQU7A1hU1d(w@a*V3R3ZgMZ~t&(y+B$c@~*wTu^i26}W5Rsx(` zTD4EGw5%-+GA%demeD0S&oSG9Sp3Jh`3wcO>K2gCZ6c=cV08E#jfTc!PTUndTNr&9 zUo%qlmN-!)TzA%BGg=#$!P8()zL%@=DJI|f3VP1Lf-#29(kh<(p-+Je(Ht;boJ2OB zBAphJB`N%ef+PlUTJlD=C*~Bo~^1FaI}{#WSU8Buv1vP zm9L1qAi^X>M^dnu!4&!}CkTKDEM_2pls1^uav>|SR4Pw;S%-JT9oNthyheo4&JMP= zw~%!@lnBB9-Cy{n-qFK{yU#xV+}fA_@|pXuU-~X8rBptdo`1xcKLGG~022`1daf?= z#h8sT97KaCidmLf1$=BcpNf z#Ih-j0bJa)rzL}cve&a+#@y(x-C@muuV#NSn0-728V>=`wQ9nc1i&H=o-)X(?T46@ z04)ZZ&CF0`CuS>)LC`GOI7huYCvUxpzVI*TCECn_7jU+mHrY{$K7ID>oAJ z&dmtpFS20?JA?dLNgU!)M&P4WyJHvg0uyVW3@bg{O75?RZQ%26nEXzp;iWicl0K zPQLvytZW>yn`NyKONsu@5ZP#oWSoOn63l2j3S&0n*(3rGF(?yY!lk)JEtQ;0pA7zB z%%?6AOd&0cQMzm-FC3H_R7sF@CbH5%ID$_yDL^pjZ|`C4>MhhsjIx_SafZC#MV&+_ zJ1Oc&U@j$krgLw+>Y8=9w7P~)zlXtK2~|}>%NivaNF^Q9%+Z`$G1P_{>jIjLErDQp z1rP#cjP*40YZZ-40C54cx(+oNgND0cWr1m}@Z~Ev@XTx1@ux3efMyJFoM1W`BQgdb zT*r?% zYNrN_(b|BC1KxkcSVS?iHG;kj?Djq~Zlli}#!i@Ti{lTPRAJ1$=QC(o<;#YM%Y4+5&H zBp4tj4s|PsqE5)Nn6C7(d(;WSUQdX&fCO*=$p@X`Bmmx?cFDU@u&dDHs(CVIZ*C>{ z!Py{f>h*#|#6=N{(khF?d|lT%U`s~(xB(8*)-g9K>xNBd7Cf#0T>_Fc#ruBpv-sS9 z^PfX0i71ZH6M)*e2I{T^Y=Qs?L7~$K2AkT16F*!a8RLyZGAHIv?N*NxQYu?onKsTs zM5t=%Fd0C%m}Mb<#>Fk`7IwlJ18K(|bF+Ex^=3zJwiH<3wI>ElixWewu& zpGH~QJW2&qN&9Chfl@-(8cZoLts%-9+1@yetd$TbyQy{P=vh6R5*(EmP|uyTyxTtk zAjZKM$M*IX>b%7A$}(OZ?ctAJeg$XOR!^rVy_#6y6rYKJ4M3dkC=l@Lo@#p^a%6mTaVRrKP z3Gu+Wd-;2wdJkTD;hWLQ%IbQr+gmyF-~;z$z245BZEb%8JDdB94QHZ+F+wS&Ok@OS z5%fet59~tE2OZozb1ZDkZRU-6J?{SkrjLIhuO;GX8J@^*s4TM~U20N8g=pADNqAC%kB4S;?OzLZ)LN-8Y4EI24}<5`!4YM4zDKGFbUJg|0t2YJ#50#J<6r;vv$!@K!}R*-bAfzk z2eGX2&Q65Cec~iechY@8zO_MW7W3`uVOY$-m+hXB@Wp~nMubcVOLnQuor@mZ?5{$> zC{uzEso`5}S3>|I0s|r8e1WLb%n+CFbWCWJf!Zv+&PEZ@A84)r34nWyG0XXMdh}*3 zkA3cUe&-D5{A@SNPARRI(xrY91DVzuB8oJ$9t;j|tYc?${}`>`>zkr1@;FWvK%(2} z14#@O3CK9Y%`?YPN5XX=-;2m_Fx#hnBSf_ga?*mF87A2-B8xyEs7cijube2XP6cEX z&CtP#_A|o#I$)T!e219@hyzuVGw`yue=lmgVR(^{Ac$UqS`@)4fGdT{8C1{$?L+%# zY3&eBKKVYp_Wak7=VMe=1zsu0$q1Sp>&|sBTH9SZsMsASFF1oz*1*ztDxO+kBbHm? zVyw9!UNggptagbk#vr8*NuO4XDZ&B_#+(Eg7yDqehB9{7^`@B~j5G7&52Im}W06VK ziVh5#3zG18N}pp#xv?9Y$$)WPPp$2J zCK)IeAVDyLW+Vx%?dwcyP*DTZ3RKzdC>qsBCk5&xLjTqdWD;XK=wdSHqUdJfvuqqY zg{0R9k77g#N4MKWEoZHES6HDySxIOCaBT`)0MvMXkG_s zCt5r*TjZ$g3s84&9OmEtQIrWgJ2wU@myK1kl*XwO)zu-@0@5TH0m z(d(dFk1Sy7PWl!Db*CWCfJDHQYT*?y2XuC1?CJR7C*P01{Pq7CoETP?S5Q?F(kNl`jJBMgL8Z#}UPz3fF1vdnL>(;di0dj_BMU934C3Wb=yla6{I&@!Ff5Sjz zI~$pgk!-(;q?4g23l#YjM~5&icULxA#8No!?OJm8v9_ zN+rpXZ4I_%DYh}f5CdVR8v-3xpp&N4D|7~+Lr8b0*9yH>64Ko(o%CvwtUwll4u(Js z2^ehIU~D`{mSn3Wxnz|}HK^u#)1A+>r~YG~bMEl&D-A9g4{xtk(yLeRz4Pup=j`u% zzwbA27x5hmXDeSqa%JHyVJ@kw86l*B&9y5A+5ou*3Fd6%?PNGoT|=BIA37@RnyH8~ zr%(XG5nf6Mb7BN57|7>tCR8fyJ?=7_Oi|@Rcg+D2SYC8LWuY2p5Zwe;TgXhp%W^b! z28frMNSZZdVSqf1A%us{fdf!U3@-}6wU-aNy$egLtL-e$>K5>|vD^zn5DCQb7)k1) zJ}&f!c=6gMKK;^J+`YbvH{Z05wM*BqzPf_i@(M;uW00kI>Eab!-QLD9PO+6F*vfOH z1=GyzY~!JNh`)05APxpTM5TA5U5*VtV-Q5hiyKL0`c-eluaw{7Is#VeOmD#M59pz^ zHq&Eb)Xp~E*jSs2&diCq+@9HDDYZfFPzy~USZ!aIQ-ak7dP@QV7S8ecg+*WYY@R3p zJH?+9?`(8_U1oV?tjW9QFVq0!UZ*|Yiy&}EcV|aM^@u#r$J*he;9&$KI7Um&yGOUj-zs&x1;naPu+VQf~lZx^Vz?QR!ei^X*8s@N_y zQR|HyRHij-u0esQ*lEmT;68Ua`!r0lhc3d-4X+L#y9;-I*AL*i-~A6zS|drvNY8x% z!Od?1t1ng20;REJN&Q8kjUjST1SlY|>5Lmwu(PHK0~V}cfAK=VS940*3I=SpQJ=Wa9F`zH&hZLJm$1&oB5YB(A_JLvBGzrF2k5Tl1I;ZDZ*LC^CJC@EW>=lP6tF1VmfN+i}`ASEup zbPk_9bq<1ataaL`MK$bqv)Zv%_0(0rL#T~~?!s%JJ!J^(0K7$XgPL#xe{ z_}W;Dv;867wA7l~VoHq-ag!Sd$G(p4#$;yt*4zeg_SJmflcS!5GwlQbS9v$n#nfl1 zavjHG&kubFZjlapP)0*FBIv0qN^^Ra@;Tmm1$5f&sf_>tAOJ~3K~(Q5yyg;AW#%{QBgWXIfRsgy zF-HubsSJ2Vd=mLQZ7)+=&Wnj7bGdZ+rmvKwJVah#Z&>LQhtin&x-;8YP3S6(NAb9a9+RI@8%Y(b7F;tHT)vjMvOE%>yA zrKJ@#+a28Z7am6Y;1ZZ$%omk`P87^2gWVdzZjV8+bYNvcRd!k%Rz1C#Du$rYUR5cw zG(*pYcC0i@qI;BNKhdwT=TR!7`&`>|U;@xsX$pN4+y z{-E|BpiNU-d9%v0M;PZvwNh_kL`N;K+FD)?mX?-75b-QaXlHX1xiQ#GG6<;=1R+At zN17%u*1{8>84m}t-fa3woRB9aHjIIErqAG5;NpRa&g)!~BA@EIU#-6vi#YhQ2TcrD z#<8y03zrP}526o*)^N4sKNN@gqh z1ok@fZ1hk9qIy&haUCb8*Rp1cO&CP2dZUqWF5rhDqIw!K(mpWvR11R1 z^-P2^3FZn8BUnQS;};fMBibq(qhWS!^2C7S7L8igW-r z9>ULtIJ|xcQsx*8dRSUJfMa*x4JJ6OG%&pca-73#j6s>I*i-WxgVS))T*q3Zl$O>x zr8-{QnK*O{6l}7B8jK<%X0E+8RJbA=HxEOufm8y6iKy8!a=NdY2MPpOD+EFT)B{k! zU3XOT-P^#KQ|*s(SAa-B1OmKi5M&C$6^5-#SFp9S3D5Iz@TNoX!XRWo1^~}wu?Els z(0g0G^}NY*KZ@$hjavPFDdo2fyWIy!o5t$e!4)fl6`4z}l@cL$4Dx>L3e@$3u*o5tJ`>84JFWNOJEtY>NG+wG(RE z2~0-5+t2|(kL4uFpy2=KwP-@S#@>``dWD&hS>RJ;ne$r{g_g$@!{Dl0Dus*%cF&C2 zHHV_P9F*x5ccOpw5?~E-mLu<9h1@s`e)Bz5aVVAdlL5`dEtwU#q+yep zime8#lR*@#?E76%Th;n=R?UK{VZNQ%A(eWalEPe?Zn8?y>0q^z&`?HPm$oxy%z<-) zp8QUx&>fW;pVETH5f?bDu?WQ^{EeoBtHh`<9YxhOX{lf?okX(nGNNKQTyM8=^w^tl z>-w!oE(~CYIgrTW`m&W6mKg>M16FB}XyW}vPTg-AfoaDDEjtxhKov=}7UURiW#E`$ zuv$-&T&ISDw$!m;V!uyQW^1# z8t3wnDPWv~*F5lu;lQmop`Ub-B?=&FAy&^;bS(Vh88@!+dbwYGxL(UlUwVLozrp=^hDM| zH2n&u(W88d?p$m#e(AtGr=ziZGvcPL*=Hq;p;f!>=rM8pjp|Jtd;7yU@##N-8uZX^bA;vOTgUe% zkeVq>HeI*mZfu%*B4aMut$H6zD6RBJD^F(G87Zx+PKgQ|p0dbTTMWr}&ZE}501^0* zG6xp|&3YR*tsMpW1gV@?*+lp|IZ2igNV_fqaOb{s6DMP#JTd8jjV+8MQN)$I!eE3b z)c0mWhb~`=bur?sEWU+s)D@>m023Jt!(B*^Wm5&Gygp3@(t}1B>5h7+WG5l4j;LA+7tug3&kG* z!26qrNNa^&cc-b9^gZFj_x%C)JSAmLpB-%Lhw7dGySug7VvPM*7}Zw2TAgP^czSCK zU)r*GYqO4TTWRBf@8RQHT|Bzohhhxe6Hr=B9#G_KKyVmfg>aqCA{feAjHGgq3ga87wXwOW!sKjW%QJmAUa&iD8RGeRIxJ&hVvVVxYFu0cH@4U?#Bo@eIsDp!jeHM1%Pwn}xpsEK0<+*s%@h&~KrBa<8K;fq1lA~Ms96-u z>SX31JX$pQFJ>u=oO7pLe@!`?vBve=NkM=1CS3tV9$2j0KvXGktsP5mflbczOJ`G- zz;{3NUOZZFV7zq=Qn`7W>R*Mu@I0cUZ*#A;GB2DsaaPb)0iouFbFcGCnx+yL1J{DV zC5o=Wsr_}uP6Hf;S%`^^O_et@?Jrl@c$XZta*m{nqQe-2=ZQ(`=9H^+MJ6x?)AkDG zzw4qHRHD_XESt(xt#~T61%;b)4rWrQt#j~TV2rNVYX_FranEgUp5(feMXY!l(4HH* zo3dM+hCZi!UyR_;9!FY_kkzAVCIiMQ=rxT(SVr*E-2Fe7(2T)=Ad2}86%Qqi-q6j} zlm;*2G`Q668d9^2t7^_Oqh%>xM@cuf0CEHDB*0Dr8*tEa1h(1$+M(-qID=^gP`N^u zr4XJ6BGOl{UcN$%#jkhM_}GQh}_xJwEovtRSXxHq`Z3;#D`%$G*P!A~o#?~KC8 z=bi_69-ba2IME;CU@gL>Tp=~aIZw}9V!v8pq6KuOVYNm#%_~jWJPZ{H4v~`cG@H(R zc)bN+dQQ{RP-6++d}BHr5)34$(x;g@BT+|y(6AcTrUoWfV7~vFNeH6jxrpsl8(5qn z#;)D)Vnd9NwbH{Rjx}S9t0cvE&_i=+d4kShcSzKH!I^Q z6mu5kpKY(ElOLDX58=&^d^Q+S}k!uP$(8NtRegi6uf*+i?+g6H|@ zEU)42yYI)rm4nqLWeUo*iUO$6cXJeNz?6GchI6MQ^SE;|_XHH@$il#}$L8VK1@LQL zbBKiu{h2O$dS=&O?YM_2=^jwX0zyjE`eXQ64yH7`G>0gxn+SlH$w|S%#G>DIP^KcT zRM+Rl0nAnmtkl46YJu7j{6>VR-GJ{0ka>pDXsGfe8*~RfjW>8bE7KUjue>8{JSx-l zA6Tt_)DOZAW1LBAkpbwBV=xdr-=BFf(0N{Tps&8GA?_m0$5JgskT?-mq7bJ>BaqL& z>_P(oJm7AsQAA=`^TxfO%xB;Y0n?RCFhXwh%s$x5a>P=DV3}3+%llm%>Y)K^thIcK zWp-nVP2l_8EKR={$K%_hT8)Q|22LG+0_(RPouT7m0WkpBJE;&0Iy$BexKxu(U0WyQ z41^m7k0!3H4$n4FK>+28YSXT?cbLogu(QJEU8DuOc=kj9l6D=@a163e@1YV6UN4{% zZ9!!uK(UE=Ne0xS#j~3QL0gOf&-2kdW8KT{6 zBg-?Sd5Wb2H=((H5NdS^d1-$AEBEQDXL7uh`Rfj9kdG3_os z^YcnWRZl>9ZU&k~0miLHMY!>@9DXj*-t5AUQ%K(fFFK*J`(Z{;ieNEcvcx7uCzZhE z0gTk3+8CxA!$;wu?F=vl4}Fnk$pBxW_4#yk0l@$GPSO70csTe+W5PRq-xom;a$^kT zd1hpmUxT&1d*A)8@XU)Rj}EuDT%g-)fP%_X6^I9O#B512PEUJW@%;I4Z`9D-iwSl?^;V4Oh{6fDv=gdiZb(n`qs3LP_9*(5F zik#h@=?>ca_X3=_vwC9GfXNhi%`1w>#A2mZU)>IN_d$-;1aJ%WonG zLu_8VitX+;>YWbi%tM~Ltefc@jn76b`n&d>XJE9D={1nK#b5E_ON>QTaX35n~O}bKr9iIFObwD=c_m6UNDnM zw3pZ4qTn;TcZXknt)10d**R#rDoO<8p${4Qh#L_+sZj3?5sp)Yqqt(WF>Q)@kcN<| za!aowZ_DEhGiQIaF@j1m7~?wSJj>MP#Y;Pku`a&C>&bEd}$r0UBe`8>@sVzVogPJF1US}vcYMuaRjKdIgYVdjhJ^w%m@>oWkq&0Sf zh;IDM=80i9yH5naF&+-T-w*sK2y5*8lb^wrvoGSv-S^Hu{n^Fv+4K#$7af_bMKl;! z473tkXAQr4LeZnV8swIpP!A64^0{c^n2$wZ)3k2xu>9&KF6I!WvX5~F#tBRi zY^q((38v-)cGirO7BJud;Fde?#^JX;jLT1b23!b8nZqcFrI6$5);ajMyagx*^01`` z+2kOVOSIAselo_*5(+2o>h%iV#2q0-r=kYg0jRqKFp|CWGb-{hb~> zKY+}0C;(BTpx_tkbqdM6;aw^DU0-pCLoQM8kI>%iffpj^6800)QMtKt3A(Ic-Pu$*RehXe-XJtBoO#khBI%^*yiyzJN{3dJd?>?)t#{l3y!WLufeI)2D ztij1&310Qj)|m!bLROHD7Q$Yo&)`tK`gzcG3qa*GVxcU(l8eEYhocoHumGin6Uh9& zW~>D<2s2dBv3h-n2^gR)k)Vc^c)vB~b^tHl&|(7sVHlnEx%aHfWrGXZ7L?%gANvpv z-*GpD@Me_zi#mh()WP|U+I^hq;!|Fqx$hxAu!Oi(M}06xy*ETKN-E760bq*5%OwEi z9H%V+r4c7%42FHQTOBB+u(h?>Bcd0Nj|Ucij_WhMjmrT3`R*A=aNbJ!hgR8RYFh>u zfB6*aJhG0)T`dF$qgNye5U?v$Z2FYNN};e9WH;gsDBU55fyg7M{u^w@S+s4^8e8*b zF+goCa&15s1_9%*N3@!4rs#ck6LKdPRvHc9B7pG??`#0Tuhll<{@~9P%zb_sw#ZsK z@zLMM(R<&H`yY8f2$Kzh(cWFe44gK%1J?#4*EvjmGmAtK0}beSDhH&{Ta1^RRs8fW z!kN*?k_5u{Av{;GlF<yxZi~zM(~0V#%M6ccQfYf+t^)B8O6ACheTYstAp@{U z!>@`ihnH^3g~o;zmMNxyvnI2f!u6!+BxcMu&19ov#pdtA&YS`S# z;W3Vz?|VD+>N3=IAgr=*R5?uZMF+c0WM}3w)8}zf5n;2(dquZ=J9q54PG_1unFl_y z13vmIZG>@(l}j7&lH3{Da)z`XIr)H8g)@i3mF=r;;I1`@F_VuIEx33F5?__;>CwPE z5JbNO-~p@5d-LbxaO1f%YlN938|H?ygpJ2_tl zv_+Lo40y!f*f+?d9Zu9CfWkR%v`WLqiL;QuYF{)f4aR3j7@r$r@W&g_7cym~$pHKj zfWLn{OD?@3#pcr+m+gHb_%)W&_b|o}1VP}($q0Y^EB^vvy@@*>c(B^g=Vu?dPM0(N zcR$MuYUdQr;PVOt*QuHs^jmcMU7TSF#^WvYH#eYUiexy%*4Y;^8V-=9W2B=Y!Z1SA zXd;&qI!$5n41TNQHp|fvRtN|`K&@6sRBPbaJKlx!FFXe(1dhDr0R&MEORWytnMSX> zgLbqq5Ie1b=JxmaFNz&rh?4?U9{3vLRH1nO~nQ)x(r*~6`VO-U)zZnSsAxn(Ui-G z1#6*|u5>WIFQBG$M`hj(iumf%c|a;tQLR}e#AFql&6tU3)D-Nw^7Cbqs4-?L9vcpwafPqg% zV9cM3@?PRA{+CS-nC@;IU&A7~I7WvKydioUB7oP$a!#-w2v^4mZfi9r*99yDBXA3? zhl;DQLU5g@-t`p zjo;1U+}^?{&d}l^@+?NXxr7Jb z{w~}#ydUkQC9E7+L#aJmqdXn ztkD**mXs(mY)}*qTiNGm*B-$W0%V*iXL&;h1--g*lrD=J0~i>z)-c8v^BUJxl`aIJ zNes1}Vs>yMXsw{K79K;sg_o&@K+^WONB~~tNpw=H31&LhjfYo50fKD^Ok|Ls! zRQVRZir4WpP60gjwp!y!tM$Kx#oxBp{se%ARhE)Zb?L&Bee&)z0kxhGj6 zN#YNrS^BWl>N~?IXtvrNLK5S1zw@g&@u@$?z2EX3IQGCh(Ox=$<(m#8h-v`mGjK=s z8mCn0zBXWSQrU}BL7P=v&JlR0g!N%#Oc`$CpT{~#)&v5UT9RA zatcv&4~0fYYg@J5wqUk6QD&*fal?J#xdcU7&~7V(Ou^@3cipkeZmtx2y1}W1r6M3r zYe=Q4Ftw6m?O5A|TLC#McvJYkk)5P{+Jcy%q=bD7 zO6k3Jma2ZeqBA|mVRLgzhB?tN_MYi1{5_}tP7_ZrV2|%7LKgVAa`Z4dTRkjY-FD6# zT0<)Z!8scB20Sielji3^^fCrJ_-bEI$0G&cr3a$=--9t91@J!r_!ety%_f?qUl`EU zrw8cX-^79MzZoDrE;D^=?CCYTheD_5j#c*jP?%i1VfdHTZ4mtRunf(z`f5&kq`;Q_ z8~%O}5%>Wg@nm|Sdbbkj7|>_pBx}Q-h;ZF!6pXIEy_w* z^a=vNKP1i{dpaITWH+8XvS+b5E|aVGiQs2P-JW4sdatUfb*-fE7{loD%Xsp4ehr`c zz^|aWx`yWR0UWyfUaZ}ICyds};xY2^2ue$&qapg6*RXQ%FycWEhmPI_`Nb1hSzX8a zfrF4zqCe>3rE90Ky>$)!txaUZ1jcA&GDB;rgE$^TNr}u_G-?e5!UqFE3CFh3z7MUG z02`+<^32gcjn>Eu8gG^+Fj_+h587xLrIAUAxZlU9-$i$G3ycxG zFhp}{34S3wGg{%|m2>!Ltk8%WSbED_ap2AOBB(cT$GvYyt=R_S0-o=KXl~=9Sxn#e zeZ1@Yegry8pj3`4XV17`HP1t*U5C^=h|l+-MFhKg1c=&TtySmURX|~^=sd(!Lg55< za*@o85tt?9xvFL@xm1b8===L25W)i4 z@gZG0c@kFIyHIa#XLk!Y~j@O4NLTc;g~ee;Zq;Prwvfk33Dmg{ZRX zjWM|3J9YI7fBYe5Y-ntukeBen0QE)#GS4v{55a|SFp*%$(iF9*j*JrId1ib4uJwH% zwPrKzZ}fH!tRK{Vy%xmrI6H7)ZRN^^^J$*TEX%T3`2P6j+ip8rZ!|;37`t-$()i-( z(_5X@)fLb8TGm*2zDHS-8b*v~c}f65W*J(I76cdA-r0oAb7bQfS(d{014LmBqj>0C zlDUV@a0h3OKMv&|QWpRK7{*CNK~zJ8KP3ib0Y8lJ(D(c>8Y`&6LH;oANF*(>58?dL?pEwYFfKxflWqrOH`L zVR=(-wnVT-R|ThJrj_mlS~_v3syZrXt5ap+)LICl213<>DTJ?r3talb0kw8# zcHXgIMl%m%E$T+g|1Fx`Si83mAYOryXy*#sUTBV)CgND^`Oq?b919?B)RDgF2nLr< zqtjW&FdZU}$6Dudzz8StHMpL*a@hh<06z7$sQx}{%(nvgy8ylg7J-co0a)6R8Twg{ ze|O>(zWMMv-n!D6N$w1V0J#m9Zd3u_Z4X5Z2bqzAQ5J6$O~4c`>ja`{?VU0f-7G`y zgwLM5oLYG~wQ$J}j>h0j8?FL)0>E#B=wn{1Iec>C+E;{`_==jJr{clJeO~x4k*7~< zV}7jO={zj+JoG$o1pvY@Vo4HHz21Z(i9FB1I71MIZmeb` z-J!LXL})O^F4$aceBt87b%2iCbmy_!u-8K#r@irbv_@krYi(9)&4$iL<8gECz*48( zSX*6PYkHo~ z!}#p4{VOE#2-aGpX^K{R2}jK?u#{{lQUz_LHUrEZSAz6xrsfq6a{ccEnkehXV|uBDihsx;5a<$2mT z)K31MOI6T~C!Z_5)fnx@gGzgy(s@9e$vlWL=L)U0gVKV+sX5El+_4?u!E#%4Vzc5_ z0)hMxhH*6OO=zt#PRB@-F`oV8qqy}=ZvmA_p9}0`=8T1BnQB-Z?LGf_`zpF5dWEmS zD|~Lg=+GW4wt2e(1DC~f`tmvWL4f7ZhafRBsX7U3KZLLG^>i|p03HSK=mSCYJ^=p^ zz&imntlN0)2Tq)(51u@SAHMrGJh--sdsml01k?lvF(MZ#c4O)dW8`*H37DRIZRVk` ziTOtG4u(jn?;K8y!Rh`Gr+NdN-09c?Oy1Xf&E=H5>2+Llo4YRDf=82YH?%+unrL8Y+pwd=Kr;vSGyH zbUZ$t=lP4{UjI&0t1pW-U$&e_je3JOYYk*F1LqumIzT5MA&s|?Y`z2)v|#-jMEd|( zy$!}aXJ>-hnC9y5MKN++5mihu!5MPtpaugo6W~@3{ZVur&K%ShMafaGwYKmCtJrd- zV{|!Bax)rM2}DZ@w-bO)Iy5#V^ek^Utqd~1Qd>;-T zy=$g3sS3m#U4P!3wRBlnNOfLNXkJTEvRE8dK0i}hWRo)^9L6kbcagnLHW&c?F6xa2 ztT9N(v8mVUFZ`>=9!>Cdw2o)VpFR-PKMP|X1n>(0-VGNi$5N&7ffHxwx4(22cPzE= z{de37Wh@K>Sj28v#u@QREN^An`$jAVkWbcVtE9E!SDrmZ|N8hzOfmBfDjQ>bses5% z0{G1FEV=sUeXd@Uo58axlK}qxeO~yRSI?e(6BlAlOX(?@e{+^3mO!3`Wki%`qj4vf z`9N@Sv8&{HYpr&O2rUEDC4wK@L9J->cC?flE=sfpU`vpff@ln)tFX2XqJd$gp2I#4>P_sn|z@Om4S-g9&y39bgzWDT0Yn*#`E_GJwAc|5g*K760ik31; zvvjGme&DubFf`z-ZHR)aS1)PL^F$QYV6-M!%Le@(d452QGYWzb%S$UZ3L`@VW|hf_ zd*lm`dBQVHV^eGSSl|c$L@N0bS-P89{Dpj!Ji>*zvm0jz+e@8J7=(dT3VEL6;OY{R zB*8dI(C?i^t}X0l2<_KE?R8Ld6|CNd;Q=&QHA|sF&R)((Dvvcx0?>5f;9Np;rJje1 zf}sjq8cw{>ayT!xELYA1wA$!Pl87rLJ_7>lVtVVs-5;LEDs75{g)$Z%3GR^>e z=54jccUrCA2jFJ_ybplT)^e>azO>b&6I(sEF*5?=L?+Y4bL^M7iPnVkaWt^1FZ-&O zcY?L>hCp;SH;_Y#@rwf_S7Qu5wm~q~(+&^UQ_oHV0H*+a8o=j4^asb&c<|M9o^D8S zzdxw)VgvuWU-yM|#*A$>>Ww2>>-BgvIJR=jtw(CLT9{_(^2Yh|0b`6c8*SUD)tAS~ zxC0>4#*oyq4N$G!?u=C~b7D-(G`Vby9crT^-}ASL=r@SbM?SH6Ih#K2kKFT)gMy1A zTC4Yap8w{<$Bw>z)b0Di_u9kpu$kqlFxrqc23eXS%Tjg{3BQhB1fDk2nWMBhE4lOM}2slBNQBh zdsX&a6xLCTYFKM8LBK#;3uZNpRuF|4G*daUJVTaCa2SNXk3e`Z#2{dybcXS$k9g-Y z2HkCFt)R7r)(YcfjK=y+_#40U0o-)-&Y5Dc*VWEw?>Wr%qBC}7A}y$BM6V7mqxpFe z?QzWOP_fJJlieMB@z;MDPkioAaQWH=2=1w5IDYi5qi_0wU;6Zi)z|wy`aoE#!`g?7 zPUoG)6BM7h0xT$asD;kE51?^(8~#%8x^*|%wG?N5?l^30;H?E%`O7zB`F-oJOKW4b zg&Ig`rO|tG3)$ra@rSozjkVyro`ud8oy&&-e1bTC{*4~;|G5{N{RP|x(9yNSet*yp zlvaQ>mVm(X0}R7!iZ-`>=2ZmSI$b=LZP<2weEc6@#{bwFVjl z%RPYm(8R!W4xPo&)}q#EKr_!Jfm%Qq1p$jNj1c%f7%_NJ9fRQz*LF5=@zPlghJ9FT z;0b|1aQL2wn(qVFVrgj=j2u*Uc>N$a^N}V)r{iIS@puF-1hiBI#oMA@Z{YAvH>25T z0gRBQF$SYPE?>HUYgaC#+uepS#yMMC4KRk=-}65FS3m!c5jC2N4P&vGp|;58Xs^wk zUd8O?dOyp488>TLyysmH%xHwcr$f$H_NSoc^C&v0))gT0d}; zIrPIv>aXeqlN*bXcd;eJ!&?-x!GvmF4JrYL@g$m7bh_fqlluYhI+jL28O^70LGAI z8I+W$HyWtbny|!?#4)T^@I4OhIiZ}>#&CGTLoJLT7<2j!A+Xim#LE}YL28Ywo0pMg zNtLJLm|_k#Tk5R1*aW4pwz7`lXn^5(=w?sVT!sMzgh2>SPJFwzdJyea2g{umblOX3 zH`*WzqFJNbD)Aolwf^}5#T;>q179RY|KY<6n^9R6$*b_#>RCiKBQG3B; ziz4P}ZZ5Q!(>LbkG7JChItA+0+^E(Vs4spI7asd0KK5sSh?mYjk37q>@nHCrG3I-o zm1*xA@jiJVsI3F`y#Rh5z+0y}ox-M~V9H2K;;s<&hgPuk@G64k@Kr@Tro&eFc z<7vG8wRj%H{+9iTd;h`vf9QBF<*!97xHYW#KaghWw^(B%kHj)lHVAp( zt*;zpK@fy6fVP?wBhPJm44eyBcsk8O2r7{0jycCUN3GUCz1~0=hLCv)#U1Mjzeu8}dFmp2gPy{HF)P+DBpSUj^{r z0eExq&m7iKgW-`?XEXHAUZ%nCT!DXo1I-6lP&?K@bhy5!@0sjLSXCB$aI+&}asyr@ z4OnH7UQHl}IpWhpjGpVk>`15=Q?~;%*rLdkr}ipleQF$3-slWyaV6B6f@s9cwk)YqhA}34-8=(Pm8>-4?!I*H+huqk)C7W@Y7ov4H1! z9zaCKXhw|rS}9?TQBkc%z8~fr8&|UJ-gbWW@;P(n<(D$XSOl^{<}#64mMNt&V~k}) zkspPvJWZw1MlxcVv6h$+%J;l<)bG_r5E^52rqawZAviJ0Va!FNO>T_dAYvgGTo|qU z>jw|Vx2)Zq+;i+L@xcQ(wLIS!!_naKa6G)AwEDSRW)JoHT>$`5SVN=JM)UA3IQ+=B zv zh)+HFNt}4$X=GWx+1uRwJq!H3XJtCvzcVU$tqE%%0q{crz864%2~$8|5?EX1O9T%# z(f+12H12F6SP!d^KUZvWy!6vwfE^nE&?|rS)+d_xb>5{ijo~w0BxihKiIedmsa1_Y zZ%VUd$@l!*n~fH-JX=yq84IggjaGB*=9|4NOY4Kp?Zg61Y4viNCV27e$?U|*XSb!w zw*lN#+^ime(?#oFIz5JPd_fH$18^3=3V;m&*HFgI5*4wnFaRWoED=peC!CpQ;?csJ z@BjJnpJg2Mef5?0|1s(gj^=qLlv40K54BbswL`by&?Dc5_HDPj{lo6o++5ES?Ugh& zi!rUe;j%p;wgm+S3-WcU;~I_8r3>gi^*Q(|!+3iepLz0A_~OZ@p|wc|J3F6OQvU3- zDtlu8F4(=^R&R2v)VBfnW&nQ;jtR>F_$Ygh5@stFTl3)E9HRa30W|JzAv_qtNP`#u z_UB=`3b4@BVtwMYY`S{-qA81uDejHW19%$1p8|LajP(mv-Pi5fFE;xN|NHmPrBUQ9Lqn689Km=EUje?UznHXNYqHtta>b=bHJR zdAi^Iisq&Ko1ggm)LQENppx=`wS@cfEE51Agn$>-5Z-bG>yP|J1S_kvL;zY)1m;?o z`8=eVLNgZ)uvb?y9}m4(%I%)*(o8|LkV%ZQpZF-SaRm+EgJ*!vtxf#iqrZ*r&UQNJ zcAwNr{RRo~E6=23y?>RzLbnq{2jMh2KL_AVQ%$B<6drG;_z=L?Jhb0`5Y2nqxbn-V zk)KQK)QPPUK>#FhbC<{92K*l<#x{7pCZF2cwBIQ2@qV${Utc%tgFpArDNB7NwZC5D z`t6_jXWSU`69)Dt8E0=d#sr@55JnLgafn6>t#A4!c!v(dGCtF>7`xjj6qYuEnKvnosXuZY*T-rim!lVu^Sy$`@$#h+6EmH`|E@O{O{HB3yoMRD3+2Jnku z?AO2Ep|Sm9v%kKf*DwF%{}QEH`n{I$x1un5V7ar>sMqSmiiip@4mxxQdT9w_eH}r& z4KN1o1z?=d%u43xCA2`>@@jFAMG@)MO*m(x18b4=``CEtOGvk_AwGK&jVOdC1gumT zZg)YJkfbrLZeN8KP$$oxJaPKj=RR(<{wL3>Y-|5YxN+-kwFbAwc(C>m01rf4u=XuQ z5xN7w!vLCa!~VZ^JWEdQzia!&W`BKytcPl?duz?k&mO()uJ6A6mfM$>TOARGA^L+p zw9&Y@c?GdnKp4Vnw{g>(??ZES4eibf4j#E3epE-)XgVkBAegyf=7ZQ4wmN%om#-Ha zV3a~S9H4vp6t9 z@F0p7CQc;JDDJ`^*!wsEC< z>GH+FGsnO1pN>EN$zOgR*q;sTul-`PzxLNxZPD?<2-e;L;4J{&1~=rri-=m>^ZX!+ zI$;#CG>$cK21bmOlq7u5^E}TJLNEZ#Xk8?r0$d0jJ#r^*Ie0TTI9jbXa+yO)1*LLW zYtd-7KwyZX2(?-bFcwNG)WRCVFoY*OfD!y4fM1K!+(_9Q^~aa5Uf#TP_0s1qpF8)5 zC!YA^3pw%AC#;tHXL5h-7n}XHzrMDLPaW1a;T#Wc0noJ8E)mfxoVH>K&iUXNW2{At z`$QBn&J<%TBF1Wd5Qs2}_<_}RX0)+co>>BL)7l}w-Rfw-iZaiq)o9svqphrkIrZ`> zF^4Gi&MRweMnKo|!xvfy*TwMi)z8N1;Kb_M;q#x|xMcRv!2a4V zHv4OT?Joz*0Y0n^VQm1SvOq8ZhvEF<3=!1{bc8W>UMYD6M5E&0w?Nb nwZHb){@P#rYk%#p*SP+FFIp|I`Q0)Y00000NkvXXu0mjfA Date: Sat, 7 Jan 2023 04:38:26 -0500 Subject: [PATCH 073/199] 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 zcmV(%K;plWn+w#x43Hy#LlJsZSaechcOYN!v@`4G$f{%eL^xtUBobIy3k0yQ_kaJd@A|L* z`md1hcwbMht@KiE{yhEZaqvyMfBuZ~cetPP_xaP;?|0#^KfgwQ`u#TYHSsn5{Gyeg zui=lM|32Z5uVL_A{)ZQ8>-QJ)ufOQe&)3-g_CmjJl)R$IH~aU6`tJ*+`16bK`vT+V zw(inf{l{CF@6XrY=l?7N|6Ts|t=Ropoc-!mqQlEeMpugxsV) zNq<}9^5@ro`SS~F`j@+Mmi`~!`DuTe$zOh+-_(Du=x?ik7XRDrJNL|8jHj^F<@cuk zxRvuAkK=|P-@08~TDdF#WnQA5e>(p(ta@Pz`(F3tpA~w4i0sN2p0L9RUwF^g6&9EH z#P^DoZ(>|*ls{~##vT{D7uY}k316(Kr=1#mxz4sfDgJpa;k<3X*LB{l@y?D1Vh7l@ka8&Dt})gSSjff(4K6*V zCr)-C}DPsV=g)3BkDQ+{&J zCD+_?&!eRBlw3-wMNk|y)#s|YmRf78y^faJ({d|+t+v*B8$I>}Xy#se?XCAd2Jbm| z<-xND4;W*{ndUR|HOs8C%|6G%eAct_Dyy!x`Wid#w84*EcHM3FJ>KwQO7HXLx4iXj zZ-2*!seS4Co-cppt6%&2H~!qU@awjJ|K~s0weY)Z@$8fjtbgtrKdtqTmk7d1cF)+b zcs+rC9j|Ty1|8kAyM^b8?wose_lQ^I$VF!NYzKFY9emyp^B4ZuyDR?es5|%nwQpB_ z{<3fJ|8?gsx9&gOxqsfbf3<7dFtqNUu&EcisCUBqc+7~6e~DH9{Ppsi*m%`3H#d|< zUfq(*g}wUA)81#-mdj*+smEM7z44y?>SfOrzL6iF_X4c3d^2%* z>C5~tJ286z#`c%TWI1=8AJDbR7g?!Q*w0ydxQuhTjl9>aLR`CFOe^&gzPwr0@YYy& zX3ZX6f5&%+xXUVg#95mrlw-<*KlicTF$kuaRZ9z3pR6bLnP%!evbtrz>&?Am!DGLF zJj|!mz3!6v(n=iP3mf@DtlVZ{nXri+*!wAi-s1|b9sd-i69Ca?H_&k1uL?_HiR zKGkT{zI>%!>cbY-{POIX_fvL{`H87%0&BkS{qZKL?R&6h`^G+MC5WWC=6>dAJ8`oY zi~ikfqPZRdao4;6N<hxl?GdDXY1 zHb#2EgMhj8nm>#Y6L4eSF)^t-kG)onqtDX5IPcoayu##SpN_Scys-So=ZBSlLmoJ5 zeLD^))yvmVa@P9s|9xTus5%ly~d~7 zyECg&S`TH;!OGW~%FT@7|HwP4dVuZ9p!KA`wx$(37P5!&jN|i4Yuf!WKEV=RT2({c3 zgp}Rn0Y7MA4PJEh{?7HKzTVQKi`g*C`IOQo5w1ZixQUl5`req>`UwH4u-d@Ij>Yag z8_u}LuCPvE5Ss;Pu`8CphZfzD)WMuwHOfq6{8}ITW{-NP*B3Gz7Q@hgcwEsDI;Nin z-=&G|4>qVZmV4f~Bsuk?1Vn^)*7kS642}CRfuv|RpBX&H%Ax;GrfT!%@ z$Gf!z+*F`@@p3E^P&=@?_Ciyx%tL@uUiIzYVIkiL0N;D3m+e|lZh%)U^D8`Vu@T%o zkpT#J+#xI88IETfn#gthw>m4SdN^%9rxh_$_-G z-$p=;ych`2UE|X?j#9COQDL3~&*cB=!_U9H_u?r}pXmjFz_b2;Tw_%LIm|u5oEP+f zgz{iG5TG<&HJ}ye08*yS$X-HwCy|%%6L21P?OD^!e)}*?&gX6AeX%xt53|7N zS>0M=f-Mrfc4g-ttPn#@M8`A(--Ig!?uKy6Zn9i~n>hHfU~9yJ1aoioB}%fD_xqJO z3ip8-lm!rSS;WGB{O-*>?qZtkoA3wsdGc|)H2tr5cqZy+GnufLHMJZkd%;fKV}w7-Y=1X$(58io67 z4>sX7gT0Ft6r%3Q5+Tjkvd;Rn4_17QZ&mTa~)?<5e>&yy_2gI5C#0~XOnDc%<0KU$5uMeCD z3oc-jO(>OrD+;)1hhebDD4b^r@_+#7+e;LY!s^8SzCfzcSVH{!g;GR<@^igu;W7XX zZ_+BRm@-ji5_`eSFHK&h^EZpu2{^D6$iyJ#LV@t5=Y0d0(!;`z-giMrpP+l=1gs=4 zHCdbb5-v70I)Qd2512;B!auxrgTnQ2f!vUq<469|wd2ubP-XGkRhZDGO)}Ry! z^dlLK;0U1(LwlglH^G3#ZqgJ&NP|vf9Ago^ZZ1v91SKpl`nY4QU^yHtbpB#j4yrC# z^eg~>TuImFrr%q|n{R!)5EnFF9*5h5SbOIPU>1WAQT=$_0s;_c`56R$-T=h9(rT6|0B5M*TC(WYFpfP#-okas0p1Ep;9aDz+#w3Z;7aH^!3YG0SiHbAa0ncK z-3bygcZW{!&IlJI32Yb7;yz>>A6U-oPQvC5N8+Yf#I{!21g(_@F$PR!{k{vF1U+Rg zY_WVE}ufXz0 z08Y^3bt?D7syrA&X#fs;F=1_2M3QooL_@U1{yOxNz&L=c$~T9nu~*NNxVLGjy}vc@ z@v(hjBZNOLeVxT-omqdFQAb-o*cp=U!685?76VF97ZM|ArU_;@P9_O(AIgG%PEd>x z&a^Ke;*l;^8OGRos9e`th{1cqzxUl>)JNiF_~*t6W;pQ-`4b^r6~4O0b5*RL(7k*^ z2q4jcog(TXctY;@%m;o?v7m!u2nV#~m$R_--Vuokh`|Wr1+Yxswl3Is;EE!80f3na z4qFl|fyKaU0kH*KgGw9Cr~(InhaXIvrx;uE1<$&F!p#5h6?gbx_4gqL(8Cf2L212w z!@Fz%_`cu}00&{L4m_0YC}^sXd0vDQLRi_stwaL98~!&^1?%) z*#wKhsu9GFi9jSia1t_v_s%Xod~TS;ZWt!Wz43cQQ}|dpK?_v4V7py^jWE#=sX|P+ zYQ#261|@@C#NEdd;}d2D5p{bS)GVJbXqx3^9Sd_m>-nkt}5G zG#hXOF+2AZWYFksJQ3!z3$B9!V}$cU0TeEcKX;P!z?Bo9RDSiGbGHc?0hTQ*KOl+{BC1T?*acH8VaQlU|L8(A|LK$wnC(&ML1ijh0 zAY*IrBUbl@P!~uhlpIeSEk6}t-3e8Oqvcdz9-zVzR?);<0}Kl)NyOvgI#?F(Yr&HB za8%m7bAc$PS9XpiPE73BG9d~dYOrDG2owrZhD;+4=3smtAPe4qPlV>a39$trfUbfC zcpapitz+}i83dK5bHNaCsEt1)m_7K*v-8>z!R7f z*4<$9%>xj{bbhgJJd0KO^?!IVfe___rs`NRMtk%flz>Hlo#jF#dY#Z}sK=+i;h2(yC?`r6OF=Yk z5V#T`QoQ+pEn&y9kC%?*+J9&zY;$tY=BDNXq(GDVi7YiMUw~Tv2A@|_F~ON?j3@yN zF5dBzdN)Iz-12J;Soxrw0~re0O*br$CFJJ%dK7*k7bpc0gh-Bd=-><}joCw*z7|#2 zVi&3%>t1Ggi2XPi0f{*N)s-%G+iLXA9WdV1t%G!Zz$CD$faK0UCcKn)~K%WuwvFA9G=Iw=lCUIw|+-!M3m;1v3 z!e!=};B9IO?A^FItp0%)qjhmd#0zm|KKKP#QEur35xu?@E~8)DhbzGq3DiPjMv-RThqv$PbtmDO zy`bW!h>U!Iyw3n1!(<3z_%LKNWEhTr62p8~Vy)o%_%<9aP6*6Rz=kwwt|rlNLyRY! ztnP-`ED!UEofn`y(>|J+)ky9BcaRw;@g5&el1u`5I$ee)t*IT6=??Z<#w zTw!8HbKw>Gh{$27*i{~Z@arE0#ngQ@RuNXKY6Fn0k81-Z0GSu^e{HXnm4TWA2pBiG z`=F+{zJ1}2ARB%Mxq@hX_&dA9YqIcb!WlCjgVx_0LqyySXgm@jHarL#_rRGw63CYq z)=DsVyn}6Z<9ZzC`7usiWkh#>7_5b$NlP-GvpifIeblE_05p$&fuAcQG%6I6r+(}z zcP$h6LXUC&c;SsIzBhgYG$uT3K_htGT^5d?OkP})B-R`02G23RAcO*98G?lZ!Jn{Y zU>6l(c8k~h6dkM~6cWLQFO(00TP{2Xjl#=z@pnqa(QgZOM&v=&0X!prL#M+V;T?*4 z!_#g^KD=9*0f)q4h--LRVZA)4@i-0;K@KAX!9cJ-L0RTi2^;KbXB@R55{Q%Eg>K;A zWZt!NxmZqagNqpw`{|i~+AhJ98zAd>*svRl1XqVhHSY~}4OY$#9+kufOp|;B4=9Ba z-5uB`Q?t!em?TQXMr2h@l7)RR%tMfxfkz&3P1w(XK44T>&xrX$D)4qdNIWdMKazs_ zfDc$MR1y<_S2G$aV77l1bE(@5O^>&v-w149hfsJek6h(E0CdUh9Tx5$4((>t5Jij9B(F5;)PhK~yJ3aZVyUXBmsz zemuj1M?x86NiAZRLrP~pfVd1^gyE7C`{Bd{CfxScOOr=3#k0KG;RNQJ(^rQXabg+^ z;tgj4Z+YJxpM;JA$7-(^ssTR_X1+J#%(PaX(kvVeTQ}2x`4YySan2O*%i$RWl7RbW zx$ll)LTKiNT429ch&=Z-Qq6_UwdW74=S5By5yawY=);k?N9?=&rkK{ZwE|>T$UOt& zABzz^nu2$r&frnJ0Xx13jx(Uz0!DWC{=*A)I;K*Ql(R$jhPjb960f)U`GmKitH-n9 zeu!+AjypGhk^RX4>l?*UB#VjqS#A~-)eH06@8yY0GsNAz9J+Octom5Lx&8Y!1-drk z%?Lc7^UMk_nNSKI881e z1Rf7CIf-t;u!H2}XK_5i^I*V}Hc!82fQ7u*jj}?~FXJE)jfhUT10qaZ_qqvpARu8H zOZ4D=o9BV|u$5*d5L|`DecF9ZQ|dqzyo&wkNC=imCO|DgCOJ zaR8*Dpro-Lz_Z^_b9m)G>I}UFbNVb2Kf8)9MHhXK#-m2pa?VH*!2}%2$Z-zDX{DjpT`c z`+{-a5Dj({!MM!OKx&Hw2h(LkEtL~=*_!qQcr6O1o9h7gMAixD5d5EuD;vr46exMJ z4APx7GbtcZ4*NDpV^S{wJL3rs-KdAVXZbz`%h6s)b>E$e(UX4Op4{ z&6YwC_8y}vFq<(7c&T8_d$SyfZ_`1>n2Yu4ZvL}Ai-fqOoUQ#bc{``+R#UXZG@_^Z zceRM+SHLP4M5rpm(~5C{xkLniv4T}+Ex(524?wD!zr)7OFL1F-ovQK;g4S31gb6VcHE1Tu%=vKD{Q1!E*D3yjg6cc;`)`G(k`vIIE zYAUJ>RvBmE3K9q&t-69Vr**SJ-4K7nEIcxhKdzF@efMo*1`Y`^Nix%aeY9f^Yi$3t z-~4*}cLJ`(588MI;xanGLNBNtfy12KIm%#xqepUZyk_L*{wh>C1Fj3iBaFn4rSH!K zQ4SlCdjWtdKmtLtQmgCn+L z-UJkbDi?Jnz`(^~_Fx)+5`g{Jkt`q5qYge!eh~ZRrIiOV*7;R4$5tycz}hE36D+q* zri96jR}1-4cuvM0!{@Cr-=h>mxV9@@RH_3<#FIf&T(Ke5Ad3*5a9b(KMHY{b69LHr zGLPust(Yutq}g3SJV6$bl{|5R_9M3zp7}^gVK>v9QYK6gvLjo6dV_}G8jCDrdUQ2Z z<@?HyGH930_7kP5Nq~IHJh~-H*wVIugj>%hMl0mX`Wj-cIU?Qh@s<~hZP5ZodV?&{ z2!>6+UF6D}IL3-1lanzPrva3ZG>B3axJ6Eg<$FJH<_kKArx2NT87EuEmFFm_14u0= zL-1|$z<8&yL_bXR#maS6=bbZ?}r3F*oWj8VbchP1PJK}mMvNe&UI?UlBM(>(EC$Hm2CVII2nxdDqKqY~ z&Doeog{hgY>s8s0CdCD zJwob=`)Q$G?N3|WJs^bG`=s?R5=Ha##H~i_4rbw|ULrxsSA*ikmCVudo=ocm$zwr@W^~2-$ZoM3va^y z2IT8ocr$llSf1fP#fkkl}(vPTu9bgf#an**H5Y#0L08 zUSe_jA;u44$%*bEb&cjQ`eQ?&x5#sU?t}VP)BpNmxW!tPR+Mb7D?y1 z5ZVm!+)PYDe^nt_sf{m}k-@GBhX+;xg>JbFZuwncenHIy)P4U+!3%LJp&<<>`gCXv ze4;ncpOYuZ;aD`JKgg!7KYjN_U=NZ;6cmVwtiwpaI@n(f&_>2Y?HI_h)>IyxFxgABdt>^yNe}71B!nwE9nntK=O--sL({pV#@62 z*FdnNj4&^V(3Ix2n>dgf8a7%Mr!=9In+W3DVoGbq*=qSp{381p#! z6x26RVRaWWE3w#*r-zBKXDRo8VjDk<_pwgT3}4u2g|3>ckJSvPP>?rgSB8aJX!gza zVYqgV&K%9s1XlZiEuUBu2d z(6B7uv=2-AfXQHD@uW5wJ&HoZ$`#gRptZ=eaFA*FAxRtioG}sQUt(Asyq#J@`$CR!u+-8;}@m86i{*7zb>e zNao3r7rc7Co&0A%GC`6EX>4CPfu(RjR@35oQ{}(4hv-F;T*OoeN>HhNYcjjEjKRSHzh+e!RLbNgQg+{0>o0B{#lh{N=xvA7 zJv@F=RayU5EsOcwSBvJ7XnEWTSfZ2)~84V#jY!92a^F_vel$)3;8~Swo4!Cy`!gkpM+Ch299l;BtKJ0Yt>Z$&f5)o!-vT2XO{` zgv8r_l2&Ki&tvxYKIahe3KkYB(}7xOGoc6|RIq!>34^yFnn2uz_D-Of03601&e^D^ zC1B4oH?pNXW*W@xR)7trf&ON1u|6E}lxG5a7_Em(p7q(r_py688NwQX;|}J9!+5xb z$axU#Mkau_tZj)up{$@IFaRhMkbFZU*P&>C_tDfjN+;Q49&o{;EcLw0>jY0cJ4D+jVA0W1t!VR3e9_ms;dsBmt`!y5kH8wHH>{_#Y69kC=R+lM-9%7r zrtY6f z5Dj>?W)7{f!2820Fq`zP8&VpuN4#&9Kxi)6BNJ<`lB{UG)>;60_Eutg#*1kI`So*D zh^b~iN5XYbv0%jr@upSl0N={-hKZ_{449hBE!iGW;00>(# z->i|XV8LymsKCo9rhjKSXl;YoU-0{X`u70;Pk_IyNNKPT@RVQ)#v>n~e%TtV`Ph}~ zu@+beT$uic4nEeyDkgWYHeZ0cP)7XX=U`w}>)dT0%$Ey@v)JqMlygMK+CX@#ZFi?R z0Pn+&sx6Vx(jVB)i=-i>ptq1Gh`72~>|i|sDq8-)QRXE$`43m`;A{%UWkrLQV@z=+!=j(zL_QsZsFA?)ut zXEpZ&NsSj_5sQUDvS(Ib!ElfrLG7fFz*E@v(wbRuC?o*`4jE4f5L@#7&4|sgYXJUZ zFXt0Jd+w^eb=h{HUnz;fb5k=&5kVNDLdPq&$8P%0^GQ@ZBG^q4e2*QXivKGciMe zY|~=I)vS)Z#sp`E>2s5-@K$tq1XU)t2_e2wDg?HXpc=q{nH3SxQj3XR1bWcFgqd|^ zv^v~@w*)>cWGfcEXj4VJt`QOSesH+?-3ZNA+|wmnTgGyHG#()S?DD8m|c(+@0<<6 z7ISwu_>cOs$1?$Nx&SIj^L<%iz@;_&6YqbQBl#b`Ucpmr58zLW7hx~IuSaCt+zuXU zL6LdG4hi^7Ap~1XSuet~6_=|5)E4Os>)EPZK6($xT71zYDgR)91kGw2n^l%XWi{A4 zVMDM!mA*ZpA=$#8!U5n0Y$>Nt0*c-M>vnf82ZfSQih`-9ZN-;|`w@SLs1s6$LPf)L z6VKw2ZFVKdIa}F67Yu^%&$(i6Wfa- zG^O3D>U_y6cC#@ryro(!1YfAAwh7*kcfn^jtR)Xna=WvQmqQ6U55n^oJ1Uc>5XIVG z$XB|pZodZ#=;y@swux$kr+CP)BK=$sl=;+IeHO?kTO{i_v@KTzKn=yl8hL>S@radu za!D*>hcy9zx6uv~9q-O2*t2iRo1k)DL@;#l0?e#}Ms!Ll6mPcP*neXU|5xGu6OF=e zfjWyuhxIYdAr*E$;{SWv6MJDa=Y;qh0J$fS9*!)F!kShDPh_>=ao7Xwpn3*s?^iLG z$bS%2*6_iy7rOR_wfa_z3_x7M$i(?sXY1l$)3Lx4 z@z08X{Al;qWA}wM9l-i+*(Y(I$3JhqOiCYwH9Q3~z*+GuFfyt$;oa7xb>~Nabel1`8IJ>+)r=|!0;nf|6>fOJzp548 z*2z>GnAY9BbpZ77CID^1_E!7V>QT_Ge1}3DRt@ZN0R;Ve-Ii|P4$(d!1J0UbIud?Q|)5EKaUW&H*Li{st9@FnMn%Rh0`ur zrB+ai*3`gUA`Lyz}K4&m5KpB0y)V z$qEz=TZ-Ik*X;Hm5w2g$f3v|BCnfkmtdec1)(xGW3W%OJkD@NNw?eQaQOM+fgw+B5 zL)8qvKX7Ld=Tyy`EVQ~C=I})>g?qPYd?u8?)%o989q^Z@tUs(WW&ZcQ{8a)q<-DJB;km~ z2Sf-45PBTh=_J$RT+oQCUCF9{aC<4PC50TYy01GI0y^8%_L%BMm+v344w~YxFUyE< ze$5D)`520eXxSUvldp;CJf_QEOD!k+x#m7ag26+(Xi*57IpTiX^1T8@B0TnFb&m^X zUqQ_5+QM1Tq08}lXtS3HW(`3G9iC@#ks(Z8m){#&;GwDX3C%XGji|bRZDE3LT{fCH z)F*fh18eBkuYmE;Kj%ysNdS&&2FjW|6EHTzIqjecA%r-4v7K4Bs`XqRq<+=(&~4p& z&Y#{L928_NBrCYiXcE&I9s#|sYhpj3iAZ0|5JzABHzM50~oxc@8$6=Ug)XnbJ<>H9qGX3;(*jc){^I`mYdovlO-0fB_gG( zGaVU*XtMT;;O7m0EQ$Gr7ud!e!PcjgK6(hz@;mdY*1f@j96wTQe!%!}-RcCV_?EXI zbx|(>8tQ_$#-`q$>k?E8gVpTl|ShfEc}tMF(WcR`CWt|ohi zQEn@F5EoiKw-l_gSV6J|J_G#40`IPl`4IB}K9OyEa2P#*nMEDl))80G&i>xq^BbMz zg9x-+e%sz}el~k{XJb@&93fy+c=Z$q7D*_BjiJ}zg^y*HL<^%TWUv1jUWzI@yD%fPp@}W!1c8}Mwiq3X?DkAZ<=IGGbcxOifZdloP zu5trY$zw&ldz$-s?AO3<2N-B^X|-wHeD=$lC5QCjlD!fL**#oZHeL@Cw5hhAe2Z>x#v4u*8#p9Cn26e~@ya1Pk}U5JEcaQRbg4 zA%T@TLN5&N%Ek~Uo0&+h*?OTBBw+Z8j+=_iPi(Nt?|V~@U(i;P}V!yFn73Ah2JC5c&3@UX`#Lu z9_bZ-6!nTwNFgHG8|eTApeftydNSC}W`gNKqTO~8L%7W|ealol!pXY%Z1(-T(ru~s z>x;KDFa<*6I1K)8fbQ`W1p;;iL1diW>hkn*G=;;7hR01i<*Rc^S;qeWL~1GGL!>0@6x)@)V%^c2V$HNyza_E;SB zSUAQKK`RDVl&T{I24?TQ+p6)Q{$I=P8qveH+F_UQR~!`ur|ljVrSdZkwmVtp=AABo z6KM``xkGWVZ?(u`-e7yS9dbG!BRk7b4@axrjz+To+=RZP#hh`M=Hu}yugj7SFBH{tX2{FJ_zg{bpKH&Xn`yt*}dmKxJ%S@*_5cY^=Rnn=tM^SmWYevz4w37&0We2kfKYbUTZ~aZKw83xt@zg< zgpD1*)2P2AOHc7}!Idl@@|c+DR$(R=j$o{IBxWPDfVs_q48z>0B_jKo2(424NtxS> z!tG*Q<{jX3&?}22(IO{qZd|f|L`6By3Ee+6D)F>HY}N3cS8%g;(H34ft4%S)?avCY z&PyWKqrGIR-Qcl|Q*|*&YgQh2T}o^Dh)}Umc($QDgv1EgRFe|PvQ{KGw5DP4{cbHg zaL2?d!u(Vj?9)qu#Itfg-PUF}g!r?&DcUhPoQbzi&*XDC7!SKH$PYMw62Zo)OITa+ z=44(dS%S8wnH~qC)<$qzYsTtt3-7}Hs1z=+1?X}9<*F8QK*4V8av)(~Il%wj&@j`t z2Cgizunv%|?@my=E!UTL1Y}kv*gE5i(1pJa+;e(%pO7^+-tg1=gaYf(oXr)SA9@Ti zJPUaX9a>0;(=hJrPFT@@`t0AvdErXaN!-;D6XUZ3&WcgE5BCdyHb<5pVjbd;7vBk$ zZ6Y5o7MxpwwzE1WTJL~IbR(DLpTMI>bPmZP201Iz**Uj!8HZ$&_4{mK>JHs0VZag= zd-I^hK$vlkicE8!7&$N;tIckR?flMRk|%&aCtR>QOJcsG^;@@pjexiM%JW!F>A<4A z7vdG zX`ss_Il<=)X{nxnRy@|4B=f@2qQDR8++dMrz+suoKB|JZxCn)f7~O8NZ(xwig8V=< zb(%{eE+ii6!LnV7VZahn9d9@Vb}}sMpMgiBA;`-XSd{&gE0}hASR=sO9EXxBCD7Va zFz4PRkjN0Q(U{AFD&W{d(RRje!ofUc!PktjH?OrFp5TywFD}}U4WtoN5LxkgrN<-L zAnC9%L>v2to0vrnXC#$njQBXwp#01~f~iBRLD=fdPsb@vBpM62(P+uzta+baZ7Ye3 zmU-$4hXVpVUVhE&I!nj$S$}tTYd-DDPKj0WE=)x^#f)Vm00AqC3!?95Z3}_j(kcrJ zE$+c|SqA%mOcA=aF;7_Iz3UF$+*-C)W}f;X%&^uznhw?O^@`Rek7@5)$-0mI`o=TSiw2m}w;`5xL-mXj}) zx%R!DJe;Cy+73A^X0d?>r?{-Bbz0uL9WtBcX}g<$rnEhoE47;4Oh-je1Kl7ND*hZT z<=6M>js_z7UQ#vnX|;vcUitS_fuoAxG>#hdnr!PpX9lEgk>Pt(%bOkH?nqe(-St>o ziZQ)wf}v^-t_o9k_iRw;XBgu-Vqv+!{ZA6l#ailGY$V>NAnk&Q5KFaosXuHBzF$5k z3ie=sg>+Md=IZz)^C{-4p;xRszKliOXYLJKb9op-G-y2PbC^$rG+T>oQ8`o@7bLcy zS{c*jKnx+{F@6tNkG+}233ThZtV{1`j6^g>CjlO3=w1D~4P%Yh9H(K^ib$ugMNhx1 ztZ>#-Tr8cnTOIFb{hwxT`5>}AL&-KO!U%zX%62oT)VeiLR8=;!172Fr2dT)-S=taH zt60lz&6FDfueL{Y&Cx&N?*kcsOl&V_eg*gyv4r)7>0$mVN~aJwaEZO07a{k4?1Wuz zTyuQZ=gFYXYfyKN*oG;vQ3Pa9iJ+X@GOlWZYKy{L1mVaThBh>8TO42s-z4zt+gS#G zX6;Y9PMYrMSfa<1mPDEyMgymU>!N zbC&vM!Ey{C25Td5GeD2f&I%%1mIqJ2&dk(8@Ca5?-|u|Ox05l;32w|-~YWq=Y7 zgYLg8UeH8`4_HW39g!wto=}pf4Qu{?NxQrTUkk+^E;o8ow=DE`z!5-z;m!-oX$Uh* zv?Bpei@QD3E(9$Ivsj#X)T5X^lQzBFa9Se71MNKyjaPYe#Y*kp zOii^d+LZNcTS!A_D}tj%&K@+EX^ruu!w)?PN&<=g3h&Asj3sqti& z${9`pO9t5X(!DWM$2t=+mZdSm&v57RJOjhx=XAkflYo7*XX;YH-zL@s1Q1!T2y1!m z0%5yz;~Jy$l>LN{=dl$JW$oa1x6l?oZrtIQ&FT|R_@ID@Zw&dW);l>X?z5c&>5rA} znsT)a)p-)eX?SXyArlRMO?c+z;4lY1ERT3vMR}rW@wD|8OR(UIJPCdeG!o8p}mS4ZKoep?OnUh$Hr;EVfW5tdP+?HEwcXr=S z<-#P6{N8?G$pl%hX#rES$a6gWfLKxT=P*rZD`c2Rw3Z)^pmaxnc@!Ij_#eXmomC+Y zpL)wmS2SlvQ#jv}e38Y=s3xGLc z`j8iBiVeAh+Vp{nX`;!T8iYZZKezSjwVuPoi=X*mG{&d^&Z~#iFn3WZkmE$Mm1;Q(TP{EV2aW7p--r_%H$I)ii{W!qk zJ_ey=@w5g9xU2_8f@T(ATh|~RSg4ts~hs`j5>`F+oBBNTEjmCcrQqUT3 zvtPVOlcq!`3@tdjr3S<|57|wKVWf!3`;1QH+vnMQYsL$apFMDJ*f}cZdT-Rbnhs}& z2iP*YoD}GYh}U!IJ>7RpK@oP%e!bhI=D43Od+nAlKq)Wd<~jrS*6BlY{7Ch1!I4~$ zgC4COgW|e>bYOcqRhl5&?QCPLl1qff?90xdbmr#<>u>XyPD`hKRFEX=X>SFJXA5vO z7&k7189*(g#jgAW*}Cn_bmWw+KVU|6RyGpDdK^fZH6C_|ni^$C5&NJD(1e577GX27 zGu!}r6ye2gZ08m)*CHU?vVESn18T;`GEq<9Lung-EhHz%e)@*lRp-&YHV{a+yQzBi zE#RuJ$-rhg^LJt+%GzS0?I-I97w_X#4%sx2o2{o>XZlJw)!6YW7SlQD$~pJCEPKbg zf49dWBv~P9-el~_wBcoG2 z?8I7s&o}%ik?3%Le%5R)Esae8>MtUnQ{WwGV8h{kx(M0coA7>Ws}M4dqo~iLuXty{ zPYbf;AUe8Jn?zkWW!PjeJG1X#&o#F_oWoX5cl_qHfR#NxTd>I86}M$;*uxhbfZ_zs z?=_0tc4wjZAHW!&Bi3dV!=3pX?)2cG;}qI|ADv=~6E7JyhFKK-DIrmBmvd?Dm3M-* zgFAG7dm@~4iX*eJ#qPptpEBF~)^kQ>jkfhxtcU7CJF-4T(U>iM!Po-ylKqnmv9W#* zhtc#paSnb0U@lOHqB% zl5;pU5<)fpSPc#{$uS7 zzs=HBXRLCK_tCk|4b3Hd%2i@FVDieWK;4d)yyNQVY5~yRO(&kPywB-4d-P$ zJnW4~iaBF3*ZWy>mri%>}OX& z{kB!JETuCpgs^X#iDz zJo!h|FOb>3Ig86*t4Lly#A%v;PPQSUcqkjcMwnQxBfG|yTZ;h0wVZcU6Zsa$gIL)G zMa1$1jj^zxlguO`Bm)5vq^MC84n;+s%uK>4DI@`cZ3Gn+c2%&Ut6)Q2#a*zWqCV^u z0qgouVMXyNief`S*7r+50+*-#ae=dbXJ5sX zBPqGdLx!$R*`?V1dCAaOwfDA{bdyB>78uFQe?!>(M9=;58)7_LV1!m>Ue+>^WnB$Wgk#W_3rRSXw zBYbilPQtZ{l2eXRV=Z4)74L&Kr-$9`iT%E+9CrWCV^_k(r0!?0POi^ca>Hr0e!1m# z?;gFQPrRGF=>I3!GrG8s-L&AdV~p1;T19S3T?+t-9=K{%1aARCGiek8t~A( z=a3zVHaSC+t>z>qS;O!%(a#09i>pf2YZJ8XQ^}m7@%|y>J6orlO?o{^h>z~RIG-J6 zU(~fM=4Rp6!t7Xecwyl+U0SyRSNKN?CT>p`Z&oEt?76SotoLtSU#(GGS-q5A6nLX5 zXU1Nq{&zSj%hV&PYUjG8Rr7K}4=+tTB8FGqx?D3`x}cM7xOU-^VdL!c$R+JheE&AX zOLj47^@Dm<^n^L;b7r#(dpQv63#Zld@YF&u$Z;v@^^A@%QMk+mK|KaAKdxp=qh z!7)RyL)t@;j#e+;-z>IUyea;C=1VL!%~|#5&cRuiq02!9a;qW7{}B=|&OWe!yM5=B zvPXSu1K()Ipfm5!Dy@rqQM|?Kc79!U@V=2ccX|};pY`j~e!W~A%uDnkQ{JW6+TFT3 zSm;(6d9uR|;%>#DGjPXuiSipo{S`cAT|jR9est>_cmLHji=XqjUAN{YF`~-StzUXR zk;eBaN!k`RH(&0N(LMV>A7N*v&Shq51{aMuaoN4BUGsM_JI|dbL`9B={g!+fZ9zj)H(}M;yjjcgV&@fQaj!pF%LppqPN+^> zS+ILle8>Vgf$80zkUd`VO4>2Kyt1GBvAZMVR@WTz@SS>W8aLm;x)8aOz1U^3TmWr& z>7H}$f+qd#dj0K<-*xxUgh##mNqZiPmnZoQaUM6U`{+z|@Zq#omaFvZgsCCU7kces z(+@?esNh}EH;y^b*ujmvw;md+?HvWafgCjGhaFoIoQ8Q**Zq)`&Gl)E=yTN$=QEe@ zYUi`G!?legV&0e0&+{hj6*@l}vJfBsELlPS*e)LYmOT6EGrUB(f$^ujY*gjj8Ioyl zR=o1@O|~>z++QFC1X%rrC?)(N=mZz z3N^#qLb(G9huNL_jW{e3nj|BKP3AeERn7z>$SV<(P zw+fFo#OLoVW*GuN2^CHeC~a)>WEjFlgd`*o%7hT#nGixkoZvxx8IJLBK7j~uteKX- zQmez1IBC!V>TC)KVFFBs;{q`b`G$Yv7bG!)pHSR~y% zE`t^Uv`C>869{1!2t!Cd#OF#~AQ7Jjw0KgCCqM{-C*vEn2psiMX%rY(Qc8izNsd}6 zH%=I8j(P_8i&Bvi3l7H!y0M(ZF-K?dqDLrD>Vzy(2sut+2n z@nEjNBoIt$v|vLTWD%In;~HlSYlDJ>Ky@+0b_D{)agYw`r6Dn$N)x0~MT%Jl4%6V- zG%caQxgszf=8fq{pbB%jC<3EAI0)vWTmW7Z*Y#6_Qc}fx#luK)Xp^tCHzrF%9V{2c-n%0^8JB zF6Ix7%jMWIoY)A`uoR$Z5CTJ{qKR0X#&kJ`o8x1?Zk)eV#EmvuG`nknqnmLITw>r( zcoQcx9XxsT;LTNj{ZImzNe=AB0ehWz3 z>)%4#)M-I64S(8dBmWC2pHfZzFXFeEV&Z+;YR3PD6chDRNHf04sLicUDXj~(h5zN0 zw(!3ODH( q+U9Y_CS`!5HKUWxUox^&g+;&4whq&6B3{S)E%Y1ptf~3S0mH From f5955a4738ee5164c5c391086f1c4c0dde6d91a8 Mon Sep 17 00:00:00 2001 From: seth Date: Sat, 7 Jan 2023 04:41:09 -0500 Subject: [PATCH 074/199] 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 dc16e7889..83096aef4 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 zcmV)SK(fDyP)7002F_dQ@0+Qek%> zaB^>EX>4U6ba`-PAZ2)IW&i+q+NGUYmKZsXW&dpzwFKjATn-1bb9x88{Jxj&5tSJg zRoU4c3dtpxyA34p@E!m{*ZaT!*LVHLfBZ*CPtR8_?Ww)ioBwjpJq~{J+&}+~^J}>C z`StwQ$M5gLKY#uj@%OiWOngi~fAQ3xkKvEc|9-AUvp}|Pr`!y-8%jGAGf_e zA2t8&7yZ+&eDlxG{?q6G_}%r_-O7LLV*1M>*FQi0?dPvh{(e`^(*MJq^Yf>f{L|;Z zmh#UN{blvf;(z=3&O7rKBUhHX{=U>7uX29J*YU#HTR#_%R`15o!X?`I(fRRU)eBSD z_qr!PR_GzJU0=vyhY`N;o{uXm9x=!FitJU4am92#`LK6AuCtTbAK{CQJ>2PGlh@hy zoZ_F)63%V=y{>b&hdb}Um&U-w0^jm)|G56$Z}>i4A%y#P@i7aYD@GTa$8hKTl`mrt z!uPwVm8bMFYO1-GT5GF<;%KSmtCdz;YftNKJn6|#dFs=i z=jqSru_r*Y^x9j0dhcU!&%u=k-#z$(F=m`;=2@;;XPeLLb1ckf}y|7+i_mj7wr;{WT;U2fgKxpV(@-~Mjbwqa^-YqibpJ#ox0$1vZ?-hKUjzc=0O*>8E~+La64{p~no zX!R{^eL!K_t;Ykp4}tQ<&fc~QiSMRUBlBqad0Ck+#r~DM&C>dM zpVC6A4{jUh^WB~G**sq)czm^7>8Wdm^1jbrA9P`~kF73aR>5t(d8F8CiYxE8=dfPD z?_G`8zu(EL@IH9Zd@fe);~{3)weEa>sos09{3JXIf9-5o>NlRV9;~p=$qL==3(xmx z0Yt6E-NU+`ukNu*?;nod1D?L_uf8nh*Pr)(UF8)L#*Bl$ylyC8p7YuDE4=B4=MWKR zz(ugx-XK`z+1#jr+`Rrs{hj?yqwyDYl*s=tUYc;SF!ud6og=rIF{ZD~eF3)_`z*XL zzSt*!9a+x$#Mt4Nd12}c&?+Ifm|3*;e3`c+ppJXDujHLa!)c#y;SgI=h2L-oP=OaM z^(DNlv513}U$CIBy_07y?D>oZr!K(J{SH_z1NEiHn3xcH0K&@($_%N^$cJk%;#pYfW7C#c-E*q zaeXF{?txz5jX2 zaXve=mA`t`_yVW%dk8gr3Fz|fmKRU823CXvm9SUC=)=JJaK)z;)B>N2W5I0s#+z-s zu&({q=WAg0^4UP1p-DE7wZ^&tXWpVWJQjQH`Q=af1u_flxxR7s5yoKG#`8i~3Q-P3 z4Qx%H?< z*1r40w>)c&2O9zvim`>A0bp_E-dGjZHZBbk%xMoUlLmo7OvCF#JZw?QR{@HKa=<}w z+t%-hIp8C0Vl;kh#nAS(>j2e(S4^&9#;oD%yS*>(oT0WD+roYc4!Hb15n{FXm%Z=& zNV&H~80!7vi(@a}gCE4%cujU0wA4gFZ2TD1Pdq4yFIepI)TAx^D z1>bXMcl}Ec4jkdn6Tg046PBXB7f~&|PycXk!VL~pd^hjQa-??v1y~`!Bg%8{ z4;T?wFrM-wd8IkT!HbQ3nqVdDV=Rq`LFC*KcmZs8EShEotPYm%+A)j>Qh)E8@Dsk9 zEBAQKh4a@2H1sdmIl!1w!Jm}`fiZEMh;hk>0XD(|4)Z<>NWb$oR6Ky&ghPVFP#AD~ zh}%RtmUMJDw05QAMxqt2d0_3{DT9$?<_{-uNn6J73$paO{|AQg^YOjYiSr$21#)sG z?7|en_}Fk5msBGWMZ9xEs}raa(fo--kr~`c-WUjig1^KaV(z$KVpD?l!Lr!Y^lZf3 z#;DL)*Nrhp%#3Y+Si`lS7f_G2;ANY?E9Jf2(}7P-&<(sMSRB%euD#xRGW!QBEM;$)Ejt3)=p8EiZTQi6tL!Th{b4vg5sy&36+|DJU0UBW ziH$MeF5CxxfazEOPmApkt>IBCU&JwaaN(Z7Pp%6|@VF2!i2Se-k$3GMj>98;05C32 zuqSr1nQPn;14{tig>3?7IP`b#2c{J&F`^(@hf4)_09xu;2m`)#0)I75NYE@`8(zm+ z2Lk^6UWBa&1c3cuS7QE1zCdqGrkqz7fQr zlDJUv+iyUx^TI4S-h*CY1z>e!sL zaBtKbCJE>+XzU!s`h`u_^E9|9pdVrWC4v|{hui?giQ8si8^+9zv2Fn)O9>ehW!?za z8$;~{^Nd=#^gZ+EcG=D>p6)GPvnmyA zSr;KD!Z|lY80c|}fE3-2h0H|6g56Y{wV-WL!!I0c0hXvu^qfLU`~}7#5g&h=Xod`f zfDLrSQviL;kGzE-0J_hDf1*sG*73` z(GX(IdQNI%a2-77QM)GD9DIkZOS~B}AY5QlR~mon_M-4ZSOK?#uc_D#MDv3?6uc3J zi#pwv_adAMfuB5!m{RX~EfdFe$zT_|&NAaFyd0*6A_qARQRg)X*`>$`Sl=b`cM#|0 z@jzU(?)5@70rd#}!~20i3@Sb-iGEMSbcWo3R}}(t6TX0ktdt|Nu)8I^1UV34z4aPj zjT=!FD=W^ua*%?46Z#DQkR0(uXjq^Ss7L%jN;(9R-LI?#L@sLR`7-q~V};z}M^b5M z0`7tQAr!;29+vGhzrluLI~l>{yJUY)h+?PGf;4_$b|s1dvPL(yZLXL78LP$ziS=+F zYzLFP;SKG9D-pGgwJ^O2^Y}m-)HOs4!-ACHyrc z1IKq&H7+p(42MAy(kjHPvfv+LsZQQznubamISyC5DWZwZKP*#3JbW)K999dUXSA+4 zh|rOknW&GU!7D{f5#;4@wi>BeGlAu+yCO@2OYQW69KbKE7A*)#y(K<^AUsUfqRrob#C1*(%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 3ad61668e..198e76ba1 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 bb5c8afe9..2e5b7f25d 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 d6ef442b3..24875e331 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 5c9216eb6..b5c03defb 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 0a70ddfc3..bb10cd487 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 0830a030a..eafcf4820 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 e17286e16..653e89e79 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 c184b8f3f..9cc5cc765 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 087/199] 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 3e64b74fe..f2cc7bfb9 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 a921e3781..ab80fb805 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 4e1eb4889..cc2d335be 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 6562ad2ea..992ba2ca3 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 a6cebc6fc..44c13f408 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 bb10cd487..4f36bffa4 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 eafcf4820..5fb5bd4e3 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 653e89e79..d450e8dff 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 9cc5cc765..15ba831e4 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 088/199] 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 f2cc7bfb9..ed8d8d2c8 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 44c13f408..7ccc946a5 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 4f36bffa4..d5e73bb80 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 089/199] 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 cc2d335be..42826aba1 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 992ba2ca3..61a3d0c01 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 95b0f8053..1ab04fc8b 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 7ccc946a5..134064853 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 d5e73bb80..9af44b5a0 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 5fb5bd4e3..d0b5be217 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 d450e8dff..be2c4492c 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 15ba831e4..b27729838 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 090/199] 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 3c8b5d392..3fd9bb233 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 091/199] 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 1ab04fc8b..01394ea40 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 d0b5be217..dcf13303c 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 be2c4492c..d955a2665 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 b27729838..f216a610e 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 092/199] 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 e0a80f20e..9d75a4574 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 093/199] 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 7a1358114..aee5245df 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 094/199] 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 9d75a4574..d074863d4 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 095/199] 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 6ca88ec60..7dc744aa9 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 cb62de6cf..000000000 --- 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 000000000..f115dd570 --- /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 a36e4a3dd..76e3d8ed0 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 cedd4a96e..2704243e9 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 9e82531ac..349d768ff 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 4de80468e..8d56d894d 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 75be25b2d..0f30689e1 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 b08f3bc4e..7d59a6ae7 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 8ab2ad1d9..0bb11d834 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 096/199] 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 c7ba9e9f3..f235a2ac2 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 75a5c0eb3..79290654c 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 ac5a36184..95be87408 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 7902e734d..000000000 --- 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 4e75de4df..000000000 --- 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 abe2b6ca0..000000000 --- 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 953e78074..000000000 --- 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 062d86ce9..000000000 --- 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 210c565e7..000000000 --- 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 d7659c27d..000000000 --- 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 7c68809a4..000000000 --- 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 d1855f4f2..000000000 --- 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 4938cae56..000000000 --- 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 3592b8e38..000000000 --- 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 024a8bcc7..000000000 --- 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 e9e2ab118..000000000 --- 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 ce25dd549..000000000 --- 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 8bf3358ed..000000000 --- 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 f5e9dce9a..000000000 --- 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 d89624f31..000000000 --- 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 0523c11bb..000000000 --- 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 10d36cb95..000000000 --- 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 097/199] 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 95274f150..87703fee5 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 f235a2ac2..2194317b3 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 7dc744aa9..60acc6fcb 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 000000000..a8da5a2f2 --- /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 098/199] 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 82ba9c7d0..f6ab13321 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 099/199] 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 fca306d78..071772c64 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 100/199] 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 ae45e0966..aec45a738 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 101/199] 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 b6e179e19..050adb87c 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 102/199] 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 d074863d4..77e934e11 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 103/199] 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 24b261ba7..4b4c73dc6 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 b80db99a8..cb6fbae0c 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 6a1807995390b2a2cbe074ee1f47d3791e0e3f10 Mon Sep 17 00:00:00 2001 From: flow Date: Fri, 25 Nov 2022 09:23:46 -0300 Subject: [PATCH 105/199] 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 eec6c7878..a1a68f5bd 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 2b0343f44..000000000 --- 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 000000000..687eaf518 --- /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 950204703..350c2edd2 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 43fa3f8d6..42021b3c9 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 2330cca1c..67b418f4a 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 919220344..932a62d9b 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 234330a78..9bf81338f 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 a8b0851ee..a79e58615 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 703de143b..000000000 --- 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 34fd9f307..6a507caf4 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 518fed7c8..f65a6a4b0 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 000000000..d18a2caa6 --- /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 4d71da21d..ae4013995 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 4c6ca64c2..114a27166 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 8dd3a8468..285fa49fe 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 163c706c4..4a98d684f 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 dc69769ab..fb6f78e82 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 498e1d6e4..36b62e3e0 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 32aa4bdbd..617b98ce1 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 f1e4759ee..af484be01 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 fa3244f6b..91146a525 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 7633030e1..000000000 --- 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 b8af22c73..000000000 --- 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 000000000..eb17008c4 --- /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 000000000..834f274a0 --- /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 747cf4c35..8e64be094 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 e1a186813..bd84fb546 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 e2d275478..7826b33da 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 abf8ada13..177ce5169 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 aec45a738..a01610892 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 0ed29311b..510c7309d 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 9754e5c43..4b096eec7 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 d9c4fadc4..38fe058b5 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 9b5d4f1be..4bcd40b5d 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 8b49bd1aa..5977fd10c 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 89935d9a2..83748e1e2 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 4a3b9f299..be9735b5c 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 24d23ba98..8a77ef7f2 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 fcf6f4fc2..190360421 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 2704243e9..4ef42d6c0 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 bd486f0da..3e3dd90da 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 000000000..7367548fd --- /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 000000000..d6b3938b2 --- /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 7c25c91c7..f45a9c4af 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 9cfa679a5..e2d0ce379 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 627e71e50..1bce3c0d4 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 9633e3b40..db8af0c5a 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 ed58eb32f..31aae746f 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 368406491..7c735d901 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 0f30689e1..853f2c540 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 c9ccbaf20..8c1fec84e 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 000000000..d672a2ac7 --- /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 000000000..af0e9f553 --- /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 000000000..3b382d201 --- /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 000000000..32aad3d91 --- /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 94365aa5f..8fe1d613c 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 bc2c686cd..b602dfac9 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 6a6aef2e9..b94377d3d 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 bad78c97d..490578adc 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 58479ab94..597a0c258 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 af92e63e9..51278546c 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 386897fdb..bf62d22f4 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 c531ea904..17f0bc932 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 40d82e6fd..6f816cfd5 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 b60d9a7a8..18b51fc33 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 4d9097b8a..b0458f335 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 098e8f894..292894699 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 106/199] 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 8a77ef7f2..89b87300b 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 190360421..b378b5a9d 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 7367548fd..b143750b2 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 d6b3938b2..3b234cd1b 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 1bce3c0d4..7c4b8952b 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 31aae746f..59399c595 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 7c735d901..e3d760a23 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 853f2c540..8941d9b77 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 8c1fec84e..137a60469 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 d672a2ac7..e8af0e7ac 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 af0e9f553..6a94c3995 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 3b382d201..161b5c22b 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 32aad3d91..f731cf56e 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 b602dfac9..cfe4080a2 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 b94377d3d..501937e2c 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 490578adc..723819fbe 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 597a0c258..6c7d0247b 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 51278546c..ee96f0dea 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 bf62d22f4..b0088a736 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 17f0bc932..5d2680b01 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 6f816cfd5..07b32c0cf 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 107/199] 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 18b51fc33..f736af087 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 108/199] 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 a1a68f5bd..77c691061 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 89b87300b..000000000 --- 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 b378b5a9d..000000000 --- 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 b143750b2..523a1636e 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 3b234cd1b..298134938 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 7c4b8952b..d9069915f 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 8941d9b77..8d4415460 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 723819fbe..2a8ab5266 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 e6704eef2..80850b4c4 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 5d2680b01..1352e2f69 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 109/199] 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 350c2edd2..275ddbe13 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 f65a6a4b0..cd40a6baf 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 d18a2caa6..49aac7128 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 ae4013995..89249c410 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 114a27166..f3cc0bbf2 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 285fa49fe..7aee4f4c8 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 617b98ce1..7498e8302 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 db63cdbbf..33c4a5298 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 eb17008c4..77b085c01 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 bd84fb546..ec38d9ee0 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 a01610892..f270f4706 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 31881414d..e73e4b186 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 523a1636e..2eb859284 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 298134938..95a5e6284 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 59399c595..c9dee449f 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 e3d760a23..39d062f91 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 8d4415460..556bd642d 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 137a60469..2fda3b680 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 e8af0e7ac..cf40fef27 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 6a94c3995..af33bf554 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 161b5c22b..e04278aff 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 f731cf56e..b51c7ccb1 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 cfe4080a2..d0f109de7 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 501937e2c..7b253dce0 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 2a8ab5266..67737a76d 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 ee96f0dea..9d26ae059 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 b0088a736..798a70e63 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 1352e2f69..88621e053 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 110/199] 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 cf40fef27..eedc5202a 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 af33bf554..45af33a23 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 111/199] 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 49aac7128..78441c344 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 95a5e6284..34120350b 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 45af33a23..d0b9234b9 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 e04278aff..6e6868c5c 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 b51c7ccb1..b95c5a405 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 6c7d0247b..12b51aa95 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 9d26ae059..895e23fdc 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 07b32c0cf..a263bd44d 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 112/199] 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 78441c344..a2078b94c 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 89249c410..32729a140 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 f3cc0bbf2..2b2885645 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 8e64be094..8d7e3acfc 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 ec38d9ee0..949fc46ec 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 c9dee449f..5eeac5d50 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 39d062f91..3aeba3ef5 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 556bd642d..04cbddcb3 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 d0b9234b9..facff91d5 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 6e6868c5c..43b772073 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 b95c5a405..547c4056c 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 d0f109de7..a1cd1f260 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 7b253dce0..47fbbe1ac 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 67737a76d..e34be7fd7 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 895e23fdc..06b72fd06 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 798a70e63..2511f5e59 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 88621e053..45902d16e 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 113/199] 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 a2078b94c..5f4e1832c 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 5eeac5d50..29cb21325 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 114/199] 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 04cbddcb3..d57e748b0 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 eedc5202a..5bbd39d30 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 facff91d5..5f9ce36d5 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 43b772073..200943dab 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 115/199] 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 29cb21325..beb8aec15 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 5bbd39d30..d9c309123 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 5f9ce36d5..05aa6a942 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 116/199] 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 beb8aec15..d52a430e7 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 3aeba3ef5..c705371af 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 d9c309123..05d44ee2d 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 05aa6a942..d8be3b6b7 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 117/199] 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 5f4e1832c..8f794955d 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 118/199] 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 2eb859284..fa3352b3b 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 2fda3b680..a3aab1dee 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 200943dab..bfa7e33d9 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 547c4056c..71fc6593b 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 8fe1d613c..73a9d3b1a 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 119/199] 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 d52a430e7..433c7b10a 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 c705371af..1fac90409 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 05d44ee2d..202aa29a9 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 d8be3b6b7..02014fd69 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 120/199] 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 b1ef1c8dd..ec4660966 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 9bf81338f..fb451938f 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 a79e58615..635f4a2b4 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 8f794955d..dfb3652c6 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 32729a140..c8981585d 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 2b2885645..8e7ed7271 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 fb6f78e82..890bff484 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 36b62e3e0..0ae4735bf 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 77b085c01..88bbc0457 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 834f274a0..ab5586fd0 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 8d7e3acfc..028480a93 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 949fc46ec..cba3afc8c 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 7826b33da..daca68d7a 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 177ce5169..88e1a6751 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 d29a5e88d..55782ba77 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 202aa29a9..be5ead90c 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 02014fd69..7e8133730 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 121/199] 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 be5ead90c..eb723159a 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 122/199] 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 9f84a9a7b..3d0d2dca5 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 000000000..e91be96c7 --- /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 000000000..716bf853a --- /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 123/199] 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 fa3352b3b..147373c9c 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 34120350b..198435322 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 f45a9c4af..9c638d1f6 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 e2d0ce379..7ee0d65d4 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 ab3bcc2fa..bf53ae80b 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 124/199] 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 687eaf518..8c9dae6fa 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 275ddbe13..5ce39d69d 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 cd40a6baf..b1f8050d6 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 147373c9c..b9367c163 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 9c638d1f6..7b2df2780 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 7ee0d65d4..5ec2bc231 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 125/199] 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 fb451938f..d95230528 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 126/199] 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 f270f4706..7ade131e4 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 127/199] 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 19d6d3c29..8d7ff044a 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 d5aa4c1a0..1a2b14973 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 128/199] 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 8bfe2c844..91cb747fe 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 129/199] 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 79290654c..0221d1b08 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 130/199] 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 5d4004d0b..1373815c6 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 c0c3892064a775b13fd5cae00f58b43bee062003 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Edgars=20C=C4=ABrulis?= Date: Sun, 15 Jan 2023 09:47:31 +0200 Subject: [PATCH 131/199] Version.cpp: Improve version parsing to handle mixed numeric and alphabetic characters MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Edgars CÄ«rulis --- launcher/Version.cpp | 38 +++++++++++++++++++++++++++++++------- 1 file changed, 31 insertions(+), 7 deletions(-) diff --git a/launcher/Version.cpp b/launcher/Version.cpp index b9090e299..5d814a25d 100644 --- a/launcher/Version.cpp +++ b/launcher/Version.cpp @@ -74,12 +74,36 @@ bool Version::operator!=(const Version &other) const void Version::parse() { m_sections.clear(); - - // FIXME: this is bad. versions can contain a lot more separators... - QStringList parts = m_string.split('.'); - - for (const auto& part : parts) - { - m_sections.append(Section(part)); + QString currentSection; + bool lastCharWasDigit = false; + for (int i = 0; i < m_string.size(); ++i) { + if(m_string[i].isDigit()){ + if(!lastCharWasDigit){ + if(!currentSection.isEmpty()){ + m_sections.append(Section(currentSection)); + } + currentSection = ""; + } + currentSection += m_string[i]; + lastCharWasDigit = true; + }else if(m_string[i].isLetter()){ + if(lastCharWasDigit){ + if(!currentSection.isEmpty()){ + m_sections.append(Section(currentSection)); + } + currentSection = ""; + } + currentSection += m_string[i]; + lastCharWasDigit = false; + } + else if(m_string[i] == '-' || m_string[i] == '_'){ + if(!currentSection.isEmpty()){ + m_sections.append(Section(currentSection)); + } + currentSection = ""; + } + } + if (!currentSection.isEmpty()) { + m_sections.append(Section(currentSection)); } } From 6fb837c529ce838efabd1899a5803c124013fbf7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Edgars=20C=C4=ABrulis?= Date: Sun, 15 Jan 2023 13:18:13 +0200 Subject: [PATCH 132/199] Version.cpp: Add version string parser to split on '.' character MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Edgars CÄ«rulis --- launcher/Version.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/launcher/Version.cpp b/launcher/Version.cpp index 5d814a25d..9481716d2 100644 --- a/launcher/Version.cpp +++ b/launcher/Version.cpp @@ -96,7 +96,7 @@ void Version::parse() currentSection += m_string[i]; lastCharWasDigit = false; } - else if(m_string[i] == '-' || m_string[i] == '_'){ + else if(m_string[i] == '.' || m_string[i] == '-' || m_string[i] == '_'){ if(!currentSection.isEmpty()){ m_sections.append(Section(currentSection)); } From de11017552a5e3e06c436051b2218c4411a0fb24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Edgars=20C=C4=ABrulis?= Date: Sun, 15 Jan 2023 14:30:18 +0200 Subject: [PATCH 133/199] Version.cpp: Use anonymous function to eliminate code duplication MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Edgars CÄ«rulis --- launcher/Version.cpp | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) diff --git a/launcher/Version.cpp b/launcher/Version.cpp index 9481716d2..f61d53e80 100644 --- a/launcher/Version.cpp +++ b/launcher/Version.cpp @@ -75,26 +75,18 @@ void Version::parse() { m_sections.clear(); QString currentSection; - bool lastCharWasDigit = false; + auto classChange = [] (QChar lastChar, QChar currentChar) { + return (( lastChar.isLetter() && currentChar.isDigit() ) || (lastChar.isDigit() && currentChar.isLetter()) ); + }; for (int i = 0; i < m_string.size(); ++i) { - if(m_string[i].isDigit()){ - if(!lastCharWasDigit){ + if(m_string[i].isDigit() || m_string[i].isLetter()){ + if(i>0 && classChange(m_string[i-1], m_string[i])){ if(!currentSection.isEmpty()){ m_sections.append(Section(currentSection)); } currentSection = ""; } currentSection += m_string[i]; - lastCharWasDigit = true; - }else if(m_string[i].isLetter()){ - if(lastCharWasDigit){ - if(!currentSection.isEmpty()){ - m_sections.append(Section(currentSection)); - } - currentSection = ""; - } - currentSection += m_string[i]; - lastCharWasDigit = false; } else if(m_string[i] == '.' || m_string[i] == '-' || m_string[i] == '_'){ if(!currentSection.isEmpty()){ From 198139feb4546fbe819b7076c1689582ea67caa7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Edgars=20C=C4=ABrulis?= Date: Sun, 15 Jan 2023 17:07:44 +0200 Subject: [PATCH 134/199] Version.cpp: Simplify Version::parse by using const auto& current_char MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Edgars CÄ«rulis --- launcher/Version.cpp | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/launcher/Version.cpp b/launcher/Version.cpp index f61d53e80..73be60587 100644 --- a/launcher/Version.cpp +++ b/launcher/Version.cpp @@ -79,16 +79,17 @@ void Version::parse() return (( lastChar.isLetter() && currentChar.isDigit() ) || (lastChar.isDigit() && currentChar.isLetter()) ); }; for (int i = 0; i < m_string.size(); ++i) { - if(m_string[i].isDigit() || m_string[i].isLetter()){ - if(i>0 && classChange(m_string[i-1], m_string[i])){ + const auto& current_char = m_string.at(i); + if(current_char.isDigit() || current_char.isLetter()){ + if(i>0 && classChange(m_string.at(i-1), current_char)){ if(!currentSection.isEmpty()){ m_sections.append(Section(currentSection)); } currentSection = ""; } - currentSection += m_string[i]; + currentSection += current_char; } - else if(m_string[i] == '.' || m_string[i] == '-' || m_string[i] == '_'){ + else if(current_char == '.' || current_char == '-' || current_char == '_'){ if(!currentSection.isEmpty()){ m_sections.append(Section(currentSection)); } From a84e4b0e07dbcb736d92e98a3beca9025c981686 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Edgars=20C=C4=ABrulis?= Date: Sun, 15 Jan 2023 17:44:17 +0200 Subject: [PATCH 135/199] Version.cpp: Format parse function code using clang-format MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Edgars CÄ«rulis --- launcher/Version.cpp | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/launcher/Version.cpp b/launcher/Version.cpp index 73be60587..0640e6d3c 100644 --- a/launcher/Version.cpp +++ b/launcher/Version.cpp @@ -75,22 +75,21 @@ void Version::parse() { m_sections.clear(); QString currentSection; - auto classChange = [] (QChar lastChar, QChar currentChar) { - return (( lastChar.isLetter() && currentChar.isDigit() ) || (lastChar.isDigit() && currentChar.isLetter()) ); + auto classChange = [](QChar lastChar, QChar currentChar) { + return ((lastChar.isLetter() && currentChar.isDigit()) || (lastChar.isDigit() && currentChar.isLetter())); }; for (int i = 0; i < m_string.size(); ++i) { const auto& current_char = m_string.at(i); - if(current_char.isDigit() || current_char.isLetter()){ - if(i>0 && classChange(m_string.at(i-1), current_char)){ - if(!currentSection.isEmpty()){ + if (current_char.isDigit() || current_char.isLetter()) { + if (i > 0 && classChange(m_string.at(i - 1), current_char)) { + if (!currentSection.isEmpty()) { m_sections.append(Section(currentSection)); } currentSection = ""; } currentSection += current_char; - } - else if(current_char == '.' || current_char == '-' || current_char == '_'){ - if(!currentSection.isEmpty()){ + } else if (current_char == '.' || current_char == '-' || current_char == '_') { + if (!currentSection.isEmpty()) { m_sections.append(Section(currentSection)); } currentSection = ""; From 3bec4a80b3de58d31992eda8497a3d099190b92d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Edgars=20C=C4=ABrulis?= Date: Tue, 17 Jan 2023 06:53:01 +0200 Subject: [PATCH 136/199] Version.cpp: Decompose version strings according to flexver MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Rachel Powers <508861+Ryex@users.noreply.github.com> Signed-off-by: Edgars CÄ«rulis --- launcher/Version.cpp | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/launcher/Version.cpp b/launcher/Version.cpp index 0640e6d3c..01f513e34 100644 --- a/launcher/Version.cpp +++ b/launcher/Version.cpp @@ -75,25 +75,20 @@ void Version::parse() { m_sections.clear(); QString currentSection; + auto classChange = [](QChar lastChar, QChar currentChar) { - return ((lastChar.isLetter() && currentChar.isDigit()) || (lastChar.isDigit() && currentChar.isLetter())); + return !lastChar.isNull() && ((!lastChar.isDigit() && currentChar.isDigit()) || (lastChar.isDigit() && !currentChar.isDigit())); }; + for (int i = 0; i < m_string.size(); ++i) { const auto& current_char = m_string.at(i); - if (current_char.isDigit() || current_char.isLetter()) { - if (i > 0 && classChange(m_string.at(i - 1), current_char)) { - if (!currentSection.isEmpty()) { - m_sections.append(Section(currentSection)); - } - currentSection = ""; - } - currentSection += current_char; - } else if (current_char == '.' || current_char == '-' || current_char == '_') { + if ((i > 0 && classChange(m_string.at(i - 1), current_char)) || current_char == '.' || current_char == '-' || current_char == '+') { if (!currentSection.isEmpty()) { m_sections.append(Section(currentSection)); } currentSection = ""; } + currentSection += current_char; } if (!currentSection.isEmpty()) { m_sections.append(Section(currentSection)); From 730f714e973eadf76d2f834a9e062ce5bb44e41f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Edgars=20C=C4=ABrulis?= Date: Tue, 17 Jan 2023 07:33:36 +0200 Subject: [PATCH 137/199] Version.cpp: Remove unnecessary QStringList include MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Edgars CÄ«rulis --- launcher/Version.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/launcher/Version.cpp b/launcher/Version.cpp index 01f513e34..9fdd955b7 100644 --- a/launcher/Version.cpp +++ b/launcher/Version.cpp @@ -1,6 +1,5 @@ #include "Version.h" -#include #include #include #include From ad74fedfba45fe0f36ff387e586b21d4ede8ca83 Mon Sep 17 00:00:00 2001 From: flow Date: Tue, 17 Jan 2023 22:51:54 -0300 Subject: [PATCH 138/199] 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 80bba02fc..5d9068517 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 139/199] 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 a890013ef..190d48d8f 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 9934537e19c7ce6f9bf926cc8abba023297b0a40 Mon Sep 17 00:00:00 2001 From: Rachel Powers <508861+Ryex@users.noreply.github.com> Date: Thu, 19 Jan 2023 09:46:35 +0200 Subject: [PATCH 140/199] feat: add debug printing for Version Signed-off-by: Rachel Powers <508861+Ryex@users.noreply.github.com> --- launcher/Version.cpp | 18 ++++++++++++++++++ launcher/Version.h | 7 +++++++ tests/CMakeLists.txt | 3 +++ tests/Version_test.cpp | 3 ++- 4 files changed, 30 insertions(+), 1 deletion(-) diff --git a/launcher/Version.cpp b/launcher/Version.cpp index 9fdd955b7..2129ebfd7 100644 --- a/launcher/Version.cpp +++ b/launcher/Version.cpp @@ -1,5 +1,6 @@ #include "Version.h" +#include #include #include #include @@ -93,3 +94,20 @@ void Version::parse() m_sections.append(Section(currentSection)); } } + + +/// qDebug print support for the BlockedMod struct +QDebug operator<<(QDebug debug, const Version& v) +{ + QDebugStateSaver saver(debug); + + debug.nospace() << "Version{ string: " << v.toString() << ", sections: [ "; + + for (auto s : v.m_sections) { + debug.nospace() << s.m_fullString << ", "; + } + + debug.nospace() << " ]" << " }"; + + return debug; +} \ No newline at end of file diff --git a/launcher/Version.h b/launcher/Version.h index aceb7a073..c09273744 100644 --- a/launcher/Version.h +++ b/launcher/Version.h @@ -35,6 +35,7 @@ #pragma once +#include #include #include #include @@ -59,6 +60,8 @@ public: return m_string; } + friend QDebug operator<<(QDebug debug, const Version& v); + private: QString m_string; struct Section @@ -143,7 +146,11 @@ private: } } }; + + QList
    m_sections; void parse(); }; + + diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 9f84a9a7b..0f716a751 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -53,3 +53,6 @@ ecm_add_test(Packwiz_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR ecm_add_test(Index_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test TEST_NAME Index) + +ecm_add_test(Version_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test + TEST_NAME Version) diff --git a/tests/Version_test.cpp b/tests/Version_test.cpp index 734528b7e..6836b6fa0 100644 --- a/tests/Version_test.cpp +++ b/tests/Version_test.cpp @@ -15,7 +15,6 @@ #include -#include #include class ModUtilsTest : public QObject @@ -74,6 +73,8 @@ private slots: const auto v1 = Version(first); const auto v2 = Version(second); + qDebug() << v1 << "vs" << v2; + QCOMPARE(v1 < v2, lessThan); QCOMPARE(v1 > v2, !lessThan && !equal); QCOMPARE(v1 == v2, equal); From 7ed993b54e20d74c000a29720bc9317ad4849ed0 Mon Sep 17 00:00:00 2001 From: Rachel Powers <508861+Ryex@users.noreply.github.com> Date: Wed, 18 Jan 2023 10:11:53 -0700 Subject: [PATCH 141/199] fix: proper null padded version comparison Signed-off-by: Rachel Powers <508861+Ryex@users.noreply.github.com> --- launcher/Version.cpp | 17 ++++++++++------- launcher/Version.h | 27 ++++++++++++++++++++++++++- 2 files changed, 36 insertions(+), 8 deletions(-) diff --git a/launcher/Version.cpp b/launcher/Version.cpp index 2129ebfd7..d59339e7b 100644 --- a/launcher/Version.cpp +++ b/launcher/Version.cpp @@ -15,9 +15,9 @@ bool Version::operator<(const Version &other) const const int size = qMax(m_sections.size(), other.m_sections.size()); for (int i = 0; i < size; ++i) { - const Section sec1 = (i >= m_sections.size()) ? Section("0") : m_sections.at(i); + const Section sec1 = (i >= m_sections.size()) ? Section("") : m_sections.at(i); const Section sec2 = - (i >= other.m_sections.size()) ? Section("0") : other.m_sections.at(i); + (i >= other.m_sections.size()) ? Section("") : other.m_sections.at(i); if (sec1 != sec2) { return sec1 < sec2; @@ -35,9 +35,9 @@ bool Version::operator>(const Version &other) const const int size = qMax(m_sections.size(), other.m_sections.size()); for (int i = 0; i < size; ++i) { - const Section sec1 = (i >= m_sections.size()) ? Section("0") : m_sections.at(i); + const Section sec1 = (i >= m_sections.size()) ? Section("") : m_sections.at(i); const Section sec2 = - (i >= other.m_sections.size()) ? Section("0") : other.m_sections.at(i); + (i >= other.m_sections.size()) ? Section("") : other.m_sections.at(i); if (sec1 != sec2) { return sec1 > sec2; @@ -55,9 +55,9 @@ bool Version::operator==(const Version &other) const const int size = qMax(m_sections.size(), other.m_sections.size()); for (int i = 0; i < size; ++i) { - const Section sec1 = (i >= m_sections.size()) ? Section("0") : m_sections.at(i); + const Section sec1 = (i >= m_sections.size()) ? Section("") : m_sections.at(i); const Section sec2 = - (i >= other.m_sections.size()) ? Section("0") : other.m_sections.at(i); + (i >= other.m_sections.size()) ? Section("") : other.m_sections.at(i); if (sec1 != sec2) { return false; @@ -103,8 +103,11 @@ QDebug operator<<(QDebug debug, const Version& v) debug.nospace() << "Version{ string: " << v.toString() << ", sections: [ "; + bool first = true; for (auto s : v.m_sections) { - debug.nospace() << s.m_fullString << ", "; + if (!first) debug.nospace() << ", "; + debug.nospace() << s.m_fullString; + first = false; } debug.nospace() << " ]" << " }"; diff --git a/launcher/Version.h b/launcher/Version.h index c09273744..1f1bea833 100644 --- a/launcher/Version.h +++ b/launcher/Version.h @@ -69,6 +69,7 @@ private: explicit Section(const QString &fullString) { m_fullString = fullString; + m_isNull = true; int cutoff = m_fullString.size(); for(int i = 0; i < m_fullString.size(); i++) { @@ -86,6 +87,7 @@ private: if(numPart.size()) { numValid = true; + m_isNull = false; m_numPart = numPart.toInt(); } #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) @@ -95,6 +97,7 @@ private: #endif if(stringPart.size()) { + m_isNull = false; m_stringPart = stringPart.toString(); } } @@ -103,9 +106,17 @@ private: int m_numPart = 0; QString m_stringPart; QString m_fullString; + bool m_isNull; inline bool operator!=(const Section &other) const { + if (m_isNull && other.numValid) { + return 0 != other.m_numPart; + } else if (numValid && other.m_isNull) { + return m_numPart != 0; + } else if (m_isNull || other.m_isNull) { + return false; + } if(numValid && other.numValid) { return m_numPart != other.m_numPart || m_stringPart != other.m_stringPart; @@ -116,7 +127,14 @@ private: } } inline bool operator<(const Section &other) const - { + { + if (m_isNull && other.numValid) { + return 0 < other.m_numPart; + } else if (numValid && other.m_isNull) { + return m_numPart < 0; + } else if (m_isNull || other.m_isNull) { + return true; + } if(numValid && other.numValid) { if(m_numPart < other.m_numPart) @@ -132,6 +150,13 @@ private: } inline bool operator>(const Section &other) const { + if (m_isNull && other.numValid) { + return 0 > other.m_numPart; + } else if (numValid && other.m_isNull) { + return m_numPart > 0; + } else if (m_isNull || other.m_isNull) { + return false; + } if(numValid && other.numValid) { if(m_numPart > other.m_numPart) From f49ad2ee03974c9fe94882d99d1a2bee67b87285 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Edgars=20C=C4=ABrulis?= Date: Thu, 19 Jan 2023 10:39:57 +0200 Subject: [PATCH 142/199] Version.h: Fix comparison of null version in Version class MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Rachel Powers <508861+Ryex@users.noreply.github.com> Signed-off-by: Edgars CÄ«rulis --- launcher/Version.h | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/launcher/Version.h b/launcher/Version.h index 1f1bea833..9db03521f 100644 --- a/launcher/Version.h +++ b/launcher/Version.h @@ -115,7 +115,8 @@ private: } else if (numValid && other.m_isNull) { return m_numPart != 0; } else if (m_isNull || other.m_isNull) { - return false; + if ((m_stringPart == ".") || (other.m_stringPart == ".")) return false; + return true; } if(numValid && other.numValid) { From 0199d8a74fbd76f3f37c02e4702dd5bed09fad93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Edgars=20C=C4=ABrulis?= Date: Thu, 19 Jan 2023 14:11:45 +0200 Subject: [PATCH 143/199] Version.cpp: Add new line MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Edgars CÄ«rulis --- launcher/Version.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/launcher/Version.cpp b/launcher/Version.cpp index d59339e7b..9b96f68e5 100644 --- a/launcher/Version.cpp +++ b/launcher/Version.cpp @@ -113,4 +113,4 @@ QDebug operator<<(QDebug debug, const Version& v) debug.nospace() << " ]" << " }"; return debug; -} \ No newline at end of file +} From 5ae69c079a15fa16945b306e29925e800cb28c87 Mon Sep 17 00:00:00 2001 From: flow Date: Thu, 19 Jan 2023 21:18:39 -0300 Subject: [PATCH 144/199] feat(tests): add FlexVer test vector to the Version tests Signed-off-by: flow --- tests/Version_test.cpp | 103 ++++++++++++++++++++---- tests/testdata/Version/test_vectors.txt | 63 +++++++++++++++ 2 files changed, 152 insertions(+), 14 deletions(-) create mode 100644 tests/testdata/Version/test_vectors.txt diff --git a/tests/Version_test.cpp b/tests/Version_test.cpp index 6836b6fa0..bb0a7f5a6 100644 --- a/tests/Version_test.cpp +++ b/tests/Version_test.cpp @@ -17,15 +17,20 @@ #include -class ModUtilsTest : public QObject -{ +class VersionTest : public QObject { Q_OBJECT - void setupVersions() + + void addDataColumns() { QTest::addColumn("first"); QTest::addColumn("second"); QTest::addColumn("lessThan"); QTest::addColumn("equal"); + } + + void setupVersions() + { + addDataColumns(); QTest::newRow("equal, explicit") << "1.2.0" << "1.2.0" << false << true; QTest::newRow("equal, implicit 1") << "1.2" << "1.2.0" << false << true; @@ -49,20 +54,12 @@ class ModUtilsTest : public QObject QTest::newRow("greaterThan, two-digit") << "1.42" << "1.41" << false << false; } -private slots: - void initTestCase() - { - - } - void cleanupTestCase() - { - - } - + private slots: void test_versionCompare_data() { setupVersions(); } + void test_versionCompare() { QFETCH(QString, first); @@ -79,8 +76,86 @@ private slots: QCOMPARE(v1 > v2, !lessThan && !equal); QCOMPARE(v1 == v2, equal); } + + void test_flexVerTestVector_data() + { + addDataColumns(); + + QDir test_vector_dir(QFINDTESTDATA("testdata/Version")); + + QFile vector_file{test_vector_dir.absoluteFilePath("test_vectors.txt")}; + + vector_file.open(QFile::OpenModeFlag::ReadOnly); + + int test_number = 0; + const QString test_name_template { "FlexVer test #%1 (%2)" }; + for (auto line = vector_file.readLine(); !vector_file.atEnd(); line = vector_file.readLine()) { + line = line.simplified(); + if (line.startsWith('#') || line.isEmpty()) + continue; + + test_number += 1; + + auto split_line = line.split('<'); + if (split_line.size() == 2) { + QString first{split_line.first().simplified()}; + QString second{split_line.last().simplified()}; + + auto new_test_name = test_name_template.arg(QString::number(test_number), "lessThan").toLatin1().data(); + QTest::newRow(new_test_name) << first << second << true << false; + + continue; + } + + split_line = line.split('='); + if (split_line.size() == 2) { + QString first{split_line.first().simplified()}; + QString second{split_line.last().simplified()}; + + auto new_test_name = test_name_template.arg(QString::number(test_number), "equals").toLatin1().data(); + QTest::newRow(new_test_name) << first << second << false << true; + + continue; + } + + split_line = line.split('>'); + if (split_line.size() == 2) { + QString first{split_line.first().simplified()}; + QString second{split_line.last().simplified()}; + + auto new_test_name = test_name_template.arg(QString::number(test_number), "greaterThan").toLatin1().data(); + QTest::newRow(new_test_name) << first << second << false << false; + + continue; + } + + qCritical() << "Unexpected separator in the test vector: "; + qCritical() << line; + + QVERIFY(0 != 0); + } + + vector_file.close(); + } + + void test_flexVerTestVector() + { + QFETCH(QString, first); + QFETCH(QString, second); + QFETCH(bool, lessThan); + QFETCH(bool, equal); + + const auto v1 = Version(first); + const auto v2 = Version(second); + + qDebug() << v1 << "vs" << v2; + + QCOMPARE(v1 < v2, lessThan); + QCOMPARE(v1 > v2, !lessThan && !equal); + QCOMPARE(v1 == v2, equal); + } }; -QTEST_GUILESS_MAIN(ModUtilsTest) +QTEST_GUILESS_MAIN(VersionTest) #include "Version_test.moc" diff --git a/tests/testdata/Version/test_vectors.txt b/tests/testdata/Version/test_vectors.txt new file mode 100644 index 000000000..e6c6507cf --- /dev/null +++ b/tests/testdata/Version/test_vectors.txt @@ -0,0 +1,63 @@ +# Test vector from: +# https://github.com/unascribed/FlexVer/blob/704e12759b6e59220ff888f8bf2ec15b8f8fd969/test/test_vectors.txt +# +# This test file is formatted as " ", seperated by the space character +# Implementations should ignore lines starting with "#" and lines that have a length of 0 + +# Basic numeric ordering (lexical string sort fails these) +10 > 2 +100 > 10 + +# Trivial common numerics +1.0 < 1.1 +1.0 < 1.0.1 +1.1 > 1.0.1 + +# SemVer compatibility +1.5 > 1.5-pre1 +1.5 = 1.5+foobar + +# SemVer incompatibility +1.5 < 1.5-2 +1.5-pre10 > 1.5-pre2 + +# Empty strings + = +1 > + < 1 + +# Check boundary between textual and prerelease +a-a < a + +# Check boundary between textual and appendix +a+a = a + +# Dash is included in prerelease comparison (if stripped it will be a smaller component) +# Note that a-a < a=a regardless since the prerelease splits the component creating a smaller first component; 0 is added to force splitting regardless +a0-a < a0=a + +# Pre-releases must contain only non-digit +1.16.5-10 > 1.16.5 + +# Pre-releases can have multiple dashes (should not be split) +# Reasoning for test data: "p-a!" > "p-a-" (correct); "p-a!" < "p-a t-" (what happens if every dash creates a new component) +-a- > -a! + +# Misc +b1.7.3 > a1.2.6 +b1.2.6 > a1.7.3 +a1.1.2 < a1.1.2_01 +1.16.5-0.00.5 > 1.14.2-1.3.7 +1.0.0 < 1.0.0_01 +1.0.1 > 1.0.0_01 +1.0.0_01 < 1.0.1 +0.17.1-beta.1 < 0.17.1 +0.17.1-beta.1 < 0.17.1-beta.2 +1.4.5_01 = 1.4.5_01+fabric-1.17 +1.4.5_01 = 1.4.5_01+fabric-1.17+ohgod +14w16a < 18w40b +18w40a < 18w40b +1.4.5_01+fabric-1.17 < 18w40b +13w02a < c0.3.0_01 +0.6.0-1.18.x < 0.9.beta-1.18.x + From 81848e05f100a135ad1d307ccabb796be0540daa Mon Sep 17 00:00:00 2001 From: flow Date: Thu, 19 Jan 2023 21:31:55 -0300 Subject: [PATCH 145/199] refactor: simplify Version operators Signed-off-by: flow --- launcher/Version.cpp | 66 +++++++++++++++++--------------------------- launcher/Version.h | 4 +-- 2 files changed, 28 insertions(+), 42 deletions(-) diff --git a/launcher/Version.cpp b/launcher/Version.cpp index 9b96f68e5..9307aab36 100644 --- a/launcher/Version.cpp +++ b/launcher/Version.cpp @@ -1,67 +1,41 @@ #include "Version.h" #include -#include #include #include +#include -Version::Version(const QString &str) : m_string(str) +Version::Version(QString str) : m_string(std::move(str)) { parse(); } -bool Version::operator<(const Version &other) const +bool Version::operator<(const Version& other) const { - const int size = qMax(m_sections.size(), other.m_sections.size()); - for (int i = 0; i < size; ++i) - { - const Section sec1 = (i >= m_sections.size()) ? Section("") : m_sections.at(i); + const auto size = qMax(m_sections.size(), other.m_sections.size()); + for (int i = 0; i < size; ++i) { + const Section sec1 = + (i >= m_sections.size()) ? Section("") : m_sections.at(i); const Section sec2 = (i >= other.m_sections.size()) ? Section("") : other.m_sections.at(i); + if (sec1 != sec2) - { return sec1 < sec2; - } } return false; } -bool Version::operator<=(const Version &other) const +bool Version::operator==(const Version& other) const { - return *this < other || *this == other; -} -bool Version::operator>(const Version &other) const -{ - const int size = qMax(m_sections.size(), other.m_sections.size()); - for (int i = 0; i < size; ++i) - { - const Section sec1 = (i >= m_sections.size()) ? Section("") : m_sections.at(i); + const auto size = qMax(m_sections.size(), other.m_sections.size()); + for (int i = 0; i < size; ++i) { + const Section sec1 = + (i >= m_sections.size()) ? Section("") : m_sections.at(i); const Section sec2 = (i >= other.m_sections.size()) ? Section("") : other.m_sections.at(i); - if (sec1 != sec2) - { - return sec1 > sec2; - } - } - return false; -} -bool Version::operator>=(const Version &other) const -{ - return *this > other || *this == other; -} -bool Version::operator==(const Version &other) const -{ - const int size = qMax(m_sections.size(), other.m_sections.size()); - for (int i = 0; i < size; ++i) - { - const Section sec1 = (i >= m_sections.size()) ? Section("") : m_sections.at(i); - const Section sec2 = - (i >= other.m_sections.size()) ? Section("") : other.m_sections.at(i); if (sec1 != sec2) - { return false; - } } return true; @@ -70,6 +44,18 @@ bool Version::operator!=(const Version &other) const { return !operator==(other); } +bool Version::operator<=(const Version &other) const +{ + return *this < other || *this == other; +} +bool Version::operator>(const Version &other) const +{ + return !(*this <= other); +} +bool Version::operator>=(const Version &other) const +{ + return !(*this < other); +} void Version::parse() { @@ -96,7 +82,7 @@ void Version::parse() } -/// qDebug print support for the BlockedMod struct +/// qDebug print support for the Version class QDebug operator<<(QDebug debug, const Version& v) { QDebugStateSaver saver(debug); diff --git a/launcher/Version.h b/launcher/Version.h index 9db03521f..b587319ab 100644 --- a/launcher/Version.h +++ b/launcher/Version.h @@ -45,8 +45,8 @@ class QUrl; class Version { public: - Version(const QString &str); - Version() {} + Version(QString str); + Version() = default; bool operator<(const Version &other) const; bool operator<=(const Version &other) const; From bcebb1920ff5df4f2a311984b296bfd8d5969997 Mon Sep 17 00:00:00 2001 From: flow Date: Thu, 19 Jan 2023 21:59:33 -0300 Subject: [PATCH 146/199] refactor: clean up Section struct Signed-off-by: flow --- launcher/Version.h | 128 +++++++++++++++++++-------------------------- 1 file changed, 53 insertions(+), 75 deletions(-) diff --git a/launcher/Version.h b/launcher/Version.h index b587319ab..23481c29c 100644 --- a/launcher/Version.h +++ b/launcher/Version.h @@ -36,15 +36,14 @@ #pragma once #include +#include #include #include -#include class QUrl; -class Version -{ -public: +class Version { + public: Version(QString str); Version() = default; @@ -55,125 +54,104 @@ public: bool operator==(const Version &other) const; bool operator!=(const Version &other) const; - QString toString() const - { - return m_string; - } + QString toString() const { return m_string; } friend QDebug operator<<(QDebug debug, const Version& v); -private: - QString m_string; - struct Section - { - explicit Section(const QString &fullString) + private: + struct Section { + explicit Section(QString fullString) : m_isNull(true), m_fullString(std::move(fullString)) { - m_fullString = fullString; - m_isNull = true; int cutoff = m_fullString.size(); - for(int i = 0; i < m_fullString.size(); i++) - { - if(!m_fullString[i].isDigit()) - { + for (int i = 0; i < m_fullString.size(); i++) { + if (!m_fullString[i].isDigit()) { cutoff = i; break; } } + #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) auto numPart = QStringView{m_fullString}.left(cutoff); #else auto numPart = m_fullString.leftRef(cutoff); #endif - if(numPart.size()) - { - numValid = true; + + if (!numPart.isEmpty()) { m_isNull = false; m_numPart = numPart.toInt(); } + #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) auto stringPart = QStringView{m_fullString}.mid(cutoff); #else auto stringPart = m_fullString.midRef(cutoff); #endif - if(stringPart.size()) - { + + if (!stringPart.isEmpty()) { m_isNull = false; m_stringPart = stringPart.toString(); } } - explicit Section() {} - bool numValid = false; + + explicit Section() = default; + + bool m_isNull = false; int m_numPart = 0; + QString m_stringPart; QString m_fullString; - bool m_isNull; - inline bool operator!=(const Section &other) const + inline bool operator==(const Section& other) const { - if (m_isNull && other.numValid) { - return 0 != other.m_numPart; - } else if (numValid && other.m_isNull) { - return m_numPart != 0; - } else if (m_isNull || other.m_isNull) { - if ((m_stringPart == ".") || (other.m_stringPart == ".")) return false; - return true; - } - if(numValid && other.numValid) - { - return m_numPart != other.m_numPart || m_stringPart != other.m_stringPart; - } - else - { - return m_fullString != other.m_fullString; - } + if (m_isNull && !other.m_isNull) + return other.m_numPart == 0; + + if (!m_isNull && other.m_isNull) + return m_numPart == 0; + + if (m_isNull || other.m_isNull) + return (m_stringPart == ".") || (other.m_stringPart == "."); + + if (!m_isNull && !other.m_isNull) + return (m_numPart == other.m_numPart) && (m_stringPart == other.m_stringPart); + + return m_fullString == other.m_fullString; } + inline bool operator<(const Section &other) const { - if (m_isNull && other.numValid) { - return 0 < other.m_numPart; - } else if (numValid && other.m_isNull) { + if (m_isNull && !other.m_isNull) + return other.m_numPart > 0; + + if (!m_isNull && other.m_isNull) return m_numPart < 0; - } else if (m_isNull || other.m_isNull) { + + if (m_isNull || other.m_isNull) return true; - } - if(numValid && other.numValid) - { + + if (!m_isNull && !other.m_isNull) { if(m_numPart < other.m_numPart) return true; if(m_numPart == other.m_numPart && m_stringPart < other.m_stringPart) return true; return false; } - else - { - return m_fullString < other.m_fullString; - } + + return m_fullString < other.m_fullString; + } + + inline bool operator!=(const Section& other) const + { + return !(*this == other); } inline bool operator>(const Section &other) const { - if (m_isNull && other.numValid) { - return 0 > other.m_numPart; - } else if (numValid && other.m_isNull) { - return m_numPart > 0; - } else if (m_isNull || other.m_isNull) { - return false; - } - if(numValid && other.numValid) - { - if(m_numPart > other.m_numPart) - return true; - if(m_numPart == other.m_numPart && m_stringPart > other.m_stringPart) - return true; - return false; - } - else - { - return m_fullString > other.m_fullString; - } + return !(*this < other || *this == other); } }; - + private: + QString m_string; QList
    m_sections; void parse(); From cdc9f93f712081c45f661500e9e6a719eed09b6e Mon Sep 17 00:00:00 2001 From: Tayou Date: Fri, 20 Jan 2023 15:13:25 +0100 Subject: [PATCH 147/199] 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 4991f4cc1..2cd077f88 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 4e830b6cb..655e7df03 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 6bf5f4288..84b5325a5 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 69a8e3df4..d8b442fd3 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 148/199] 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 5d9068517..6649b7248 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 149/199] 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 428be563b..cee2038f4 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 a0a7896cb..4004d4151 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 150/199] 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 65f586675..093f44d38 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 655e7df03..30bbf6854 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 84b5325a5..fab21a8f1 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 000000000..6078ecbf4 --- /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 cee2038f4..a029b0a88 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 151/199] 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 30bbf6854..69ef30167 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 6078ecbf4..218f0a2a2 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 a029b0a88..717958fd3 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 4004d4151..59bda5140 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 152/199] 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 69ef30167..ca6827e0d 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 717958fd3..4f81f444d 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 153/199] 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 ca6827e0d..d89ab97c3 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 154/199] 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 d89ab97c3..1fc948073 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 155/199] 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 1fc948073..ae458b38c 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 4f81f444d..540d599dd 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 59bda5140..c47f3a596 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 156/199] 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 ae458b38c..dbbaa2b03 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 157/199] 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 218f0a2a2..8437cb2e6 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 158/199] 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 dbbaa2b03..79e01d917 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 8437cb2e6..42f70996b 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 159/199] 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 79e01d917..a51cd55f8 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 445f9e5f717bf1ad9b764704b320bbec237a7682 Mon Sep 17 00:00:00 2001 From: flow Date: Fri, 20 Jan 2023 11:11:35 -0300 Subject: [PATCH 160/199] feat+fix(Version): make comparsion FlexVer-compatible ... and fixes a minor issue in the parsing. This changes the expected behavior of Versions in one significant way: Now, Versions like 1.2 or 1.5 evaluate to LESS THAN 1.2.0 and 1.5.0 respectively. This makes sense for sorting versions, since one expects the versions without patch release to 'contain' the ones with, so the ones without should be evaluated uniformily with the ones with the patch. Signed-off-by: flow --- launcher/Version.cpp | 94 +++++++++++++++++++++++++++--------------- launcher/Version.h | 66 ++++++++++++++++------------- tests/Version_test.cpp | 16 +++---- 3 files changed, 106 insertions(+), 70 deletions(-) diff --git a/launcher/Version.cpp b/launcher/Version.cpp index 9307aab36..e4311f314 100644 --- a/launcher/Version.cpp +++ b/launcher/Version.cpp @@ -10,49 +10,63 @@ Version::Version(QString str) : m_string(std::move(str)) parse(); } +#define VERSION_OPERATOR(return_on_different) \ + bool exclude_our_sections = false; \ + bool exclude_their_sections = false; \ + \ + const auto size = qMax(m_sections.size(), other.m_sections.size()); \ + for (int i = 0; i < size; ++i) { \ + Section sec1 = (i >= m_sections.size()) ? Section() : m_sections.at(i); \ + Section sec2 = (i >= other.m_sections.size()) ? Section() : other.m_sections.at(i); \ + \ + { /* Don't include appendixes in the comparison */ \ + if (sec1.isAppendix()) \ + exclude_our_sections = true; \ + if (sec2.isAppendix()) \ + exclude_their_sections = true; \ + \ + if (exclude_our_sections) { \ + sec1 = Section(); \ + if (sec2.m_isNull) \ + break; \ + } \ + \ + if (exclude_their_sections) { \ + sec2 = Section(); \ + if (sec1.m_isNull) \ + break; \ + } \ + } \ + \ + if (sec1 != sec2) \ + return return_on_different; \ + } + bool Version::operator<(const Version& other) const { - const auto size = qMax(m_sections.size(), other.m_sections.size()); - for (int i = 0; i < size; ++i) { - const Section sec1 = - (i >= m_sections.size()) ? Section("") : m_sections.at(i); - const Section sec2 = - (i >= other.m_sections.size()) ? Section("") : other.m_sections.at(i); - - if (sec1 != sec2) - return sec1 < sec2; - } + VERSION_OPERATOR(sec1 < sec2) return false; } bool Version::operator==(const Version& other) const { - const auto size = qMax(m_sections.size(), other.m_sections.size()); - for (int i = 0; i < size; ++i) { - const Section sec1 = - (i >= m_sections.size()) ? Section("") : m_sections.at(i); - const Section sec2 = - (i >= other.m_sections.size()) ? Section("") : other.m_sections.at(i); - - if (sec1 != sec2) - return false; - } + VERSION_OPERATOR(false) return true; } -bool Version::operator!=(const Version &other) const +bool Version::operator!=(const Version& other) const { return !operator==(other); } -bool Version::operator<=(const Version &other) const +bool Version::operator<=(const Version& other) const { return *this < other || *this == other; } -bool Version::operator>(const Version &other) const +bool Version::operator>(const Version& other) const { return !(*this <= other); } -bool Version::operator>=(const Version &other) const +bool Version::operator>=(const Version& other) const { return !(*this < other); } @@ -62,25 +76,37 @@ void Version::parse() m_sections.clear(); QString currentSection; - auto classChange = [](QChar lastChar, QChar currentChar) { - return !lastChar.isNull() && ((!lastChar.isDigit() && currentChar.isDigit()) || (lastChar.isDigit() && !currentChar.isDigit())); + if (m_string.isEmpty()) + return; + + auto classChange = [&](QChar lastChar, QChar currentChar) { + if (lastChar.isNull()) + return false; + if (lastChar.isDigit() != currentChar.isDigit()) + return true; + + const QList s_separators{ '.', '-', '+' }; + if (s_separators.contains(currentChar) && currentSection.at(0) != currentChar) + return true; + + return false; }; - for (int i = 0; i < m_string.size(); ++i) { + currentSection += m_string.at(0); + for (int i = 1; i < m_string.size(); ++i) { const auto& current_char = m_string.at(i); - if ((i > 0 && classChange(m_string.at(i - 1), current_char)) || current_char == '.' || current_char == '-' || current_char == '+') { - if (!currentSection.isEmpty()) { + if (classChange(m_string.at(i - 1), current_char)) { + if (!currentSection.isEmpty()) m_sections.append(Section(currentSection)); - } currentSection = ""; } + currentSection += current_char; } - if (!currentSection.isEmpty()) { - m_sections.append(Section(currentSection)); - } -} + if (!currentSection.isEmpty()) + m_sections.append(Section(currentSection)); +} /// qDebug print support for the Version class QDebug operator<<(QDebug debug, const Version& v) diff --git a/launcher/Version.h b/launcher/Version.h index 23481c29c..659f8e54e 100644 --- a/launcher/Version.h +++ b/launcher/Version.h @@ -1,6 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-only /* * PolyMC - Minecraft Launcher + * Copyright (C) 2023 flowln * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify @@ -60,7 +61,7 @@ class Version { private: struct Section { - explicit Section(QString fullString) : m_isNull(true), m_fullString(std::move(fullString)) + explicit Section(QString fullString) : m_fullString(std::move(fullString)) { int cutoff = m_fullString.size(); for (int i = 0; i < m_fullString.size(); i++) { @@ -95,45 +96,54 @@ class Version { explicit Section() = default; - bool m_isNull = false; - int m_numPart = 0; + bool m_isNull = true; + int m_numPart = 0; QString m_stringPart; + QString m_fullString; + [[nodiscard]] inline bool isAppendix() const { return m_stringPart.startsWith('+'); } + [[nodiscard]] inline bool isPreRelease() const { return m_stringPart.startsWith('-') && m_stringPart.length() > 1; } + inline bool operator==(const Section& other) const { if (m_isNull && !other.m_isNull) - return other.m_numPart == 0; - + return false; if (!m_isNull && other.m_isNull) - return m_numPart == 0; - - if (m_isNull || other.m_isNull) - return (m_stringPart == ".") || (other.m_stringPart == "."); - - if (!m_isNull && !other.m_isNull) - return (m_numPart == other.m_numPart) && (m_stringPart == other.m_stringPart); - - return m_fullString == other.m_fullString; - } - - inline bool operator<(const Section &other) const - { - if (m_isNull && !other.m_isNull) - return other.m_numPart > 0; - - if (!m_isNull && other.m_isNull) - return m_numPart < 0; - - if (m_isNull || other.m_isNull) - return true; + return false; if (!m_isNull && !other.m_isNull) { - if(m_numPart < other.m_numPart) + return (m_numPart == other.m_numPart) && (m_stringPart == other.m_stringPart); + } + + return true; + } + + inline bool operator<(const Section& other) const + { + static auto unequal_is_less = [](Section const& non_null) -> bool { + if (non_null.m_stringPart.isEmpty()) + return non_null.m_numPart == 0; + return (non_null.m_stringPart != QLatin1Char('.')) && non_null.isPreRelease(); + }; + + if (!m_isNull && other.m_isNull) + return unequal_is_less(*this); + if (m_isNull && !other.m_isNull) + return !unequal_is_less(other); + + if (!m_isNull && !other.m_isNull) { + if (m_numPart < other.m_numPart) return true; - if(m_numPart == other.m_numPart && m_stringPart < other.m_stringPart) + if (m_numPart == other.m_numPart && m_stringPart < other.m_stringPart) return true; + + if (!m_stringPart.isEmpty() && other.m_stringPart.isEmpty()) + return false; + if (m_stringPart.isEmpty() && !other.m_stringPart.isEmpty()) + return true; + return false; } diff --git a/tests/Version_test.cpp b/tests/Version_test.cpp index bb0a7f5a6..afb4c6102 100644 --- a/tests/Version_test.cpp +++ b/tests/Version_test.cpp @@ -33,24 +33,24 @@ class VersionTest : public QObject { addDataColumns(); QTest::newRow("equal, explicit") << "1.2.0" << "1.2.0" << false << true; - QTest::newRow("equal, implicit 1") << "1.2" << "1.2.0" << false << true; - QTest::newRow("equal, implicit 2") << "1.2.0" << "1.2" << false << true; QTest::newRow("equal, two-digit") << "1.42" << "1.42" << false << true; QTest::newRow("lessThan, explicit 1") << "1.2.0" << "1.2.1" << true << false; QTest::newRow("lessThan, explicit 2") << "1.2.0" << "1.3.0" << true << false; QTest::newRow("lessThan, explicit 3") << "1.2.0" << "2.2.0" << true << false; - QTest::newRow("lessThan, implicit 1") << "1.2" << "1.2.1" << true << false; - QTest::newRow("lessThan, implicit 2") << "1.2" << "1.3.0" << true << false; - QTest::newRow("lessThan, implicit 3") << "1.2" << "2.2.0" << true << false; + QTest::newRow("lessThan, implicit 1") << "1.2" << "1.2.0" << true << false; + QTest::newRow("lessThan, implicit 2") << "1.2" << "1.2.1" << true << false; + QTest::newRow("lessThan, implicit 3") << "1.2" << "1.3.0" << true << false; + QTest::newRow("lessThan, implicit 4") << "1.2" << "2.2.0" << true << false; QTest::newRow("lessThan, two-digit") << "1.41" << "1.42" << true << false; QTest::newRow("greaterThan, explicit 1") << "1.2.1" << "1.2.0" << false << false; QTest::newRow("greaterThan, explicit 2") << "1.3.0" << "1.2.0" << false << false; QTest::newRow("greaterThan, explicit 3") << "2.2.0" << "1.2.0" << false << false; - QTest::newRow("greaterThan, implicit 1") << "1.2.1" << "1.2" << false << false; - QTest::newRow("greaterThan, implicit 2") << "1.3.0" << "1.2" << false << false; - QTest::newRow("greaterThan, implicit 3") << "2.2.0" << "1.2" << false << false; + QTest::newRow("greaterThan, implicit 1") << "1.2.0" << "1.2" << false << false; + QTest::newRow("greaterThan, implicit 2") << "1.2.1" << "1.2" << false << false; + QTest::newRow("greaterThan, implicit 3") << "1.3.0" << "1.2" << false << false; + QTest::newRow("greaterThan, implicit 4") << "2.2.0" << "1.2" << false << false; QTest::newRow("greaterThan, two-digit") << "1.42" << "1.41" << false << false; } From 2a949fcb867ca86594481780101edb37409e8198 Mon Sep 17 00:00:00 2001 From: TheLastRar Date: Sun, 8 Jan 2023 18:12:14 +0000 Subject: [PATCH 161/199] 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 2194317b3..f32a73d1c 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 162/199] 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 8c9dae6fa..98bcf2592 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 5ce39d69d..73ad2d070 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 dfb3652c6..34f337791 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 c8981585d..57f70047a 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 8e7ed7271..06d749e6d 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 88bbc0457..ac994c31d 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 ab5586fd0..94813bec8 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 028480a93..0c601d222 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 cba3afc8c..dda273032 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 8d56d894d..dc983d9a9 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 55782ba77..1ac6fc038 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 433c7b10a..3ffe6cb06 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 1fac90409..5d4a77859 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 d57e748b0..04be43ada 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 a3aab1dee..c3b58cd63 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 eb723159a..8af701045 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 7e8133730..610b631c0 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 bfa7e33d9..bbd465bc1 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 71fc6593b..1896d53ea 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 a1cd1f260..de1f2122d 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 47fbbe1ac..625a2a7d2 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 e34be7fd7..485431a7b 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 12b51aa95..b21a53ad1 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 06b72fd06..73d551330 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 2511f5e59..56cab1466 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 45902d16e..b82f800ed 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 a263bd44d..be38eff11 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 163/199] 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 f32a73d1c..37bb49ba7 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 164/199] 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 5f70ab945..537ffb682 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 165/199] 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 537ffb682..608fc618c 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 166/199] 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 a8da5a2f2..5ba25ff40 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 167/199] 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 608fc618c..6a798822a 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 168/199] 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 a52c5db34..fdfb434bb 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 169/199] 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 190d48d8f..3cc37b2a2 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 170/199] 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 d4a1284f5..387f735ce 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 6b3fd296f..70bf5784a 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 9741fd95a..070ee283c 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 ec4660966..a1c64b433 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 e2f0aa002..b29af8576 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 7aeb61bf7..f01875861 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 de4e1012d..97815eba8 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 15062c2b4..16fdfdb1c 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 6db21622e..d55bc17f2 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 d0a5ed314..8a814cbfa 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 3a3aa8643..07ad48823 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 42021b3c9..da7c1d840 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 67b418f4a..731cd0ba6 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 73d570f18..48cf5d428 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 416b8f2c9..f1987e0c1 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 b86b0936a..5900ea988 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 fc614a8c7..d5c632715 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 ebac707da..da4bd091f 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 561f6202e..5a32cfaf7 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 2fce2942e..3ee7e2e0f 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 78ef4386d..3677a1dca 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 dd2466654..8ccb0e1d3 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 7a0bd2f32..96fd3ba35 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 33a575c24..b94101111 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 932a62d9b..f7582b8f1 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 d95230528..34d969f02 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 635f4a2b4..03cae4e4a 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 291ad9161..4bd8b7f22 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 7f1beb1af..d3a737bb1 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 4b926ec37..5ef9a4090 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 7aee4f4c8..06a895027 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 890bff484..964b559c7 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 af484be01..2177ddadc 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 ac994c31d..010ac15e9 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 36aa60c77..e8768c5cd 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 06b3788b7..8d45fc5c3 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 2979663df..68d4751cd 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 5a16113dc..29e3d129d 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 daca68d7a..d1be72099 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 c5a27c9dc..94c0bf772 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 6438d9ef9..8fd43d219 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 19731b385..77c503f09 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 fd3dbedc1..5982c8c98 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 3faa5db5b..7e1df322f 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 f3b190222..79b6af8d4 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 7c194bbc8..5a0b2e747 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 3b9697320..1f1520d0a 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 b46919fb4..d074d2e2e 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 38f482966..46db48049 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 4ef42d6c0..8618b9240 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 3e3dd90da..1a92f6134 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 b9367c163..fa829bfb8 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 d200652ac..943153956 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 166f36bb7..183bad9a8 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 8af701045..db7d26f86 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 2ce04068b..9ad26f472 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 127c3de52..5961ea026 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 ce2b2b181..e80654158 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 80850b4c4..346a00b0e 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 b2af1ac0c..50f0c72d1 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 b15af2444..859da97e9 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 e91be96c7..0cc909584 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 6649b7248..558cd2c06 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 171/199] 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 a51cd55f8..9bc0d61f2 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 fab21a8f1..56ecf575d 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 42f70996b..3967709ac 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 172/199] 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 3967709ac..a328a92fd 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 173/199] 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 a328a92fd..c9c9af94b 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 174/199] 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 540d599dd..ffc2dfd1e 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 175/199] 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 ffc2dfd1e..ac34e3aaa 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 176/199] 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 e8765b3de..6d21f5eda 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 c9c9af94b..2b6a10b10 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 177/199] 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 1373815c6..6ec4f6a4c 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 178/199] 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 387f735ce..ae7a69c66 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 1b3dc4990..caee074d1 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 179/199] 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 558cd2c06..95eb4a30b 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 180/199] 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 6ec4f6a4c..8ea02ea5e 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 181/199] 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 8ea02ea5e..ec1a06eda 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 182/199] 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 ec1a06eda..86e88fa13 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 183/199] 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 ae7a69c66..0d3b086f6 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 caee074d1..1b3dc4990 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 184/199] 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 94c0bf772..6814e6457 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 185/199] 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 280f6b269..c2e33f4b2 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 186/199] 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 c2e33f4b2..888b68609 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( From edaa66f6223b1a4fc21cb26ae5e78f23893e56d7 Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Sat, 4 Feb 2023 01:06:15 +0100 Subject: [PATCH 187/199] fix: use /usr/bin/env bash in launch script This should make it possible to run these scripts on any system, as /bin/bash is not standard! Notably this fixes the script on NixOS. Signed-off-by: Sefa Eyeoglu --- launcher/Launcher.in | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/launcher/Launcher.in b/launcher/Launcher.in index 68fac26a0..1a23f2555 100755 --- a/launcher/Launcher.in +++ b/launcher/Launcher.in @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # Basic start script for running the launcher with the libs packaged with it. function printerror { From c125c96e8851b0386756b34102b97ad87dc13680 Mon Sep 17 00:00:00 2001 From: BalkanMadman Date: Sat, 4 Feb 2023 16:48:06 +0200 Subject: [PATCH 188/199] Java installations detection fix for Linux Signed-off-by: BalkanMadman --- launcher/java/JavaUtils.cpp | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/launcher/java/JavaUtils.cpp b/launcher/java/JavaUtils.cpp index 5efbc7a86..e55663aab 100644 --- a/launcher/java/JavaUtils.cpp +++ b/launcher/java/JavaUtils.cpp @@ -412,8 +412,6 @@ QList JavaUtils::FindJavaPaths() #elif defined(Q_OS_LINUX) QList JavaUtils::FindJavaPaths() { - qDebug() << "Linux Java detection incomplete - defaulting to \"java\""; - QList javas; javas.append(this->GetDefaultJava()->path); auto scanJavaDir = [&](const QString & dirPath) @@ -421,20 +419,11 @@ QList JavaUtils::FindJavaPaths() QDir dir(dirPath); if(!dir.exists()) return; - auto entries = dir.entryInfoList(QDir::Dirs | QDir::NoDotAndDotDot | QDir::NoSymLinks); + auto entries = dir.entryInfoList(QDir::Dirs | QDir::NoDotAndDotDot); for(auto & entry: entries) { - QString prefix; - if(entry.isAbsolute()) - { - prefix = entry.absoluteFilePath(); - } - else - { - prefix = entry.filePath(); - } - + prefix = entry.canonicalFilePath(); javas.append(FS::PathCombine(prefix, "jre/bin/java")); javas.append(FS::PathCombine(prefix, "bin/java")); } From 34460dd77a8c5eb6896849f061d1118b2585e525 Mon Sep 17 00:00:00 2001 From: Rachel Powers <508861+Ryex@users.noreply.github.com> Date: Sat, 4 Feb 2023 12:28:52 -0700 Subject: [PATCH 189/199] ensure command env vars use native path seperators fix #824 Signed-off-by: Rachel Powers <508861+Ryex@users.noreply.github.com> --- launcher/minecraft/MinecraftInstance.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/launcher/minecraft/MinecraftInstance.cpp b/launcher/minecraft/MinecraftInstance.cpp index 8a814cbfa..4fe234c4f 100644 --- a/launcher/minecraft/MinecraftInstance.cpp +++ b/launcher/minecraft/MinecraftInstance.cpp @@ -461,8 +461,8 @@ QMap MinecraftInstance::getVariables() QMap out; out.insert("INST_NAME", name()); out.insert("INST_ID", id()); - out.insert("INST_DIR", QDir(instanceRoot()).absolutePath()); - out.insert("INST_MC_DIR", QDir(gameRoot()).absolutePath()); + out.insert("INST_DIR", QDir::toNativeSeparators(QDir(instanceRoot()).absolutePath())); + out.insert("INST_MC_DIR", QDir::toNativeSeparators(QDir(gameRoot()).absolutePath())); out.insert("INST_JAVA", settings()->get("JavaPath").toString()); out.insert("INST_JAVA_ARGS", javaArguments().join(' ')); return out; From 8114d8778f460633224adb64227eb466d3a86982 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 6 Feb 2023 15:31:42 +0000 Subject: [PATCH 190/199] chore(deps): update cachix/install-nix-action action to v19 --- .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 86e88fa13..9cc07ee52 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -575,7 +575,7 @@ jobs: submodules: 'true' - name: Install nix if: inputs.build_type == 'Debug' - uses: cachix/install-nix-action@v18 + uses: cachix/install-nix-action@v19 with: install_url: https://nixos.org/nix/install extra_nix_config: | From 8440c2819ba27f77d968809180b5fec5b3682dc8 Mon Sep 17 00:00:00 2001 From: ktheticdev <64607352+ktheticdev@users.noreply.github.com> Date: Tue, 7 Feb 2023 11:10:43 +0400 Subject: [PATCH 191/199] Fix README.md typo Signed-off-by: ktheticdev <64607352+ktheticdev@users.noreply.github.com> --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 8765da93b..6271dff3c 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ 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?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) +[![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=COPR&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. From d886d32bd844734a8195af459b7293879866205e Mon Sep 17 00:00:00 2001 From: PandaNinjas Date: Tue, 7 Feb 2023 17:21:00 +0000 Subject: [PATCH 192/199] Replace potentially ReDOSable regex Signed-off-by: PandaNinjas --- launcher/InstanceImportTask.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/launcher/InstanceImportTask.cpp b/launcher/InstanceImportTask.cpp index 70bf5784a..6b5317e5e 100644 --- a/launcher/InstanceImportTask.cpp +++ b/launcher/InstanceImportTask.cpp @@ -361,7 +361,7 @@ void InstanceImportTask::processModrinth() } else { QString pack_id; if (!m_sourceUrl.isEmpty()) { - QRegularExpression regex(R"(data\/(.*)\/versions)"); + QRegularExpression regex(R"(data\/([^\/]*)\/versions)"); pack_id = regex.match(m_sourceUrl.toString()).captured(1); } From 104863846b9b47dab5e01413b6d177b8a647b2c3 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 10 Feb 2023 06:24:45 +0000 Subject: [PATCH 193/199] chore(deps): update actions/cache action to v3.2.5 --- .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 86e88fa13..2eaee4503 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -151,7 +151,7 @@ jobs: - name: Retrieve ccache cache (Windows MinGW-w64) if: runner.os == 'Windows' && matrix.msystem != '' && inputs.build_type == 'Debug' - uses: actions/cache@v3.2.4 + uses: actions/cache@v3.2.5 with: path: '${{ github.workspace }}\.ccache' key: ${{ matrix.os }}-mingw-w64-ccache-${{ github.run_id }} From 6be7eed878bc701407c6c3efd93d9944e1079490 Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Fri, 10 Feb 2023 09:17:48 +0100 Subject: [PATCH 194/199] fix: don't extract files outside of target path This should fix a security issue regarding path traversal in zip files. Signed-off-by: Sefa Eyeoglu --- launcher/MMCZip.cpp | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/launcher/MMCZip.cpp b/launcher/MMCZip.cpp index f66003432..734eacd84 100644 --- a/launcher/MMCZip.cpp +++ b/launcher/MMCZip.cpp @@ -275,7 +275,8 @@ bool MMCZip::findFilesInZip(QuaZip * zip, const QString & what, QStringList & re // ours std::optional MMCZip::extractSubDir(QuaZip *zip, const QString & subdir, const QString &target) { - QDir directory(target); + auto absDirectoryUrl = QUrl::fromLocalFile(target); + QStringList extracted; qDebug() << "Extracting subdir" << subdir << "from" << zip->getZipName() << "to" << target; @@ -317,11 +318,16 @@ std::optional MMCZip::extractSubDir(QuaZip *zip, const QString & su QString absFilePath; if(name.isEmpty()) { - absFilePath = directory.absoluteFilePath(name) + "/"; + absFilePath = FS::PathCombine(target, "/"); // FIXME this seems weird } else { - absFilePath = directory.absoluteFilePath(path + name); + absFilePath = FS::PathCombine(target, path + name); + } + + if (!absDirectoryUrl.isParentOf(QUrl::fromLocalFile(absFilePath))) { + qWarning() << "Extracting" << name << "was cancelled, because it was effectively outside of the target path" << target; + return std::nullopt; } if (!JlCompress::extractFile(zip, "", absFilePath)) From e70a5a47ee8bf67715a46a6ac668dad7685d08be Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Fri, 10 Feb 2023 10:46:21 +0100 Subject: [PATCH 195/199] fix: ignore absolute paths when extracting Signed-off-by: Sefa Eyeoglu --- launcher/MMCZip.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/launcher/MMCZip.cpp b/launcher/MMCZip.cpp index 734eacd84..31460bf40 100644 --- a/launcher/MMCZip.cpp +++ b/launcher/MMCZip.cpp @@ -306,6 +306,11 @@ std::optional MMCZip::extractSubDir(QuaZip *zip, const QString & su name.remove(0, subdir.size()); auto original_name = name; + // Fix subdirs/files ending with a / getting transformed into absolute paths + if(name.startsWith('/')){ + name = name.mid(1); + } + // Fix weird "folders with a single file get squashed" thing QString path; if(name.contains('/') && !name.endsWith('/')){ From 381d7413c800b3121a4665994be0fb2cc33137cc Mon Sep 17 00:00:00 2001 From: RaptaG <77157639+RaptaG@users.noreply.github.com> Date: Fri, 10 Feb 2023 19:47:08 +0200 Subject: [PATCH 196/199] Link license in the shield badge So that no trash URL shows when hovering! Signed-off-by: RaptaG <77157639+RaptaG@users.noreply.github.com> --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 6271dff3c..aaa1fd4c6 100644 --- a/README.md +++ b/README.md @@ -96,7 +96,7 @@ Be aware that if you build this software without removing the provided API keys 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) +## License [![https://github.com/PrismLauncher/PrismLauncher/blob/develop/LICENSE](https://img.shields.io/github/license/PrismLauncher/PrismLauncher?label=License&logo=gnu&color=C4282D)](LICENSE) All launcher code is available under the GPL-3.0-only license. From 80840f1fdb14f2972e6bc487b061f419334894da Mon Sep 17 00:00:00 2001 From: Rachel Powers <508861+Ryex@users.noreply.github.com> Date: Sun, 12 Feb 2023 22:32:34 -0700 Subject: [PATCH 197/199] fix: add missing header to Application.cpp fails to compile on KISS Linux without Signed-off-by: Rachel Powers <508861+Ryex@users.noreply.github.com> --- launcher/Application.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/launcher/Application.cpp b/launcher/Application.cpp index 0d3b086f6..caaa74c8b 100644 --- a/launcher/Application.cpp +++ b/launcher/Application.cpp @@ -79,6 +79,7 @@ #include #include +#include #include #include #include From 89c945ecc8de579e8f93ae302a7dabf4629e188f Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Tue, 14 Feb 2023 11:10:29 +0100 Subject: [PATCH 198/199] feat(ci): add Windows codesigning Signed-off-by: Sefa Eyeoglu --- .github/workflows/build.yml | 27 ++++++++++++++++++++++++++- .github/workflows/trigger_builds.yml | 2 ++ .github/workflows/trigger_release.yml | 3 +++ 3 files changed, 31 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 625ac099d..c3b9f2067 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -15,6 +15,12 @@ on: SPARKLE_ED25519_KEY: description: Private key for signing Sparkle updates required: false + WINDOWS_CODESIGN_CERT: + description: Certificate for signing Windows builds + required: false + WINDOWS_CODESIGN_PASSWORD: + description: Password for signing Windows builds + required: false CACHIX_AUTH_TOKEN: description: Private token for authenticating against Cachix cache required: false @@ -40,6 +46,7 @@ jobs: - os: windows-2022 name: "Windows-MinGW-w64" msystem: clang64 + vcvars_arch: 'amd64_x86' - os: windows-2022 name: "Windows-MSVC-Legacy" @@ -225,7 +232,7 @@ jobs: cache: ${{ inputs.is_qt_cached }} - name: Install MSVC (Windows MSVC) - if: runner.os == 'Windows' && matrix.msystem == '' + if: runner.os == 'Windows' # We want this for MinGW builds as well, as we need SignTool uses: ilammy/msvc-dev-cmd@v1 with: vsversion: 2022 @@ -377,6 +384,19 @@ jobs: Copy-Item D:/a/PrismLauncher/Qt/Tools/OpenSSL/Win_x86/bin/libssl-1_1.dll -Destination libssl-1_1.dll } + - name: Fetch codesign certificate (Windows) + if: runner.os == 'Windows' + shell: bash # yes, we are not using MSYS2 or PowerShell here + run: | + echo '${{ secrets.WINDOWS_CODESIGN_CERT }}' | base64 --decode > codesign.pfx + + - name: Sign executable (Windows) + if: runner.os == 'Windows' + run: | + cd ${{ env.INSTALL_DIR }} + # We ship the exact same executable for portable and non-portable editions, so signing just once is fine + SignTool sign /fd sha256 /td sha256 /f ../codesign.pfx /p '${{ secrets.WINDOWS_CODESIGN_PASSWORD }}' /tr http://timestamp.digicert.com prismlauncher.exe + - name: Package (Windows MinGW-w64, portable) if: runner.os == 'Windows' && matrix.msystem != '' shell: msys2 {0} @@ -396,6 +416,11 @@ jobs: cd ${{ env.INSTALL_DIR }} makensis -NOCD "${{ github.workspace }}/${{ env.BUILD_DIR }}/program_info/win_install.nsi" + - name: Sign installer (Windows) + if: runner.os == 'Windows' + run: | + SignTool sign /fd sha256 /td sha256 /f codesign.pfx /p '${{ secrets.WINDOWS_CODESIGN_PASSWORD }}' /tr http://timestamp.digicert.com PrismLauncher-Setup.exe + - name: Package (Linux) if: runner.os == 'Linux' run: | diff --git a/.github/workflows/trigger_builds.yml b/.github/workflows/trigger_builds.yml index a08193a06..26ee4380b 100644 --- a/.github/workflows/trigger_builds.yml +++ b/.github/workflows/trigger_builds.yml @@ -31,4 +31,6 @@ jobs: is_qt_cached: true secrets: SPARKLE_ED25519_KEY: ${{ secrets.SPARKLE_ED25519_KEY }} + WINDOWS_CODESIGN_CERT: ${{ secrets.WINDOWS_CODESIGN_CERT }} + WINDOWS_CODESIGN_PASSWORD: ${{ secrets.WINDOWS_CODESIGN_PASSWORD }} CACHIX_AUTH_TOKEN: ${{ secrets.CACHIX_AUTH_TOKEN }} diff --git a/.github/workflows/trigger_release.yml b/.github/workflows/trigger_release.yml index a2f89819c..3c56a38ea 100644 --- a/.github/workflows/trigger_release.yml +++ b/.github/workflows/trigger_release.yml @@ -15,6 +15,9 @@ jobs: is_qt_cached: false secrets: SPARKLE_ED25519_KEY: ${{ secrets.SPARKLE_ED25519_KEY }} + WINDOWS_CODESIGN_CERT: ${{ secrets.WINDOWS_CODESIGN_CERT }} + WINDOWS_CODESIGN_PASSWORD: ${{ secrets.WINDOWS_CODESIGN_PASSWORD }} + CACHIX_AUTH_TOKEN: ${{ secrets.CACHIX_AUTH_TOKEN }} create_release: needs: build_release From 33bf85a387e535a27ad23e42b72c5fe7cce7f64c Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Wed, 15 Feb 2023 21:34:12 +0100 Subject: [PATCH 199/199] fix(actions): don't fail if code signing certificate is missing Signed-off-by: Sefa Eyeoglu --- .github/workflows/build.yml | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c3b9f2067..c844f3565 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -393,9 +393,13 @@ jobs: - name: Sign executable (Windows) if: runner.os == 'Windows' run: | - cd ${{ env.INSTALL_DIR }} - # We ship the exact same executable for portable and non-portable editions, so signing just once is fine - SignTool sign /fd sha256 /td sha256 /f ../codesign.pfx /p '${{ secrets.WINDOWS_CODESIGN_PASSWORD }}' /tr http://timestamp.digicert.com prismlauncher.exe + if (Get-Content ./codesign.pfx){ + cd ${{ env.INSTALL_DIR }} + # We ship the exact same executable for portable and non-portable editions, so signing just once is fine + SignTool sign /fd sha256 /td sha256 /f ../codesign.pfx /p '${{ secrets.WINDOWS_CODESIGN_PASSWORD }}' /tr http://timestamp.digicert.com prismlauncher.exe + } else { + ":warning: Skipped code signing for Windows, as certificate was not present." >> $env:GITHUB_STEP_SUMMARY + } - name: Package (Windows MinGW-w64, portable) if: runner.os == 'Windows' && matrix.msystem != '' @@ -419,7 +423,11 @@ jobs: - name: Sign installer (Windows) if: runner.os == 'Windows' run: | - SignTool sign /fd sha256 /td sha256 /f codesign.pfx /p '${{ secrets.WINDOWS_CODESIGN_PASSWORD }}' /tr http://timestamp.digicert.com PrismLauncher-Setup.exe + if (Get-Content ./codesign.pfx){ + SignTool sign /fd sha256 /td sha256 /f codesign.pfx /p '${{ secrets.WINDOWS_CODESIGN_PASSWORD }}' /tr http://timestamp.digicert.com PrismLauncher-Setup.exe + } else { + ":warning: Skipped code signing for Windows, as certificate was not present." >> $env:GITHUB_STEP_SUMMARY + } - name: Package (Linux) if: runner.os == 'Linux'