diff options
Diffstat (limited to 'launcher')
114 files changed, 3170 insertions, 467 deletions
diff --git a/launcher/Application.cpp b/launcher/Application.cpp index 9bd4ebae..a3d6216e 100644 --- a/launcher/Application.cpp +++ b/launcher/Application.cpp @@ -14,7 +14,7 @@ #include "ui/pages/global/ProxyPage.h" #include "ui/pages/global/ExternalToolsPage.h" #include "ui/pages/global/AccountListPage.h" -#include "ui/pages/global/PasteEEPage.h" +#include "ui/pages/global/APIPage.h" #include "ui/pages/global/CustomCommandsPage.h" #include "ui/themes/ITheme.h" @@ -187,7 +187,9 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv) setApplicationName(BuildConfig.LAUNCHER_NAME); setApplicationDisplayName(BuildConfig.LAUNCHER_DISPLAYNAME); setApplicationVersion(BuildConfig.printableVersionString()); - + #if (QT_VERSION >= QT_VERSION_CHECK(5,7,0)) + setDesktopFileName(BuildConfig.LAUNCHER_DESKTOPFILENAME); + #endif startTime = QDateTime::currentDateTime(); #ifdef Q_OS_LINUX @@ -311,7 +313,7 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv) dataPath = xdgDataHome + "/polymc"; adjustedBy += "XDG standard " + dataPath; #elif defined(Q_OS_MAC) - QDir foo(FS::PathCombine(applicationDirPath(), "../../Data")); + QDir foo(FS::PathCombine(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation), "..")); dataPath = foo.absolutePath(); adjustedBy += "Fallback to special Mac location " + dataPath; #else @@ -435,7 +437,7 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv) #endif /* - * Establish the mechanism for communication with an already running MultiMC that uses the same data path. + * Establish the mechanism for communication with an already running PolyMC 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. */ @@ -529,10 +531,8 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv) #elif defined(Q_OS_WIN32) m_rootPath = binPath; #elif defined(Q_OS_MAC) - QDir foo(FS::PathCombine(binPath, "../..")); + QDir foo(FS::PathCombine(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation), "..")); m_rootPath = foo.absolutePath(); - // on macOS, touch the root to force Finder to reload the .app metadata (and fix any icon change issues) - FS::updateTimestamp(m_rootPath); #endif #ifdef MULTIMC_JARS_LOCATION @@ -595,7 +595,7 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv) m_settings->registerSetting("AutoUpdate", true); // Theming - m_settings->registerSetting("IconTheme", QString("multimc")); + m_settings->registerSetting("IconTheme", QString("pe_colored")); m_settings->registerSetting("ApplicationTheme", QString("system")); // Notifications @@ -662,7 +662,7 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv) // Memory m_settings->registerSetting({"MinMemAlloc", "MinMemoryAlloc"}, 512); - m_settings->registerSetting({"MaxMemAlloc", "MaxMemoryAlloc"}, 1024); + m_settings->registerSetting({"MaxMemAlloc", "MaxMemoryAlloc"}, 4096); m_settings->registerSetting("PermGen", 128); // Java Settings @@ -714,8 +714,13 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv) m_settings->registerSetting("UpdateDialogGeometry", ""); - // paste.ee API key - m_settings->registerSetting("PasteEEAPIKey", "multimc"); + // pastebin URL + m_settings->registerSetting("PastebinURL", "https://0x0.st"); + + m_settings->registerSetting("CloseAfterLaunch", false); + + // Custom MSA credentials + m_settings->registerSetting("MSAClientIDOverride", ""); // Init page provider { @@ -728,7 +733,7 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv) m_globalSettingsProvider->addPage<ProxyPage>(); m_globalSettingsProvider->addPage<ExternalToolsPage>(); m_globalSettingsProvider->addPage<AccountListPage>(); - m_globalSettingsProvider->addPage<PasteEEPage>(); + m_globalSettingsProvider->addPage<APIPage>(); } qDebug() << "<> Settings loaded."; } @@ -1514,3 +1519,13 @@ QString Application::getJarsPath() } return m_jarsPath; } + +QString Application::getMSAClientID() +{ + QString clientIDOverride = m_settings->get("MSAClientIDOverride").toString(); + if (!clientIDOverride.isEmpty()) { + return clientIDOverride; + } + + return BuildConfig.MSA_CLIENT_ID; +} diff --git a/launcher/Application.h b/launcher/Application.h index c1cd8224..fb41d647 100644 --- a/launcher/Application.h +++ b/launcher/Application.h @@ -117,6 +117,8 @@ public: QString getJarsPath(); + QString getMSAClientID(); + /// this is the root of the 'installation'. Used for automatic updates const QString &root() { return m_rootPath; diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index b5c52afa..90149c3b 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -37,6 +37,10 @@ set(CORE_SOURCES InstanceImportTask.h InstanceImportTask.cpp + # Mod downloading task + ModDownloadTask.h + ModDownloadTask.cpp + # Use tracking separate from memory management Usable.h @@ -221,7 +225,11 @@ set(MINECRAFT_SOURCES minecraft/auth/flows/Mojang.h minecraft/auth/flows/MSA.cpp minecraft/auth/flows/MSA.h + minecraft/auth/flows/Offline.cpp + minecraft/auth/flows/Offline.h + minecraft/auth/steps/OfflineStep.cpp + minecraft/auth/steps/OfflineStep.h minecraft/auth/steps/EntitlementsStep.cpp minecraft/auth/steps/EntitlementsStep.h minecraft/auth/steps/GetSkinStep.cpp @@ -506,12 +514,19 @@ set(FLAME_SOURCES # Flame modplatform/flame/FlamePackIndex.cpp modplatform/flame/FlamePackIndex.h + modplatform/flame/FlameModIndex.cpp + modplatform/flame/FlameModIndex.h modplatform/flame/PackManifest.h modplatform/flame/PackManifest.cpp modplatform/flame/FileResolvingTask.h modplatform/flame/FileResolvingTask.cpp ) +set(MODRINTH_SOURCES + modplatform/modrinth/ModrinthPackIndex.cpp + modplatform/modrinth/ModrinthPackIndex.h +) + set(MODPACKSCH_SOURCES modplatform/modpacksch/FTBPackInstallTask.h modplatform/modpacksch/FTBPackInstallTask.cpp @@ -566,6 +581,7 @@ set(LOGIC_SOURCES ${ICONS_SOURCES} ${FTB_SOURCES} ${FLAME_SOURCES} + ${MODRINTH_SOURCES} ${MODPACKSCH_SOURCES} ${TECHNIC_SOURCES} ${ATLAUNCHER_SOURCES} @@ -707,8 +723,8 @@ SET(LAUNCHER_SOURCES ui/pages/global/LauncherPage.h ui/pages/global/ProxyPage.cpp ui/pages/global/ProxyPage.h - ui/pages/global/PasteEEPage.cpp - ui/pages/global/PasteEEPage.h + ui/pages/global/APIPage.cpp + ui/pages/global/APIPage.h # GUI - platform pages ui/pages/modplatform/VanillaPage.cpp @@ -739,6 +755,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/technic/TechnicModel.cpp ui/pages/modplatform/technic/TechnicModel.h @@ -748,6 +768,11 @@ SET(LAUNCHER_SOURCES ui/pages/modplatform/ImportPage.cpp ui/pages/modplatform/ImportPage.h + ui/pages/modplatform/modrinth/ModrinthModel.cpp + ui/pages/modplatform/modrinth/ModrinthModel.h + ui/pages/modplatform/modrinth/ModrinthPage.cpp + ui/pages/modplatform/modrinth/ModrinthPage.h + # GUI - dialogs ui/dialogs/AboutDialog.cpp ui/dialogs/AboutDialog.h @@ -769,6 +794,8 @@ SET(LAUNCHER_SOURCES ui/dialogs/LoginDialog.h ui/dialogs/MSALoginDialog.cpp ui/dialogs/MSALoginDialog.h + ui/dialogs/OfflineLoginDialog.cpp + ui/dialogs/OfflineLoginDialog.h ui/dialogs/NewComponentDialog.cpp ui/dialogs/NewComponentDialog.h ui/dialogs/NewInstanceDialog.cpp @@ -785,6 +812,8 @@ SET(LAUNCHER_SOURCES ui/dialogs/VersionSelectDialog.h ui/dialogs/SkinUploadDialog.cpp ui/dialogs/SkinUploadDialog.h + ui/dialogs/ModDownloadDialog.cpp + ui/dialogs/ModDownloadDialog.h # GUI - widgets @@ -842,7 +871,7 @@ qt5_wrap_ui(LAUNCHER_UI ui/pages/global/AccountListPage.ui ui/pages/global/JavaPage.ui ui/pages/global/LauncherPage.ui - ui/pages/global/PasteEEPage.ui + ui/pages/global/APIPage.ui ui/pages/global/ProxyPage.ui ui/pages/global/MinecraftPage.ui ui/pages/global/ExternalToolsPage.ui @@ -861,10 +890,12 @@ qt5_wrap_ui(LAUNCHER_UI ui/pages/modplatform/atlauncher/AtlPage.ui ui/pages/modplatform/VanillaPage.ui ui/pages/modplatform/flame/FlamePage.ui + ui/pages/modplatform/flame/FlameModPage.ui ui/pages/modplatform/legacy_ftb/Page.ui ui/pages/modplatform/ImportPage.ui ui/pages/modplatform/ftb/FtbPage.ui ui/pages/modplatform/technic/TechnicPage.ui + ui/pages/modplatform/modrinth/ModrinthPage.ui ui/widgets/InstanceCardWidget.ui ui/widgets/CustomCommands.ui ui/widgets/MCModInfoFrame.ui @@ -880,6 +911,7 @@ qt5_wrap_ui(LAUNCHER_UI ui/dialogs/ExportInstanceDialog.ui ui/dialogs/IconPickerDialog.ui ui/dialogs/MSALoginDialog.ui + ui/dialogs/OfflineLoginDialog.ui ui/dialogs/AboutDialog.ui ui/dialogs/LoginDialog.ui ui/dialogs/EditAccountDialog.ui @@ -908,9 +940,8 @@ endif() add_library(Launcher_logic STATIC ${LOGIC_SOURCES} ${LAUNCHER_SOURCES} ${LAUNCHER_UI} ${LAUNCHER_RESOURCES}) target_link_libraries(Launcher_logic systeminfo - Launcher_quazip Launcher_classparser - ${NBT_NAME} + nbt++ ${ZLIB_LIBRARIES} optional-bare tomlc99 @@ -926,9 +957,9 @@ target_link_libraries(Launcher_logic ) target_link_libraries(Launcher_logic Launcher_iconfix - ${QUAZIP_LIBRARIES} + QuaZip::QuaZip hoedown - Launcher_rainbow + PolyMC_rainbow LocalPeer ) diff --git a/launcher/InstanceImportTask.cpp b/launcher/InstanceImportTask.cpp index 8cd68d7b..ec378538 100644 --- a/launcher/InstanceImportTask.cpp +++ b/launcher/InstanceImportTask.cpp @@ -29,7 +29,7 @@ #include "modplatform/flame/FileResolvingTask.h" #include "modplatform/flame/PackManifest.h" #include "Json.h" -#include <quazipdir.h> +#include <quazip/quazipdir.h> #include "modplatform/technic/TechnicPackProcessor.h" #include "icons/IconList.h" diff --git a/launcher/InstancePageProvider.h b/launcher/InstancePageProvider.h index 2af90b91..97eeab8c 100644 --- a/launcher/InstancePageProvider.h +++ b/launcher/InstancePageProvider.h @@ -37,7 +37,7 @@ public: if(onesix) { values.append(new VersionPage(onesix.get())); - auto modsPage = new ModFolderPage(onesix.get(), onesix->loaderModList(), "mods", "loadermods", tr("Loader mods"), "Loader-mods"); + auto modsPage = new ModFolderPage(onesix.get(), onesix->loaderModList(), "mods", "loadermods", tr("Mods"), "Loader-mods"); modsPage->setFilter("%1 (*.zip *.jar *.litemod)"); values.append(modsPage); values.append(new CoreModFolderPage(onesix.get(), onesix->coreModList(), "coremods", "coremods", tr("Core mods"), "Core-mods")); @@ -74,3 +74,4 @@ public: protected: InstancePtr inst; }; + diff --git a/launcher/LaunchController.cpp b/launcher/LaunchController.cpp index 7750be1a..32fc99cb 100644 --- a/launcher/LaunchController.cpp +++ b/launcher/LaunchController.cpp @@ -116,6 +116,12 @@ void LaunchController::login() { m_session->wants_online = m_online; m_accountToUse->fillSession(m_session); + // Launch immediately in true offline mode + if(m_accountToUse->isOffline()) { + launchInstance(); + return; + } + switch(m_accountToUse->accountState()) { case AccountState::Offline: { m_session->wants_online = false; diff --git a/launcher/Launcher.in b/launcher/Launcher.in index b79b276b..5e5e2c2b 100755 --- a/launcher/Launcher.in +++ b/launcher/Launcher.in @@ -14,7 +14,7 @@ if [[ $EUID -eq 0 ]]; then fi -LAUNCHER_NAME=@Launcher_Name@ +LAUNCHER_NAME=@Launcher_APP_BINARY_NAME@ LAUNCHER_DIR="$(dirname "$(readlink -f "$0")")" echo "Launcher Dir: ${LAUNCHER_DIR}" diff --git a/launcher/MMCZip.cpp b/launcher/MMCZip.cpp index b25c61e7..9d7e4cc2 100644 --- a/launcher/MMCZip.cpp +++ b/launcher/MMCZip.cpp @@ -13,17 +13,16 @@ * limitations under the License. */ -#include <quazip.h> -#include <quazipdir.h> -#include <quazipfile.h> -#include <JlCompress.h> +#include <quazip/quazip.h> +#include <quazip/quazipdir.h> +#include <quazip/quazipfile.h> #include "MMCZip.h" #include "FileSystem.h" #include <QDebug> // ours -bool MMCZip::mergeZipFiles(QuaZip *into, QFileInfo from, QSet<QString> &contained, const JlCompress::FilterFunction filter) +bool MMCZip::mergeZipFiles(QuaZip *into, QFileInfo from, QSet<QString> &contained, const FilterFunction filter) { QuaZip modZip(from.filePath()); modZip.open(QuaZip::mdUnzip); @@ -74,6 +73,39 @@ bool MMCZip::mergeZipFiles(QuaZip *into, QFileInfo from, QSet<QString> &containe return true; } +bool MMCZip::compressDirFiles(QuaZip *zip, QString dir, QFileInfoList files) +{ + QDir directory(dir); + if (!directory.exists()) return false; + + for (auto e : files) { + auto filePath = directory.relativeFilePath(e.absoluteFilePath()); + if( !JlCompress::compressFile(zip, e.absoluteFilePath(), filePath)) return false; + } + + return true; +} + +bool MMCZip::compressDirFiles(QString fileCompressed, QString dir, QFileInfoList files) +{ + QuaZip zip(fileCompressed); + QDir().mkpath(QFileInfo(fileCompressed).absolutePath()); + if(!zip.open(QuaZip::mdCreate)) { + QFile::remove(fileCompressed); + return false; + } + + auto result = compressDirFiles(&zip, dir, files); + + zip.close(); + if(zip.getZipError()!=0) { + QFile::remove(fileCompressed); + return false; + } + + return result; +} + // ours bool MMCZip::createModdedJar(QString sourceJarPath, QString targetJarPath, const QList<Mod>& mods) { @@ -122,13 +154,22 @@ bool MMCZip::createModdedJar(QString sourceJarPath, QString targetJarPath, const } else if (mod.type() == Mod::MOD_FOLDER) { + // untested, but seems to be unused / not possible to reach // FIXME: buggy - does not work with addedFiles auto filename = mod.filename(); QString what_to_zip = filename.absoluteFilePath(); QDir dir(what_to_zip); dir.cdUp(); QString parent_dir = dir.absolutePath(); - if (!JlCompress::compressSubDir(&zipOut, what_to_zip, parent_dir, addedFiles)) + auto files = QFileInfoList(); + MMCZip::collectFileListRecursively(what_to_zip, nullptr, &files, nullptr); + + for (auto e : files) { + if (addedFiles.contains(e.filePath())) + files.removeAll(e); + } + + if (!MMCZip::compressDirFiles(&zipOut, parent_dir, files)) { zipOut.close(); QFile::remove(targetJarPath); @@ -136,7 +177,7 @@ bool MMCZip::createModdedJar(QString sourceJarPath, QString targetJarPath, const return false; } qDebug() << "Adding folder " << filename.fileName() << " from " - << filename.absoluteFilePath(); + << filename.absoluteFilePath(); } else { @@ -310,3 +351,37 @@ bool MMCZip::extractFile(QString fileCompressed, QString file, QString target) } return MMCZip::extractRelFile(&zip, file, target); } + +bool MMCZip::collectFileListRecursively(const QString& rootDir, const QString& subDir, QFileInfoList *files, + MMCZip::FilterFunction excludeFilter) { + QDir rootDirectory(rootDir); + if (!rootDirectory.exists()) return false; + + QDir directory; + if (subDir == nullptr) + directory = rootDirectory; + else + directory = QDir(subDir); + + if (!directory.exists()) return false; // shouldn't ever happen + + // recurse directories + QFileInfoList entries = directory.entryInfoList(QDir::AllDirs | QDir::NoDotAndDotDot | QDir::Hidden); + for (const auto& e: entries) { + if (!collectFileListRecursively(rootDir, e.filePath(), files, excludeFilter)) + return false; + } + + // collect files + entries = directory.entryInfoList(QDir::Files); + for (const auto& e: entries) { + QString relativeFilePath = rootDirectory.relativeFilePath(e.absoluteFilePath()); + if (excludeFilter && excludeFilter(relativeFilePath)) { + qDebug() << "Skipping file " << relativeFilePath; + continue; + } + + files->append(e.filePath()); // we want the original paths for MMCZip::compressDirFiles + } + return true; +} diff --git a/launcher/MMCZip.h b/launcher/MMCZip.h index 9c47fa11..0f7aa254 100644 --- a/launcher/MMCZip.h +++ b/launcher/MMCZip.h @@ -21,17 +21,36 @@ #include "minecraft/mod/Mod.h" #include <functional> -#include <JlCompress.h> +#include <quazip/JlCompress.h> #include <nonstd/optional> namespace MMCZip { + using FilterFunction = std::function<bool(const QString &)>; /** * Merge two zip files, using a filter function */ bool mergeZipFiles(QuaZip *into, QFileInfo from, QSet<QString> &contained, - const JlCompress::FilterFunction filter = nullptr); + const FilterFunction filter = nullptr); + + /** + * Compress directory, by providing a list of files to compress + * \param zip target archive + * \param dir directory that will be compressed (to compress with relative paths) + * \param files list of files to compress + * \return true for success or false for failure + */ + bool compressDirFiles(QuaZip *zip, QString dir, QFileInfoList files); + + /** + * Compress directory, by providing a list of files to compress + * \param fileCompressed target archive file + * \param dir directory that will be compressed (to compress with relative paths) + * \param files list of files to compress + * \return true for success or false for failure + */ + bool compressDirFiles(QString fileCompressed, QString dir, QFileInfoList files); /** * take a source jar, add mods to it, resulting in target jar @@ -89,4 +108,13 @@ namespace MMCZip */ bool extractFile(QString fileCompressed, QString file, QString dir); + /** + * Populate a QFileInfoList with a directory tree recursively, while allowing to excludeFilter what shouldn't be included. + * \param rootDir directory to start off + * \param subDir subdirectory, should be nullptr for first invocation + * \param files resulting list of QFileInfo + * \param excludeFilter function to excludeFilter which files shouldn't be included (returning true means to excude) + * \return true for success or false for failure + */ + bool collectFileListRecursively(const QString &rootDir, const QString &subDir, QFileInfoList *files, FilterFunction excludeFilter); } diff --git a/launcher/ModDownloadTask.cpp b/launcher/ModDownloadTask.cpp new file mode 100644 index 00000000..08a02d29 --- /dev/null +++ b/launcher/ModDownloadTask.cpp @@ -0,0 +1,39 @@ +#include "ModDownloadTask.h" +#include "Application.h" + +ModDownloadTask::ModDownloadTask(const QUrl sourceUrl,const QString filename, const std::shared_ptr<ModFolderModel> mods) +: m_sourceUrl(sourceUrl), mods(mods), filename(filename) { +} + +void ModDownloadTask::executeTask() { + setStatus(tr("Downloading mod:\n%1").arg(m_sourceUrl.toString())); + + m_filesNetJob.reset(new NetJob(tr("Mod download"), APPLICATION->network())); + m_filesNetJob->addNetAction(Net::Download::makeFile(m_sourceUrl, mods->dir().absoluteFilePath(filename))); + 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); + m_filesNetJob->start(); +} + +void ModDownloadTask::downloadSucceeded() +{ + emitSucceeded(); + m_filesNetJob.reset(); +} + +void ModDownloadTask::downloadFailed(QString reason) +{ + emitFailed(reason); + m_filesNetJob.reset(); +} + +void ModDownloadTask::downloadProgressChanged(qint64 current, qint64 total) +{ + emit progress(current, total); +} + +bool ModDownloadTask::abort() { + return m_filesNetJob->abort(); +} + diff --git a/launcher/ModDownloadTask.h b/launcher/ModDownloadTask.h new file mode 100644 index 00000000..7e4f1b7d --- /dev/null +++ b/launcher/ModDownloadTask.h @@ -0,0 +1,34 @@ +#pragma once +#include "QObjectPtr.h" +#include "tasks/Task.h" +#include "minecraft/mod/ModFolderModel.h" +#include "net/NetJob.h" +#include <QUrl> + + +class ModDownloadTask : public Task { + Q_OBJECT +public: + explicit ModDownloadTask(const QUrl sourceUrl, const QString filename, const std::shared_ptr<ModFolderModel> mods); + +public slots: + bool abort() override; +protected: + //! Entry point for tasks. + void executeTask() override; + +private: + QUrl m_sourceUrl; + NetJob::Ptr m_filesNetJob; + const std::shared_ptr<ModFolderModel> mods; + const QString filename; + + void downloadProgressChanged(qint64 current, qint64 total); + + void downloadFailed(QString reason); + + void downloadSucceeded(); +}; + + + diff --git a/launcher/UpdateController.cpp b/launcher/UpdateController.cpp index f9b7d349..c02cd1e7 100644 --- a/launcher/UpdateController.cpp +++ b/launcher/UpdateController.cpp @@ -93,7 +93,7 @@ void UpdateController::installUpdates() qDebug() << "Installing updates."; #ifdef Q_OS_WIN QString finishCmd = QApplication::applicationFilePath(); -#elif defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) +#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(); diff --git a/launcher/java/JavaChecker.cpp b/launcher/java/JavaChecker.cpp index 80c599cc..35ddc35c 100644 --- a/launcher/java/JavaChecker.cpp +++ b/launcher/java/JavaChecker.cpp @@ -103,11 +103,15 @@ void JavaChecker::finished(int exitcode, QProcess::ExitStatus status) for(QString line : lines) { line = line.trimmed(); + // NOTE: workaround for GH-4125, where garbage is getting printed into stdout on bedrock linux + if (line.contains("/bedrock/strata")) { + continue; + } auto parts = line.split('=', QString::SkipEmptyParts); if(parts.size() != 2 || parts[0].isEmpty() || parts[1].isEmpty()) { - success = false; + continue; } else { diff --git a/launcher/java/JavaInstallList.cpp b/launcher/java/JavaInstallList.cpp index 07f2bd8c..a0a60871 100644 --- a/launcher/java/JavaInstallList.cpp +++ b/launcher/java/JavaInstallList.cpp @@ -120,8 +120,8 @@ void JavaInstallList::updateListData(QList<BaseVersionPtr> versions) bool sortJavas(BaseVersionPtr left, BaseVersionPtr right) { - auto rleft = std::dynamic_pointer_cast<JavaInstall>(left); - auto rright = std::dynamic_pointer_cast<JavaInstall>(right); + auto rleft = std::dynamic_pointer_cast<JavaInstall>(right); + auto rright = std::dynamic_pointer_cast<JavaInstall>(left); return (*rleft) > (*rright); } diff --git a/launcher/java/JavaUtils.cpp b/launcher/java/JavaUtils.cpp index 8249fc29..6e5dfeae 100644 --- a/launcher/java/JavaUtils.cpp +++ b/launcher/java/JavaUtils.cpp @@ -77,14 +77,14 @@ QProcessEnvironment CleanEnviroment() qDebug() << "Env: ignoring" << key << value; continue; } - // filter MultiMC-related things + // filter PolyMC-related things if(key.startsWith("QT_")) { qDebug() << "Env: ignoring" << key << value; continue; } #if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) - // Do not pass LD_* variables to java. They were intended for MultiMC + // Do not pass LD_* variables to java. They were intended for PolyMC if(key.startsWith("LD_")) { qDebug() << "Env: ignoring" << key << value; @@ -149,6 +149,21 @@ JavaInstallPtr JavaUtils::GetDefaultJava() return javaVersion; } +QStringList addJavasFromEnv(QList<QString> javas) +{ + QByteArray env = qgetenv("POLYMC_JAVA_PATHS"); +#if defined(Q_OS_WIN32) + QList<QString> javaPaths = QString::fromLocal8Bit(env).split(QLatin1String(";")); +#else + QList<QString> javaPaths = QString::fromLocal8Bit(env).split(QLatin1String(":")); +#endif + for(QString i : javaPaths) + { + javas.append(i); + }; + return javas; +} + #if defined(Q_OS_WIN32) QList<JavaInstallPtr> JavaUtils::FindJavaFromRegistryKey(DWORD keyType, QString keyName, QString keyJavaDir, QString subkeySuffix) { @@ -290,7 +305,7 @@ QList<QString> JavaUtils::FindJavaPaths() KEY_WOW64_64KEY, "SOFTWARE\\Azul Systems\\Zulu", "InstallationPath"); QList<JavaInstallPtr> ZULU32s = this->FindJavaFromRegistryKey( KEY_WOW64_32KEY, "SOFTWARE\\Azul Systems\\Zulu", "InstallationPath"); - + // BellSoft Liberica QList<JavaInstallPtr> LIBERICA64s = this->FindJavaFromRegistryKey( KEY_WOW64_64KEY, "SOFTWARE\\BellSoft\\Liberica", "InstallationPath"); @@ -328,7 +343,7 @@ QList<QString> JavaUtils::FindJavaPaths() java_candidates.append(ADOPTIUMJDK32s); java_candidates.append(ZULU32s); java_candidates.append(LIBERICA32s); - + java_candidates.append(MakeJavaPtr(this->GetDefaultJava()->path)); QList<QString> candidates; @@ -363,7 +378,7 @@ QList<QString> JavaUtils::FindJavaPaths() javas.append(systemLibraryJVMDir.absolutePath() + "/" + java + "/Contents/Home/bin/java"); javas.append(systemLibraryJVMDir.absolutePath() + "/" + java + "/Contents/Commands/java"); } - return javas; + return addJavasFromEnv(javas); } #elif defined(Q_OS_LINUX) @@ -402,14 +417,14 @@ QList<QString> JavaUtils::FindJavaPaths() scanJavaDir("/usr/lib/jvm"); scanJavaDir("/usr/lib64/jvm"); scanJavaDir("/usr/lib32/jvm"); - // javas stored in MultiMC's folder + // javas stored in PolyMC's folder scanJavaDir("java"); // manually installed JDKs in /opt scanJavaDir("/opt/jdk"); scanJavaDir("/opt/jdks"); // flatpak scanJavaDir("/app/jdk"); - return javas; + return addJavasFromEnv(javas); } #else QList<QString> JavaUtils::FindJavaPaths() @@ -419,6 +434,6 @@ QList<QString> JavaUtils::FindJavaPaths() QList<QString> javas; javas.append(this->GetDefaultJava()->path); - return javas; + return addJavasFromEnv(javas); } #endif diff --git a/launcher/launch/LaunchTask.cpp b/launcher/launch/LaunchTask.cpp index e6f6bbac..231a6398 100644 --- a/launcher/launch/LaunchTask.cpp +++ b/launcher/launch/LaunchTask.cpp @@ -212,7 +212,7 @@ shared_qobject_ptr<LogModel> LaunchTask::getLogModel() m_logModel->setMaxLines(m_instance->getConsoleMaxLines()); m_logModel->setStopOnOverflow(m_instance->shouldStopOnConsoleOverflow()); // FIXME: should this really be here? - m_logModel->setOverflowMessage(tr("MultiMC stopped watching the game log because the log length surpassed %1 lines.\n" + m_logModel->setOverflowMessage(tr("PolyMC stopped watching the game log because the log length surpassed %1 lines.\n" "You may have to fix your mods because the game is still logging to files and" " likely wasting harddrive space at an alarming rate!").arg(m_logModel->getMaxLines())); } @@ -277,4 +277,3 @@ QString LaunchTask::substituteVariables(const QString &cmd) const } return out; } - diff --git a/launcher/launch/steps/CheckJava.cpp b/launcher/launch/steps/CheckJava.cpp index fb338231..d3f2148c 100644 --- a/launcher/launch/steps/CheckJava.cpp +++ b/launcher/launch/steps/CheckJava.cpp @@ -87,14 +87,14 @@ void CheckJava::checkJavaFinished(JavaCheckResult result) // Error message displayed if java can't start emit logLine(QString("Could not start java:"), MessageLevel::Error); emit logLines(result.errorLog.split('\n'), MessageLevel::Error); - emit logLine("\nCheck your MultiMC Java settings.", MessageLevel::Launcher); + emit logLine("\nCheck your PolyMC Java settings.", MessageLevel::Launcher); printSystemInfo(false, false); emitFailed(QString("Could not start java!")); return; } case JavaCheckResult::Validity::ReturnedInvalidData: { - emit logLine(QString("Java checker returned some invalid data MultiMC doesn't understand:"), MessageLevel::Error); + emit logLine(QString("Java checker returned some invalid data PolyMC doesn't understand:"), MessageLevel::Error); emit logLines(result.outLog.split('\n'), MessageLevel::Warning); emit logLine("\nMinecraft might not start properly.", MessageLevel::Launcher); printSystemInfo(false, false); diff --git a/launcher/minecraft/Library.h b/launcher/minecraft/Library.h index 41d41a8b..0740a7ca 100644 --- a/launcher/minecraft/Library.h +++ b/launcher/minecraft/Library.h @@ -156,7 +156,7 @@ public: /* methods */ QStringList & failedLocalFiles, const QString & overridePath) const; private: /* methods */ - /// the default storage prefix used by MultiMC + /// the default storage prefix used by PolyMC static QString defaultStoragePrefix(); /// Get the prefix - root of the storage to be used @@ -177,23 +177,23 @@ protected: /* data */ /// DEPRECATED URL prefix of the maven repo where the file can be downloaded QString m_repositoryURL; - /// DEPRECATED: MultiMC-specific absolute URL. takes precedence over the implicit maven repo URL, if defined + /// DEPRECATED: PolyMC-specific absolute URL. takes precedence over the implicit maven repo URL, if defined QString m_absoluteURL; - /// MultiMC extension - filename override + /// PolyMC extension - filename override QString m_filename; - /// DEPRECATED MultiMC extension - display name + /// DEPRECATED PolyMC extension - display name QString m_displayname; /** - * MultiMC-specific type hint - modifies how the library is treated + * PolyMC-specific type hint - modifies how the library is treated */ QString m_hint; /** - * storage - by default the local libraries folder in multimc, but could be elsewhere - * MultiMC specific, because of FTB. + * storage - by default the local libraries folder in polymc, but could be elsewhere + * PolyMC specific, because of FTB. */ QString m_storagePrefix; @@ -215,3 +215,4 @@ protected: /* data */ /// MOJANG: container with Mojang style download info MojangLibraryDownloadInfo::Ptr m_mojangDownloads; }; + diff --git a/launcher/minecraft/MinecraftInstance.cpp b/launcher/minecraft/MinecraftInstance.cpp index 0b3c049b..7327f9d5 100644 --- a/launcher/minecraft/MinecraftInstance.cpp +++ b/launcher/minecraft/MinecraftInstance.cpp @@ -445,7 +445,7 @@ QStringList MinecraftInstance::processMinecraftArgs( } // blatant self-promotion. - token_mapping["profile_name"] = token_mapping["version_name"] = "MultiMC5"; + token_mapping["profile_name"] = token_mapping["version_name"] = "PolyMC"; token_mapping["version_type"] = profile->getMinecraftVersionType(); diff --git a/launcher/minecraft/MinecraftLoadAndCheck.h b/launcher/minecraft/MinecraftLoadAndCheck.h index bfeae46b..d9af3ace 100644 --- a/launcher/minecraft/MinecraftLoadAndCheck.h +++ b/launcher/minecraft/MinecraftLoadAndCheck.h @@ -20,7 +20,7 @@ #include <QUrl> #include "tasks/Task.h" -#include <quazip.h> +#include <quazip/quazip.h> #include "QObjectPtr.h" diff --git a/launcher/minecraft/MinecraftUpdate.h b/launcher/minecraft/MinecraftUpdate.h index fadebff9..9ebef656 100644 --- a/launcher/minecraft/MinecraftUpdate.h +++ b/launcher/minecraft/MinecraftUpdate.h @@ -22,7 +22,7 @@ #include "net/NetJob.h" #include "tasks/Task.h" #include "minecraft/VersionFilterData.h" -#include <quazip.h> +#include <quazip/quazip.h> class MinecraftVersion; class MinecraftInstance; diff --git a/launcher/minecraft/VersionFile.h b/launcher/minecraft/VersionFile.h index b79fcd4f..239a4069 100644 --- a/launcher/minecraft/VersionFile.h +++ b/launcher/minecraft/VersionFile.h @@ -27,19 +27,19 @@ public: /* methods */ void applyTo(LaunchProfile* profile); public: /* data */ - /// MultiMC: order hint for this version file if no explicit order is set + /// PolyMC: order hint for this version file if no explicit order is set int order = 0; - /// MultiMC: human readable name of this package + /// PolyMC: human readable name of this package QString name; - /// MultiMC: package ID of this package + /// PolyMC: package ID of this package QString uid; - /// MultiMC: version of this package + /// PolyMC: version of this package QString version; - /// MultiMC: DEPRECATED dependency on a Minecraft version + /// PolyMC: DEPRECATED dependency on a Minecraft version QString dependsOnMinecraftVersion; /// Mojang: DEPRECATED used to version the Mojang version format @@ -51,7 +51,7 @@ public: /* data */ /// Mojang: class to launch Minecraft with QString mainClass; - /// MultiMC: class to launch legacy Minecraft with (embed in a custom window) + /// PolyMC: class to launch legacy Minecraft with (embed in a custom window) QString appletClass; /// Mojang: Minecraft launch arguments (may contain placeholders for variable substitution) @@ -69,35 +69,35 @@ public: /* data */ /// Mojang: DEPRECATED asset group to be used with Minecraft QString assets; - /// MultiMC: list of tweaker mod arguments for launchwrapper + /// PolyMC: list of tweaker mod arguments for launchwrapper QStringList addTweakers; /// Mojang: list of libraries to add to the version QList<LibraryPtr> libraries; - /// MultiMC: list of maven files to put in the libraries folder, but not in classpath + /// PolyMC: list of maven files to put in the libraries folder, but not in classpath QList<LibraryPtr> mavenFiles; /// The main jar (Minecraft version library, normally) LibraryPtr mainJar; - /// MultiMC: list of attached traits of this version file - used to enable features + /// PolyMC: list of attached traits of this version file - used to enable features QSet<QString> traits; - /// MultiMC: list of jar mods added to this version + /// PolyMC: list of jar mods added to this version QList<LibraryPtr> jarMods; - /// MultiMC: list of mods added to this version + /// PolyMC: list of mods added to this version QList<LibraryPtr> mods; /** - * MultiMC: set of packages this depends on + * PolyMC: set of packages this depends on * NOTE: this is shared with the meta format!!! */ Meta::RequireSet requires; /** - * MultiMC: set of packages this conflicts with + * PolyMC: set of packages this conflicts with * NOTE: this is shared with the meta format!!! */ Meta::RequireSet conflicts; @@ -112,3 +112,4 @@ public: // Mojang: extended asset index download information std::shared_ptr<MojangAssetIndexInfo> mojangAssetIndex; }; + diff --git a/launcher/minecraft/World.cpp b/launcher/minecraft/World.cpp index a2b4dac7..2937c116 100644 --- a/launcher/minecraft/World.cpp +++ b/launcher/minecraft/World.cpp @@ -26,9 +26,9 @@ #include <io/stream_reader.h> #include <tag_string.h> #include <tag_primitive.h> -#include <quazip.h> -#include <quazipfile.h> -#include <quazipdir.h> +#include <quazip/quazip.h> +#include <quazip/quazipfile.h> +#include <quazip/quazipdir.h> #include <QCoreApplication> diff --git a/launcher/minecraft/auth/AccountData.cpp b/launcher/minecraft/auth/AccountData.cpp index 7526c951..9b84fe1a 100644 --- a/launcher/minecraft/auth/AccountData.cpp +++ b/launcher/minecraft/auth/AccountData.cpp @@ -314,6 +314,8 @@ bool AccountData::resumeStateFromV3(QJsonObject data) { type = AccountType::MSA; } else if (typeS == "Mojang") { type = AccountType::Mojang; + } else if (typeS == "Offline") { + type = AccountType::Offline; } else { qWarning() << "Failed to parse account data: type is not recognized."; return false; @@ -363,6 +365,9 @@ QJsonObject AccountData::saveState() const { tokenToJSONV3(output, xboxApiToken, "xrp-main"); tokenToJSONV3(output, mojangservicesToken, "xrp-mc"); } + else if (type == AccountType::Offline) { + output["type"] = "Offline"; + } tokenToJSONV3(output, yggdrasilToken, "ygg"); profileToJSONV3(output, minecraftProfile, "profile"); @@ -371,7 +376,7 @@ QJsonObject AccountData::saveState() const { } QString AccountData::userName() const { - if(type != AccountType::Mojang) { + if(type == AccountType::MSA) { return QString(); } return yggdrasilToken.extra["userName"].toString(); @@ -427,6 +432,9 @@ QString AccountData::accountDisplayString() const { case AccountType::Mojang: { return userName(); } + case AccountType::Offline: { + return userName(); + } case AccountType::MSA: { if(xboxApiToken.extra.contains("gtg")) { return xboxApiToken.extra["gtg"].toString(); diff --git a/launcher/minecraft/auth/AccountData.h b/launcher/minecraft/auth/AccountData.h index abf84e43..606c1ad1 100644 --- a/launcher/minecraft/auth/AccountData.h +++ b/launcher/minecraft/auth/AccountData.h @@ -38,7 +38,8 @@ struct MinecraftProfile { enum class AccountType { MSA, - Mojang + Mojang, + Offline }; enum class AccountState { diff --git a/launcher/minecraft/auth/AccountList.cpp b/launcher/minecraft/auth/AccountList.cpp index ef8b435d..04470e1c 100644 --- a/launcher/minecraft/auth/AccountList.cpp +++ b/launcher/minecraft/auth/AccountList.cpp @@ -302,7 +302,7 @@ QVariant AccountList::data(const QModelIndex &index, int role) const } case MigrationColumn: { - if(account->isMSA()) { + if(account->isMSA() || account->isOffline()) { return tr("N/A", "Can Migrate?"); } if (account->canMigrate()) { diff --git a/launcher/minecraft/auth/AccountList.h b/launcher/minecraft/auth/AccountList.h index fa1e7431..025926ae 100644 --- a/launcher/minecraft/auth/AccountList.h +++ b/launcher/minecraft/auth/AccountList.h @@ -24,7 +24,7 @@ /*! * List of available Mojang accounts. - * This should be loaded in the background by MultiMC on startup. + * This should be loaded in the background by PolyMC on startup. */ class AccountList : public QAbstractListModel { @@ -158,3 +158,4 @@ protected: */ bool m_autosave = false; }; + diff --git a/launcher/minecraft/auth/AuthRequest.cpp b/launcher/minecraft/auth/AuthRequest.cpp index 459d2354..feface80 100644 --- a/launcher/minecraft/auth/AuthRequest.cpp +++ b/launcher/minecraft/auth/AuthRequest.cpp @@ -44,7 +44,7 @@ void AuthRequest::onRequestFinished() { if (reply_ != qobject_cast<QNetworkReply *>(sender())) { return; } - httpStatus_ = 200; + httpStatus_ = reply_->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); finish(); } diff --git a/launcher/minecraft/auth/MinecraftAccount.cpp b/launcher/minecraft/auth/MinecraftAccount.cpp index ed9e945e..ffc81ed8 100644 --- a/launcher/minecraft/auth/MinecraftAccount.cpp +++ b/launcher/minecraft/auth/MinecraftAccount.cpp @@ -30,6 +30,7 @@ #include "flows/MSA.h" #include "flows/Mojang.h" +#include "flows/Offline.h" MinecraftAccount::MinecraftAccount(QObject* parent) : QObject(parent) { data.internalId = QUuid::createUuid().toString().remove(QRegExp("[{}-]")); @@ -68,6 +69,23 @@ MinecraftAccountPtr MinecraftAccount::createBlankMSA() return account; } +MinecraftAccountPtr MinecraftAccount::createOffline(const QString &username) +{ + MinecraftAccountPtr account = new MinecraftAccount(); + account->data.type = AccountType::Offline; + account->data.yggdrasilToken.token = "offline"; + account->data.yggdrasilToken.validity = Katabasis::Validity::Certain; + account->data.yggdrasilToken.issueInstant = QDateTime::currentDateTimeUtc(); + account->data.yggdrasilToken.extra["userName"] = username; + account->data.yggdrasilToken.extra["clientToken"] = QUuid::createUuid().toString().remove(QRegExp("[{}-]")); + account->data.minecraftEntitlement.ownsMinecraft = true; + account->data.minecraftEntitlement.canPlayMinecraft = true; + account->data.minecraftProfile.id = QUuid::createUuid().toString().remove(QRegExp("[{}-]")); + account->data.minecraftProfile.name = username; + account->data.minecraftProfile.validity = Katabasis::Validity::Certain; + return account; +} + QJsonObject MinecraftAccount::saveToJson() const { @@ -111,6 +129,16 @@ shared_qobject_ptr<AccountTask> MinecraftAccount::loginMSA() { return m_currentTask; } +shared_qobject_ptr<AccountTask> MinecraftAccount::loginOffline() { + Q_ASSERT(m_currentTask.get() == nullptr); + + m_currentTask.reset(new OfflineLogin(&data)); + connect(m_currentTask.get(), SIGNAL(succeeded()), SLOT(authSucceeded())); + connect(m_currentTask.get(), SIGNAL(failed(QString)), SLOT(authFailed(QString))); + emit activityChanged(true); + return m_currentTask; +} + shared_qobject_ptr<AccountTask> MinecraftAccount::refresh() { if(m_currentTask) { return m_currentTask; @@ -119,6 +147,9 @@ shared_qobject_ptr<AccountTask> MinecraftAccount::refresh() { if(data.type == AccountType::MSA) { m_currentTask.reset(new MSASilent(&data)); } + else if(data.type == AccountType::Offline) { + m_currentTask.reset(new OfflineRefresh(&data)); + } else { m_currentTask.reset(new MojangRefresh(&data)); } diff --git a/launcher/minecraft/auth/MinecraftAccount.h b/launcher/minecraft/auth/MinecraftAccount.h index 4ac0a3e5..6592f9c0 100644 --- a/launcher/minecraft/auth/MinecraftAccount.h +++ b/launcher/minecraft/auth/MinecraftAccount.h @@ -41,7 +41,7 @@ Q_DECLARE_METATYPE(MinecraftAccountPtr) * A profile within someone's Mojang account. * * Currently, the profile system has not been implemented by Mojang yet, - * but we might as well add some things for it in MultiMC right now so + * but we might as well add some things for it in PolyMC right now so * we don't have to rip the code to pieces to add it later. */ struct AccountProfile @@ -73,6 +73,8 @@ public: /* construction */ static MinecraftAccountPtr createBlankMSA(); + static MinecraftAccountPtr createOffline(const QString &username); + static MinecraftAccountPtr loadFromJsonV2(const QJsonObject &json); static MinecraftAccountPtr loadFromJsonV3(const QJsonObject &json); @@ -89,6 +91,8 @@ public: /* manipulation */ shared_qobject_ptr<AccountTask> loginMSA(); + shared_qobject_ptr<AccountTask> loginOffline(); + shared_qobject_ptr<AccountTask> refresh(); shared_qobject_ptr<AccountTask> currentTask(); @@ -128,6 +132,10 @@ public: /* queries */ return data.type == AccountType::MSA; } + bool isOffline() const { + return data.type == AccountType::Offline; + } + bool ownsMinecraft() const { return data.minecraftEntitlement.ownsMinecraft; } @@ -149,6 +157,10 @@ public: /* queries */ return "msa"; } break; + case AccountType::Offline: { + return "offline"; + } + break; default: { return "unknown"; } @@ -198,3 +210,4 @@ slots: void authSucceeded(); void authFailed(QString reason); }; + diff --git a/launcher/minecraft/auth/Parsers.cpp b/launcher/minecraft/auth/Parsers.cpp index ed31e934..2dd36562 100644 --- a/launcher/minecraft/auth/Parsers.cpp +++ b/launcher/minecraft/auth/Parsers.cpp @@ -94,7 +94,7 @@ bool parseXTokenResponse(QByteArray & data, Katabasis::Token &output, QString na return false; } if(!getString(obj.value("Token"), output.token)) { - qWarning() << "User Token is not a timestamp"; + qWarning() << "User Token is not a string"; return false; } auto arrayVal = obj.value("DisplayClaims").toObject().value("xui"); diff --git a/launcher/minecraft/auth/flows/Offline.cpp b/launcher/minecraft/auth/flows/Offline.cpp new file mode 100644 index 00000000..fc614a8c --- /dev/null +++ b/launcher/minecraft/auth/flows/Offline.cpp @@ -0,0 +1,17 @@ +#include "Offline.h" + +#include "minecraft/auth/steps/OfflineStep.h" + +OfflineRefresh::OfflineRefresh( + AccountData *data, + QObject *parent +) : AuthFlow(data, parent) { + m_steps.append(new OfflineStep(m_data)); +} + +OfflineLogin::OfflineLogin( + AccountData *data, + QObject *parent +) : AuthFlow(data, parent) { + m_steps.append(new OfflineStep(m_data)); +} diff --git a/launcher/minecraft/auth/flows/Offline.h b/launcher/minecraft/auth/flows/Offline.h new file mode 100644 index 00000000..5d1f83a4 --- /dev/null +++ b/launcher/minecraft/auth/flows/Offline.h @@ -0,0 +1,22 @@ +#pragma once +#include "AuthFlow.h" + +class OfflineRefresh : public AuthFlow +{ + Q_OBJECT +public: + explicit OfflineRefresh( + AccountData *data, + QObject *parent = 0 + ); +}; + +class OfflineLogin : public AuthFlow +{ + Q_OBJECT +public: + explicit OfflineLogin( + AccountData *data, + QObject *parent = 0 + ); +}; diff --git a/launcher/minecraft/auth/steps/MSAStep.cpp b/launcher/minecraft/auth/steps/MSAStep.cpp index bc10aa4e..779aee43 100644 --- a/launcher/minecraft/auth/steps/MSAStep.cpp +++ b/launcher/minecraft/auth/steps/MSAStep.cpp @@ -14,7 +14,7 @@ using Activity = Katabasis::Activity; MSAStep::MSAStep(AccountData* data, Action action) : AuthStep(data), m_action(action) { OAuth2::Options opts; opts.scope = "XboxLive.signin offline_access"; - opts.clientIdentifier = BuildConfig.MSA_CLIENT_ID; + opts.clientIdentifier = APPLICATION->getMSAClientID(); opts.authorizationUrl = "https://login.microsoftonline.com/consumers/oauth2/v2.0/devicecode"; opts.accessTokenUrl = "https://login.microsoftonline.com/consumers/oauth2/v2.0/token"; diff --git a/launcher/minecraft/auth/steps/MinecraftProfileStep.cpp b/launcher/minecraft/auth/steps/MinecraftProfileStep.cpp index 9fef99b0..add91659 100644 --- a/launcher/minecraft/auth/steps/MinecraftProfileStep.cpp +++ b/launcher/minecraft/auth/steps/MinecraftProfileStep.cpp @@ -56,6 +56,14 @@ void MinecraftProfileStep::onRequestDone( return; } if (error != QNetworkReply::NoError) { + qWarning() << "Error getting profile:"; + qWarning() << " HTTP Status: " << requestor->httpStatus_; + qWarning() << " Internal error no.: " << error; + qWarning() << " Error string: " << requestor->errorString_; + + qWarning() << " Response:"; + qWarning() << QString::fromUtf8(data); + emit finished( AccountTaskState::STATE_FAILED_SOFT, tr("Minecraft Java profile acquisition failed.") diff --git a/launcher/minecraft/auth/steps/OfflineStep.cpp b/launcher/minecraft/auth/steps/OfflineStep.cpp new file mode 100644 index 00000000..dc092bfd --- /dev/null +++ b/launcher/minecraft/auth/steps/OfflineStep.cpp @@ -0,0 +1,18 @@ +#include "OfflineStep.h" + +#include "Application.h" + +OfflineStep::OfflineStep(AccountData* data) : AuthStep(data) {} +OfflineStep::~OfflineStep() noexcept = default; + +QString OfflineStep::describe() { + return tr("Creating offline account."); +} + +void OfflineStep::rehydrate() { + // NOOP +} + +void OfflineStep::perform() { + emit finished(AccountTaskState::STATE_WORKING, tr("Created offline account.")); +} diff --git a/launcher/minecraft/auth/steps/OfflineStep.h b/launcher/minecraft/auth/steps/OfflineStep.h new file mode 100644 index 00000000..436597cd --- /dev/null +++ b/launcher/minecraft/auth/steps/OfflineStep.h @@ -0,0 +1,19 @@ +#pragma once +#include <QObject> + +#include "QObjectPtr.h" +#include "minecraft/auth/AuthStep.h" + +#include <katabasis/DeviceFlow.h> + +class OfflineStep : public AuthStep { + Q_OBJECT +public: + explicit OfflineStep(AccountData *data); + virtual ~OfflineStep() noexcept; + + void perform() override; + void rehydrate() override; + + QString describe() override; +}; diff --git a/launcher/minecraft/launch/ExtractNatives.cpp b/launcher/minecraft/launch/ExtractNatives.cpp index 8cd439b1..7d5f4179 100644 --- a/launcher/minecraft/launch/ExtractNatives.cpp +++ b/launcher/minecraft/launch/ExtractNatives.cpp @@ -17,8 +17,8 @@ #include <minecraft/MinecraftInstance.h> #include <launch/LaunchTask.h> -#include <quazip.h> -#include <quazipdir.h> +#include <quazip/quazip.h> +#include <quazip/quazipdir.h> #include "MMCZip.h" #include "FileSystem.h" #include <QDir> diff --git a/launcher/minecraft/launch/LauncherPartLaunch.cpp b/launcher/minecraft/launch/LauncherPartLaunch.cpp index 8fd11eca..f461b847 100644 --- a/launcher/minecraft/launch/LauncherPartLaunch.cpp +++ b/launcher/minecraft/launch/LauncherPartLaunch.cpp @@ -25,6 +25,19 @@ LauncherPartLaunch::LauncherPartLaunch(LaunchTask *parent) : LaunchStep(parent) { + if (APPLICATION->settings()->get("CloseAfterLaunch").toBool()) + { + std::shared_ptr<QMetaObject::Connection> connection{new QMetaObject::Connection}; + *connection = connect(&m_process, &LoggedProcess::log, this, [=](QStringList lines, MessageLevel::Enum level) { + qDebug() << lines; + if (lines.filter(QRegularExpression(".*Setting user.+", QRegularExpression::CaseInsensitiveOption)).length() != 0) + { + APPLICATION->closeAllWindows(); + disconnect(*connection); + } + }); + } + connect(&m_process, &LoggedProcess::log, this, &LauncherPartLaunch::logLines); connect(&m_process, &LoggedProcess::stateChanged, this, &LauncherPartLaunch::on_state); } @@ -155,6 +168,8 @@ void LauncherPartLaunch::on_state(LoggedProcess::State state) } case LoggedProcess::Finished: { + if (APPLICATION->settings()->get("CloseAfterLaunch").toBool()) + APPLICATION->showMainWindow(); m_parent->setPid(-1); // if the exit code wasn't 0, report this as a crash auto exitCode = m_process.exitCode(); diff --git a/launcher/minecraft/launch/MinecraftServerTarget.cpp b/launcher/minecraft/launch/MinecraftServerTarget.cpp index 0f98f356..78a33359 100644 --- a/launcher/minecraft/launch/MinecraftServerTarget.cpp +++ b/launcher/minecraft/launch/MinecraftServerTarget.cpp @@ -23,7 +23,7 @@ MinecraftServerTarget MinecraftServerTarget::parse(const QString &fullAddress) { // The logic below replicates the exact logic minecraft uses for parsing server addresses. // While the conversion is not lossless and eats errors, it ensures the same behavior - // within Minecraft and MultiMC when entering server addresses. + // within Minecraft and PolyMC when entering server addresses. if (fullAddress.startsWith("[")) { int bracket = fullAddress.indexOf("]"); diff --git a/launcher/minecraft/mod/LocalModParseTask.cpp b/launcher/minecraft/mod/LocalModParseTask.cpp index 8ac5885f..757a2187 100644 --- a/launcher/minecraft/mod/LocalModParseTask.cpp +++ b/launcher/minecraft/mod/LocalModParseTask.cpp @@ -4,8 +4,8 @@ #include <QJsonObject> #include <QJsonArray> #include <QJsonValue> -#include <quazip.h> -#include <quazipfile.h> +#include <quazip/quazip.h> +#include <quazip/quazipfile.h> #include <toml.h> #include "settings/INIFile.h" diff --git a/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp b/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp index e5db512e..8de5fc9f 100644 --- a/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp +++ b/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp @@ -19,7 +19,7 @@ #include <QtConcurrent/QtConcurrent> -#include <quazip.h> +#include <quazip/quazip.h> #include "MMCZip.h" #include "minecraft/OneSixVersionFormat.h" diff --git a/launcher/modplatform/flame/FlameModIndex.cpp b/launcher/modplatform/flame/FlameModIndex.cpp new file mode 100644 index 00000000..a8b2495a --- /dev/null +++ b/launcher/modplatform/flame/FlameModIndex.cpp @@ -0,0 +1,100 @@ +#include <QObject> +#include "FlameModIndex.h" +#include "Json.h" +#include "net/NetJob.h" +#include "BaseInstance.h" +#include "minecraft/MinecraftInstance.h" +#include "minecraft/PackProfile.h" + + +void FlameMod::loadIndexedPack(FlameMod::IndexedPack & pack, QJsonObject & obj) +{ + pack.addonId = Json::requireInteger(obj, "id"); + pack.name = Json::requireString(obj, "name"); + pack.websiteUrl = Json::ensureString(obj, "websiteUrl", ""); + pack.description = Json::ensureString(obj, "summary", ""); + + bool thumbnailFound = false; + auto attachments = Json::requireArray(obj, "attachments"); + for(auto attachmentRaw: attachments) { + auto attachmentObj = Json::requireObject(attachmentRaw); + bool isDefault = attachmentObj.value("isDefault").toBool(false); + if(isDefault) { + thumbnailFound = true; + pack.logoName = Json::requireString(attachmentObj, "title"); + pack.logoUrl = Json::requireString(attachmentObj, "thumbnailUrl"); + break; + } + } + + if(!thumbnailFound) { + throw JSONValidationError(QString("Pack without an icon, skipping: %1").arg(pack.name)); + } + + + auto authors = Json::requireArray(obj, "authors"); + for(auto authorIter: authors) { + auto author = Json::requireObject(authorIter); + FlameMod::ModpackAuthor packAuthor; + packAuthor.name = Json::requireString(author, "name"); + packAuthor.url = Json::requireString(author, "url"); + pack.authors.append(packAuthor); + } +} + +void FlameMod::loadIndexedPackVersions(FlameMod::IndexedPack & pack, QJsonArray & arr, const shared_qobject_ptr<QNetworkAccessManager>& network, BaseInstance * inst) +{ + QVector<FlameMod::IndexedVersion> unsortedVersions; + bool hasFabric = !((MinecraftInstance *)inst)->getPackProfile()->getComponentVersion("net.fabricmc.fabric-loader").isEmpty(); + QString mcVersion = ((MinecraftInstance *)inst)->getPackProfile()->getComponentVersion("net.minecraft"); + + for(auto versionIter: arr) { + auto obj = versionIter.toObject(); + FlameMod::IndexedVersion file; + file.addonId = pack.addonId; + file.fileId = Json::requireInteger(obj, "id"); + file.date = Json::requireString(obj, "fileDate"); + auto versionArray = Json::requireArray(obj, "gameVersion"); + if (versionArray.empty()) { + continue; + } + for(auto mcVer : versionArray){ + file.mcVersion.append(mcVer.toString()); + } + + file.version = Json::requireString(obj, "displayName"); + file.downloadUrl = Json::requireString(obj, "downloadUrl"); + file.fileName = Json::requireString(obj, "fileName"); + + auto modules = Json::requireArray(obj, "modules"); + bool valid = false; + for(auto m : modules){ + auto fname = Json::requireString(m.toObject(),"foldername"); + if(hasFabric){ + if(fname == "fabric.mod.json"){ + valid = true; + break; + } + }else{ + //this cannot check for the recent mcmod.toml formats + if(fname == "mcmod.info"){ + valid = true; + break; + } + } + } + if(!valid && hasFabric){ + continue; + } + + unsortedVersions.append(file); + } + auto orderSortPredicate = [](const IndexedVersion & a, const IndexedVersion & b) -> bool + { + //dates are in RFC 3339 format + return a.date > b.date; + }; + std::sort(unsortedVersions.begin(), unsortedVersions.end(), orderSortPredicate); + pack.versions = unsortedVersions; + pack.versionsLoaded = true; +} diff --git a/launcher/modplatform/flame/FlameModIndex.h b/launcher/modplatform/flame/FlameModIndex.h new file mode 100644 index 00000000..0293bb23 --- /dev/null +++ b/launcher/modplatform/flame/FlameModIndex.h @@ -0,0 +1,50 @@ +// +// Created by timoreo on 16/01/2022. +// + +#pragma once +#include <QList> +#include <QMetaType> +#include <QString> +#include <QVector> +#include <QNetworkAccessManager> +#include <QObjectPtr.h> +#include "net/NetJob.h" +#include "BaseInstance.h" + +namespace FlameMod { + struct ModpackAuthor { + QString name; + QString url; + }; + + struct IndexedVersion { + int addonId; + int fileId; + QString version; + QVector<QString> mcVersion; + QString downloadUrl; + QString date; + QString fileName; + }; + + struct IndexedPack + { + int addonId; + QString name; + QString description; + QList<ModpackAuthor> authors; + QString logoName; + QString logoUrl; + QString websiteUrl; + + bool versionsLoaded = false; + QVector<IndexedVersion> versions; + }; + + void loadIndexedPack(IndexedPack & m, QJsonObject & obj); + void loadIndexedPackVersions(IndexedPack &pack, QJsonArray &arr, const shared_qobject_ptr<QNetworkAccessManager> &network, BaseInstance *inst); + +} + +Q_DECLARE_METATYPE(FlameMod::IndexedPack) diff --git a/launcher/modplatform/legacy_ftb/PackInstallTask.h b/launcher/modplatform/legacy_ftb/PackInstallTask.h index 305635a1..a7395220 100644 --- a/launcher/modplatform/legacy_ftb/PackInstallTask.h +++ b/launcher/modplatform/legacy_ftb/PackInstallTask.h @@ -1,8 +1,8 @@ #pragma once #include "InstanceTask.h" #include "net/NetJob.h" -#include "quazip.h" -#include "quazipdir.h" +#include <quazip/quazip.h> +#include <quazip/quazipdir.h> #include "meta/Index.h" #include "meta/Version.h" #include "meta/VersionList.h" diff --git a/launcher/modplatform/modrinth/ModrinthPackIndex.cpp b/launcher/modplatform/modrinth/ModrinthPackIndex.cpp new file mode 100644 index 00000000..9017eb67 --- /dev/null +++ b/launcher/modplatform/modrinth/ModrinthPackIndex.cpp @@ -0,0 +1,95 @@ +#include <QObject> +#include "ModrinthPackIndex.h" + +#include "Json.h" +#include "net/NetJob.h" +#include "BaseInstance.h" +#include "minecraft/MinecraftInstance.h" +#include "minecraft/PackProfile.h" + + +void Modrinth::loadIndexedPack(Modrinth::IndexedPack & pack, QJsonObject & obj) +{ + pack.addonId = Json::requireString(obj, "project_id"); + pack.name = Json::requireString(obj, "title"); + pack.websiteUrl = Json::ensureString(obj, "page_url", ""); + pack.description = Json::ensureString(obj, "description", ""); + + pack.logoUrl = Json::requireString(obj, "icon_url"); + pack.logoName = pack.addonId; + + Modrinth::ModpackAuthor modAuthor; + modAuthor.name = Json::requireString(obj, "author"); + modAuthor.url = "https://modrinth.com/user/"+modAuthor.name; + pack.author = modAuthor; +} + +void Modrinth::loadIndexedPackVersions(Modrinth::IndexedPack & pack, QJsonArray & arr, const shared_qobject_ptr<QNetworkAccessManager>& network, BaseInstance * inst) +{ + QVector<Modrinth::IndexedVersion> unsortedVersions; + bool hasFabric = !((MinecraftInstance *)inst)->getPackProfile()->getComponentVersion("net.fabricmc.fabric-loader").isEmpty(); + QString mcVersion = ((MinecraftInstance *)inst)->getPackProfile()->getComponentVersion("net.minecraft"); + + for(auto versionIter: arr) { + auto obj = versionIter.toObject(); + Modrinth::IndexedVersion file; + file.addonId = Json::requireString(obj,"project_id") ; + file.fileId = Json::requireString(obj, "id"); + file.date = Json::requireString(obj, "date_published"); + auto versionArray = Json::requireArray(obj, "game_versions"); + if (versionArray.empty()) { + continue; + } + for(auto mcVer : versionArray){ + file.mcVersion.append(mcVer.toString()); + } + auto loaders = Json::requireArray(obj,"loaders"); + for(auto loader : loaders){ + file.loaders.append(loader.toString()); + } + file.version = Json::requireString(obj, "name"); + + auto files = Json::requireArray(obj, "files"); + int i = 0; + while (files.count() > 1 && i < files.count()){ + //try to resolve the correct file + auto parent = files[i].toObject(); + auto fileName = Json::requireString(parent, "filename"); + //avoid grabbing "dev" files + if(fileName.contains("javadocs",Qt::CaseInsensitive) || fileName.contains("sources",Qt::CaseInsensitive)){ + i++; + continue; + } + //grab the correct mod loader + if(fileName.contains("forge",Qt::CaseInsensitive) || fileName.contains("fabric",Qt::CaseInsensitive) ){ + if(hasFabric){ + if(fileName.contains("forge",Qt::CaseInsensitive)){ + i++; + continue; + } + }else{ + if(fileName.contains("fabric",Qt::CaseInsensitive)){ + i++; + continue; + } + } + } + break; + } + auto parent = files[i].toObject(); + if(parent.contains("url")) { + file.downloadUrl = Json::requireString(parent, "url"); + file.fileName = Json::requireString(parent, "filename"); + + unsortedVersions.append(file); + } + } + auto orderSortPredicate = [](const IndexedVersion & a, const IndexedVersion & b) -> bool + { + //dates are in RFC 3339 format + return a.date > b.date; + }; + std::sort(unsortedVersions.begin(), unsortedVersions.end(), orderSortPredicate); + pack.versions = unsortedVersions; + pack.versionsLoaded = true; +} diff --git a/launcher/modplatform/modrinth/ModrinthPackIndex.h b/launcher/modplatform/modrinth/ModrinthPackIndex.h new file mode 100644 index 00000000..3a4cd270 --- /dev/null +++ b/launcher/modplatform/modrinth/ModrinthPackIndex.h @@ -0,0 +1,48 @@ +#pragma once + +#include <QList> +#include <QMetaType> +#include <QString> +#include <QVector> +#include <QNetworkAccessManager> +#include <QObjectPtr.h> +#include "net/NetJob.h" +#include "BaseInstance.h" + +namespace Modrinth { + +struct ModpackAuthor { + QString name; + QString url; +}; + +struct IndexedVersion { + QString addonId; + QString fileId; + QString version; + QVector<QString> mcVersion; + QString downloadUrl; + QString date; + QString fileName; + QVector<QString> loaders; +}; + +struct IndexedPack +{ + QString addonId; + QString name; + QString description; + ModpackAuthor author; + QString logoName; + QString logoUrl; + QString websiteUrl; + + bool versionsLoaded = false; + QVector<IndexedVersion> versions; +}; + +void loadIndexedPack(IndexedPack & m, QJsonObject & obj); +void loadIndexedPackVersions(IndexedPack &pack, QJsonArray &arr, const shared_qobject_ptr<QNetworkAccessManager> &network, BaseInstance *inst); +} + +Q_DECLARE_METATYPE(Modrinth::IndexedPack) diff --git a/launcher/modplatform/technic/SingleZipPackInstallTask.h b/launcher/modplatform/technic/SingleZipPackInstallTask.h index 74f60941..4d1fcbff 100644 --- a/launcher/modplatform/technic/SingleZipPackInstallTask.h +++ b/launcher/modplatform/technic/SingleZipPackInstallTask.h @@ -18,7 +18,7 @@ #include "InstanceTask.h" #include "net/NetJob.h" -#include "quazip.h" +#include <quazip/quazip.h> #include <QFutureWatcher> #include <QStringList> diff --git a/launcher/modplatform/technic/TechnicPackProcessor.cpp b/launcher/modplatform/technic/TechnicPackProcessor.cpp index 52979b7c..c45061ac 100644 --- a/launcher/modplatform/technic/TechnicPackProcessor.cpp +++ b/launcher/modplatform/technic/TechnicPackProcessor.cpp @@ -19,9 +19,9 @@ #include <Json.h> #include <minecraft/MinecraftInstance.h> #include <minecraft/PackProfile.h> -#include <quazip.h> -#include <quazipdir.h> -#include <quazipfile.h> +#include <quazip/quazip.h> +#include <quazip/quazipdir.h> +#include <quazip/quazipfile.h> #include <settings/INISettingsObject.h> #include <memory> diff --git a/launcher/net/PasteUpload.cpp b/launcher/net/PasteUpload.cpp index 4b69b68a..52b82a0e 100644 --- a/launcher/net/PasteUpload.cpp +++ b/launcher/net/PasteUpload.cpp @@ -8,44 +8,34 @@ #include <QJsonDocument> #include <QFile> -PasteUpload::PasteUpload(QWidget *window, QString text, QString key) : m_window(window) +PasteUpload::PasteUpload(QWidget *window, QString text, QString url) : m_window(window), m_uploadUrl(url), m_text(text.toUtf8()) { - m_key = key; - QByteArray temp; - QJsonObject topLevelObj; - QJsonObject sectionObject; - sectionObject.insert("contents", text); - QJsonArray sectionArray; - sectionArray.append(sectionObject); - topLevelObj.insert("description", "Log Upload"); - topLevelObj.insert("sections", sectionArray); - QJsonDocument docOut; - docOut.setObject(topLevelObj); - m_jsonContent = docOut.toJson(); } PasteUpload::~PasteUpload() { } -bool PasteUpload::validateText() -{ - return m_jsonContent.size() <= maxSize(); -} - void PasteUpload::executeTask() { - QNetworkRequest request(QUrl("https://api.paste.ee/v1/pastes")); + QNetworkRequest request{QUrl(m_uploadUrl)}; request.setHeader(QNetworkRequest::UserAgentHeader, BuildConfig.USER_AGENT_UNCACHED); - request.setRawHeader("Content-Type", "application/json"); - request.setRawHeader("Content-Length", QByteArray::number(m_jsonContent.size())); - request.setRawHeader("X-Auth-Token", m_key.toStdString().c_str()); + QHttpMultiPart *multiPart = new QHttpMultiPart{QHttpMultiPart::FormDataType}; + + QHttpPart filePart; + filePart.setBody(m_text); + filePart.setHeader(QNetworkRequest::ContentTypeHeader, "text/plain"); + filePart.setHeader(QNetworkRequest::ContentDispositionHeader, "form-data; name=\"file\"; filename=\"log.txt\""); - QNetworkReply *rep = APPLICATION->network()->post(request, m_jsonContent); + multiPart->append(filePart); + + QNetworkReply *rep = APPLICATION->network()->post(request, multiPart); + multiPart->setParent(rep); m_reply = std::shared_ptr<QNetworkReply>(rep); - setStatus(tr("Uploading to paste.ee")); + setStatus(tr("Uploading to %1").arg(m_uploadUrl)); + connect(rep, &QNetworkReply::uploadProgress, this, &Task::setProgress); connect(rep, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(downloadError(QNetworkReply::NetworkError))); connect(rep, SIGNAL(finished()), this, SLOT(downloadFinished())); @@ -61,45 +51,23 @@ void PasteUpload::downloadError(QNetworkReply::NetworkError error) void PasteUpload::downloadFinished() { QByteArray data = m_reply->readAll(); - // if the download succeeded - if (m_reply->error() == QNetworkReply::NetworkError::NoError) + int statusCode = m_reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + + if (m_reply->error() != QNetworkReply::NetworkError::NoError) { + emitFailed(tr("Network error: %1").arg(m_reply->errorString())); m_reply.reset(); - QJsonParseError jsonError; - QJsonDocument doc = QJsonDocument::fromJson(data, &jsonError); - if (jsonError.error != QJsonParseError::NoError) - { - emitFailed(jsonError.errorString()); - return; - } - if (!parseResult(doc)) - { - emitFailed(tr("paste.ee returned an error. Please consult the logs for more information")); - return; - } + return; } - // else the download failed - else + else if (statusCode != 200 && statusCode != 201) { - emitFailed(QString("Network error: %1").arg(m_reply->errorString())); + QString reasonPhrase = m_reply->attribute(QNetworkRequest::HttpReasonPhraseAttribute).toString(); + emitFailed(tr("Error: %1 returned unexpected status code %2 %3").arg(m_uploadUrl).arg(statusCode).arg(reasonPhrase)); + qCritical() << m_uploadUrl << " returned unexpected status code " << statusCode << " with body: " << data; m_reply.reset(); return; } - emitSucceeded(); -} -bool PasteUpload::parseResult(QJsonDocument doc) -{ - auto object = doc.object(); - auto status = object.value("success").toBool(); - if (!status) - { - qCritical() << "paste.ee reported error:" << QString(object.value("error").toString()); - return false; - } - m_pasteLink = object.value("link").toString(); - m_pasteID = object.value("id").toString(); - qDebug() << m_pasteLink; - return true; + m_pasteLink = QString::fromUtf8(data).trimmed(); + emitSucceeded(); } - diff --git a/launcher/net/PasteUpload.h b/launcher/net/PasteUpload.h index 5514e058..62b2dc36 100644 --- a/launcher/net/PasteUpload.h +++ b/launcher/net/PasteUpload.h @@ -8,37 +8,21 @@ class PasteUpload : public Task { Q_OBJECT public: - PasteUpload(QWidget *window, QString text, QString key = "public"); + PasteUpload(QWidget *window, QString text, QString url); virtual ~PasteUpload(); QString pasteLink() { return m_pasteLink; } - QString pasteID() - { - return m_pasteID; - } - int maxSize() - { - // 2MB for paste.ee - public - if(m_key == "public") - return 1024*1024*2; - // 12MB for paste.ee - with actual key - return 1024*1024*12; - } - bool validateText(); protected: virtual void executeTask(); private: - bool parseResult(QJsonDocument doc); - QString m_error; QWidget *m_window; - QString m_pasteID; QString m_pasteLink; - QString m_key; - QByteArray m_jsonContent; + QString m_uploadUrl; + QByteArray m_text; std::shared_ptr<QNetworkReply> m_reply; public slots: diff --git a/launcher/news/NewsChecker.cpp b/launcher/news/NewsChecker.cpp index 4f4359b8..6724950f 100644 --- a/launcher/news/NewsChecker.cpp +++ b/launcher/news/NewsChecker.cpp @@ -70,7 +70,7 @@ void NewsChecker::rssDownloadFinished() } // If the parsing succeeded, read it. - QDomNodeList items = doc.elementsByTagName("item"); + QDomNodeList items = doc.elementsByTagName("entry"); m_newsEntries.clear(); for (int i = 0; i < items.length(); i++) { diff --git a/launcher/news/NewsEntry.cpp b/launcher/news/NewsEntry.cpp index 7eff657b..137703d1 100644 --- a/launcher/news/NewsEntry.cpp +++ b/launcher/news/NewsEntry.cpp @@ -24,18 +24,14 @@ NewsEntry::NewsEntry(QObject* parent) : this->title = tr("Untitled"); this->content = tr("No content."); this->link = ""; - this->author = tr("Unknown Author"); - this->pubDate = QDateTime::currentDateTime(); } -NewsEntry::NewsEntry(const QString& title, const QString& content, const QString& link, const QString& author, const QDateTime& pubDate, QObject* parent) : +NewsEntry::NewsEntry(const QString& title, const QString& content, const QString& link, QObject* parent) : QObject(parent) { this->title = title; this->content = content; this->link = link; - this->author = author; - this->pubDate = pubDate; } /*! @@ -59,19 +55,11 @@ bool NewsEntry::fromXmlElement(const QDomElement& element, NewsEntry* entry, QSt { QString title = childValue(element, "title", tr("Untitled")); QString content = childValue(element, "description", tr("No content.")); - QString link = childValue(element, "link"); - QString author = childValue(element, "dc:creator", tr("Unknown Author")); - QString pubDateStr = childValue(element, "pubDate"); - - // FIXME: For now, we're just ignoring timezones. We assume that all time zones in the RSS feed are the same. - QString dateFormat("ddd, dd MMM yyyy hh:mm:ss"); - QDateTime pubDate = QDateTime::fromString(pubDateStr, dateFormat); + QString link = childValue(element, "id"); entry->title = title; entry->content = content; entry->link = link; - entry->author = author; - entry->pubDate = pubDate; return true; } diff --git a/launcher/news/NewsEntry.h b/launcher/news/NewsEntry.h index 0dbc70a5..1fe95623 100644 --- a/launcher/news/NewsEntry.h +++ b/launcher/news/NewsEntry.h @@ -18,8 +18,6 @@ #include <QObject> #include <QString> #include <QDomElement> -#include <QDateTime> - #include <memory> class NewsEntry : public QObject @@ -36,7 +34,7 @@ public: * Constructs a new news entry. * Note that content may contain HTML. */ - NewsEntry(const QString& title, const QString& content, const QString& link, const QString& author, const QDateTime& pubDate, QObject* parent=0); + NewsEntry(const QString& title, const QString& content, const QString& link, QObject* parent=0); /*! * Attempts to load information from the given XML element into the given news entry pointer. @@ -53,12 +51,6 @@ public: //! URL to the post. QString link; - - //! The post's author. - QString author; - - //! The date and time that this post was published. - QDateTime pubDate; }; typedef std::shared_ptr<NewsEntry> NewsEntryPtr; diff --git a/launcher/notifications/NotificationChecker.cpp b/launcher/notifications/NotificationChecker.cpp index c08bcdcb..10b91691 100644 --- a/launcher/notifications/NotificationChecker.cpp +++ b/launcher/notifications/NotificationChecker.cpp @@ -44,7 +44,7 @@ void NotificationChecker::checkForNotifications() if (!m_notificationsUrl.isValid()) { qCritical() << "Failed to check for notifications. No notifications URL set." - << "If you'd like to use MultiMC's notification system, please pass the " + << "If you'd like to use PolyMC's notification system, please pass the " "URL to CMake at compile time."; return; } diff --git a/launcher/resources/multimc/128x128/instances/modrinth.png b/launcher/resources/multimc/128x128/instances/modrinth.png Binary files differnew file mode 100644 index 00000000..740bc8f0 --- /dev/null +++ b/launcher/resources/multimc/128x128/instances/modrinth.png diff --git a/launcher/resources/multimc/32x32/instances/modrinth.png b/launcher/resources/multimc/32x32/instances/modrinth.png Binary files differnew file mode 100644 index 00000000..025ed065 --- /dev/null +++ b/launcher/resources/multimc/32x32/instances/modrinth.png diff --git a/launcher/resources/multimc/multimc.qrc b/launcher/resources/multimc/multimc.qrc index 58b1d763..ef29cf9b 100644 --- a/launcher/resources/multimc/multimc.qrc +++ b/launcher/resources/multimc/multimc.qrc @@ -268,6 +268,9 @@ <file>32x32/instances/flame.png</file> <file>128x128/instances/flame.png</file> + <file>32x32/instances/modrinth.png</file> + <file>128x128/instances/modrinth.png</file> + <file>32x32/instances/gear.png</file> <file>128x128/instances/gear.png</file> diff --git a/launcher/settings/Setting.h b/launcher/settings/Setting.h index 9beeb35e..9a5b8210 100644 --- a/launcher/settings/Setting.h +++ b/launcher/settings/Setting.h @@ -33,7 +33,7 @@ public: * Construct a Setting * * Synonyms are all the possible names used in the settings object, in order of preference. - * First synonym is the ID, which identifies the setting in MultiMC. + * First synonym is the ID, which identifies the setting in PolyMC. * * defVal is the default value that will be returned when the settings object * doesn't have any value for this setting. @@ -115,3 +115,4 @@ protected: QStringList m_synonyms; QVariant m_defVal; }; + diff --git a/launcher/tools/MCEditTool.cpp b/launcher/tools/MCEditTool.cpp index 21e1a3b0..2c1ec613 100644 --- a/launcher/tools/MCEditTool.cpp +++ b/launcher/tools/MCEditTool.cpp @@ -52,7 +52,7 @@ QString MCEditTool::getProgramPath() #else const QString mceditPath = path(); QDir mceditDir(mceditPath); -#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) +#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) || defined(Q_OS_OPENBSD) if (mceditDir.exists("mcedit.sh")) { return mceditDir.absoluteFilePath("mcedit.sh"); diff --git a/launcher/translations/TranslationsModel.cpp b/launcher/translations/TranslationsModel.cpp index 2e744007..250854d3 100644 --- a/launcher/translations/TranslationsModel.cpp +++ b/launcher/translations/TranslationsModel.cpp @@ -143,6 +143,11 @@ struct TranslationsModel::Private std::unique_ptr<POTranslator> m_po_translator; QFileSystemWatcher *watcher; + + const QString m_system_locale = QLocale::system().name(); + const QString m_system_language = m_system_locale.split('_').front(); + + bool no_language_set = false; }; TranslationsModel::TranslationsModel(QString path, QObject* parent): QAbstractListModel(parent) @@ -164,7 +169,10 @@ TranslationsModel::~TranslationsModel() void TranslationsModel::translationDirChanged(const QString& path) { qDebug() << "Dir changed:" << path; - reloadLocalFiles(); + if (!d->no_language_set) + { + reloadLocalFiles(); + } selectLanguage(selectedLanguage()); } @@ -172,7 +180,26 @@ void TranslationsModel::indexReceived() { qDebug() << "Got translations index!"; d->m_index_job.reset(); - if(d->m_selectedLanguage != defaultLangCode) + + if (d->no_language_set) + { + reloadLocalFiles(); + + auto language = d->m_system_locale; + if (!findLanguage(language)) + { + language = d->m_system_language; + } + selectLanguage(language); + if (selectedLanguage() != defaultLangCode) + { + updateLanguage(selectedLanguage()); + } + APPLICATION->settings()->set("Language", selectedLanguage()); + d->no_language_set = false; + } + + else if(d->m_selectedLanguage != defaultLangCode) { downloadTranslation(d->m_selectedLanguage); } @@ -319,8 +346,19 @@ void TranslationsModel::reloadLocalFiles() { d->m_languages.append(language); } - std::sort(d->m_languages.begin(), d->m_languages.end(), [](const Language& a, const Language& b) { - return a.key.compare(b.key) < 0; + std::sort(d->m_languages.begin(), d->m_languages.end(), [this](const Language& a, const Language& b) { + if (a.key != b.key) + { + if (a.key == d->m_system_locale || a.key == d->m_system_language) + { + return true; + } + if (b.key == d->m_system_locale || b.key == d->m_system_language) + { + return false; + } + } + return a.key < b.key; }); endInsertRows(); } @@ -439,6 +477,12 @@ bool TranslationsModel::selectLanguage(QString key) { QString &langCode = key; auto langPtr = findLanguage(key); + + if (langCode.isEmpty()) + { + d->no_language_set = true; + } + if(!langPtr) { qWarning() << "Selected invalid language" << key << ", defaulting to" << defaultLangCode; @@ -576,7 +620,7 @@ void TranslationsModel::downloadIndex() d->m_index_job = 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("https://files.multimc.org/translations/index_v2.json"), entry); + d->m_index_task = Net::Download::makeCached(QUrl(BuildConfig.TRANSLATIONS_BASE_URL + "index_v2.json"), entry); d->m_index_job->addNetAction(d->m_index_task); connect(d->m_index_job.get(), &NetJob::failed, this, &TranslationsModel::indexFailed); connect(d->m_index_job.get(), &NetJob::succeeded, this, &TranslationsModel::indexReceived); diff --git a/launcher/ui/GuiUtil.cpp b/launcher/ui/GuiUtil.cpp index efb1a4df..9eb658e2 100644 --- a/launcher/ui/GuiUtil.cpp +++ b/launcher/ui/GuiUtil.cpp @@ -16,21 +16,8 @@ QString GuiUtil::uploadPaste(const QString &text, QWidget *parentWidget) { ProgressDialog dialog(parentWidget); - auto APIKeySetting = APPLICATION->settings()->get("PasteEEAPIKey").toString(); - if(APIKeySetting == "multimc") - { - APIKeySetting = BuildConfig.PASTE_EE_KEY; - } - std::unique_ptr<PasteUpload> paste(new PasteUpload(parentWidget, text, APIKeySetting)); - - if (!paste->validateText()) - { - CustomMessageBox::selectable( - parentWidget, QObject::tr("Upload failed"), - QObject::tr("The log file is too big. You'll have to upload it manually."), - QMessageBox::Warning)->exec(); - return QString(); - } + auto pasteUrlSetting = APPLICATION->settings()->get("PastebinURL").toString(); + std::unique_ptr<PasteUpload> paste(new PasteUpload(parentWidget, text, pasteUrlSetting)); dialog.execWithTask(paste.get()); if (!paste->wasSuccessful()) diff --git a/launcher/ui/MainWindow.cpp b/launcher/ui/MainWindow.cpp index 3dcc8ee9..32b27afb 100644 --- a/launcher/ui/MainWindow.cpp +++ b/launcher/ui/MainWindow.cpp @@ -1687,7 +1687,7 @@ void MainWindow::on_actionReportBug_triggered() void MainWindow::on_actionMoreNews_triggered() { - DesktopServices::openUrl(QUrl("https://multimc.org/posts.html")); + DesktopServices::openUrl(QUrl(BuildConfig.NEWS_OPEN_URL)); } void MainWindow::newsButtonClicked() @@ -1699,7 +1699,7 @@ void MainWindow::newsButtonClicked() } else { - DesktopServices::openUrl(QUrl("https://multimc.org/posts.html")); + DesktopServices::openUrl(QUrl(BuildConfig.NEWS_OPEN_URL)); } } diff --git a/launcher/ui/dialogs/AboutDialog.cpp b/launcher/ui/dialogs/AboutDialog.cpp index 2ba34f1a..ef96cc23 100644 --- a/launcher/ui/dialogs/AboutDialog.cpp +++ b/launcher/ui/dialogs/AboutDialog.cpp @@ -32,8 +32,14 @@ QString getCreditsHtml() QTextStream stream(&output); stream.setCodec(QTextCodec::codecForName("UTF-8")); stream << "<center>\n"; + + stream << "<h3>" << QObject::tr("PolyMC Developers", "About Credits") << "</h3>\n"; + stream << "<p>swirl <<a href='mailto:swurl@swurl.xyz'>swurl@swurl.xyz </a>></p>\n"; + stream << "<p>LennyMcLennington <<a href='mailto:lenny@sneed.church'>lenny@sneed.church</a>></p>\n"; + stream << "<br />\n"; + // TODO: possibly retrieve from git history at build time? - stream << "<h3>" << QObject::tr("Developers", "About Credits") << "</h3>\n"; + stream << "<h3>" << QObject::tr("MultiMC Developers", "About Credits") << "</h3>\n"; stream << "<p>Andrew Okin <<a href='mailto:forkk@forkk.net'>forkk@forkk.net</a>></p>\n"; stream << "<p>Petr Mrázek <<a href='mailto:peterix@gmail.com'>peterix@gmail.com</a>></p>\n"; stream << "<p>Sky Welch <<a href='mailto:multimc@bunnies.io'>multimc@bunnies.io</a>></p>\n"; @@ -47,6 +53,7 @@ QString getCreditsHtml() stream << "<p>Kilobyte <<a href='mailto:stiepen22@gmx.de'>stiepen22@gmx.de</a>></p>\n"; stream << "<p>Rootbear75 <<a href='https://twitter.com/rootbear75'>@rootbear75</a>></p>\n"; stream << "<p>Zeker Zhayard <<a href='https://twitter.com/zeker_zhayard'>@Zeker_Zhayard</a>></p>\n"; + stream << "<p>Everyone else who <a href='https://github.com/PolyMC/PolyMC/graphs/contributors'>contributed</a>!</p>\n"; stream << "<br />\n"; stream << "</center>\n"; @@ -83,8 +90,12 @@ AboutDialog::AboutDialog(QWidget *parent) : QDialog(parent), ui(new Ui::AboutDia ui->icon->setPixmap(APPLICATION->getThemedIcon("logo").pixmap(64)); ui->title->setText(launcherName); - ui->versionLabel->setText(tr("Version") +": " + BuildConfig.printableVersionString()); - ui->platformLabel->setText(tr("Platform") +": " + BuildConfig.BUILD_PLATFORM); + ui->versionLabel->setText(BuildConfig.printableVersionString()); + + if (!BuildConfig.BUILD_PLATFORM.isEmpty()) + ui->platformLabel->setText(tr("Platform") +": " + BuildConfig.BUILD_PLATFORM); + else + ui->platformLabel->setVisible(false); if (BuildConfig.VERSION_BUILD >= 0) ui->buildNumLabel->setText(tr("Build Number") +": " + QString::number(BuildConfig.VERSION_BUILD)); @@ -99,7 +110,7 @@ AboutDialog::AboutDialog(QWidget *parent) : QDialog(parent), ui(new Ui::AboutDia QString urlText("<html><head/><body><p><a href=\"%1\">%1</a></p></body></html>"); ui->urlLabel->setText(urlText.arg(BuildConfig.LAUNCHER_GIT)); - QString copyText("© 2012-2021 %1"); + QString copyText("© 2021-2022 %1"); ui->copyLabel->setText(copyText.arg(BuildConfig.LAUNCHER_COPYRIGHT)); connect(ui->closeButton, SIGNAL(clicked()), SLOT(close())); diff --git a/launcher/ui/dialogs/AboutDialog.ui b/launcher/ui/dialogs/AboutDialog.ui index 4db533ff..58275c66 100644 --- a/launcher/ui/dialogs/AboutDialog.ui +++ b/launcher/ui/dialogs/AboutDialog.ui @@ -80,13 +80,20 @@ </font> </property> <property name="text"> - <string notr="true">MultiMC 5</string> + <string notr="true">PolyMC</string> </property> <property name="alignment"> <set>Qt::AlignCenter</set> </property> </widget> </item> + <item> + <widget class="QLabel" name="versionLabel"> + <property name="alignment"> + <set>Qt::AlignCenter</set> + </property> + </widget> + </item> <item> <widget class="QTabWidget" name="tabWidget"> <property name="currentIndex"> @@ -152,16 +159,6 @@ </widget> </item> <item> - <widget class="QLabel" name="versionLabel"> - <property name="text"> - <string>Version:</string> - </property> - <property name="alignment"> - <set>Qt::AlignCenter</set> - </property> - </widget> - </item> - <item> <widget class="QLabel" name="platformLabel"> <property name="text"> <string>Platform:</string> diff --git a/launcher/ui/dialogs/ExportInstanceDialog.cpp b/launcher/ui/dialogs/ExportInstanceDialog.cpp index 1a164875..f3bf7abe 100644 --- a/launcher/ui/dialogs/ExportInstanceDialog.cpp +++ b/launcher/ui/dialogs/ExportInstanceDialog.cpp @@ -403,7 +403,13 @@ bool ExportInstanceDialog::doExport() auto & blocked = proxyModel->blockedPaths(); using std::placeholders::_1; - if (!JlCompress::compressDir(output, m_instance->instanceRoot(), name, std::bind(&SeparatorPrefixTree<'/'>::covers, blocked, _1))) + auto files = QFileInfoList(); + if (!MMCZip::collectFileListRecursively(m_instance->instanceRoot(), nullptr, &files, + std::bind(&SeparatorPrefixTree<'/'>::covers, blocked, _1))) { + QMessageBox::warning(this, tr("Error"), tr("Unable to export instance")); + return false; + } + if (!MMCZip::compressDirFiles(output, m_instance->instanceRoot(), files)) { QMessageBox::warning(this, tr("Error"), tr("Unable to export instance")); return false; diff --git a/launcher/ui/dialogs/MSALoginDialog.cpp b/launcher/ui/dialogs/MSALoginDialog.cpp index f46aa3b9..174ad46c 100644 --- a/launcher/ui/dialogs/MSALoginDialog.cpp +++ b/launcher/ui/dialogs/MSALoginDialog.cpp @@ -16,15 +16,19 @@ #include "MSALoginDialog.h" #include "ui_MSALoginDialog.h" +#include "DesktopServices.h" #include "minecraft/auth/AccountTask.h" #include <QtWidgets/QPushButton> #include <QUrl> +#include <QApplication> +#include <QClipboard> MSALoginDialog::MSALoginDialog(QWidget *parent) : QDialog(parent), ui(new Ui::MSALoginDialog) { ui->setupUi(this); ui->progressBar->setVisible(false); + ui->actionButton->setVisible(false); // ui->buttonBox->button(QDialogButtonBox::Cancel)->setEnabled(false); connect(ui->buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept); @@ -81,10 +85,17 @@ void MSALoginDialog::showVerificationUriAndCode(const QUrl& uri, const QString& QString urlString = uri.toString(); QString linkString = QString("<a href=\"%1\">%2</a>").arg(urlString, urlString); ui->label->setText(tr("<p>Please open up %1 in a browser and put in the code <b>%2</b> to proceed with login.</p>").arg(linkString, code)); + ui->actionButton->setVisible(true); + connect(ui->actionButton, &QPushButton::clicked, [=]() { + DesktopServices::openUrl(uri); + QClipboard* cb = QApplication::clipboard(); + cb->setText(code); + }); } void MSALoginDialog::hideVerificationUriAndCode() { m_externalLoginTimer.stop(); + ui->actionButton->setVisible(false); } void MSALoginDialog::setUserInputsEnabled(bool enable) @@ -110,6 +121,7 @@ void MSALoginDialog::onTaskFailed(const QString &reason) // Re-enable user-interaction setUserInputsEnabled(true); ui->progressBar->setVisible(false); + ui->actionButton->setVisible(false); } void MSALoginDialog::onTaskSucceeded() diff --git a/launcher/ui/dialogs/MSALoginDialog.ui b/launcher/ui/dialogs/MSALoginDialog.ui index 78cbfb26..c18d01a1 100644 --- a/launcher/ui/dialogs/MSALoginDialog.ui +++ b/launcher/ui/dialogs/MSALoginDialog.ui @@ -49,14 +49,25 @@ aaaaa</string> </widget> </item> <item> - <widget class="QDialogButtonBox" name="buttonBox"> - <property name="orientation"> - <enum>Qt::Horizontal</enum> - </property> - <property name="standardButtons"> - <set>QDialogButtonBox::Cancel</set> - </property> - </widget> + <layout class="QHBoxLayout" name="horizontalLayout"> + <item> + <widget class="QPushButton" name="actionButton"> + <property name="text"> + <string>Open page and copy code</string> + </property> + </widget> + </item> + <item> + <widget class="QDialogButtonBox" name="buttonBox"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="standardButtons"> + <set>QDialogButtonBox::Cancel</set> + </property> + </widget> + </item> + </layout> </item> </layout> </widget> diff --git a/launcher/ui/dialogs/ModDownloadDialog.cpp b/launcher/ui/dialogs/ModDownloadDialog.cpp new file mode 100644 index 00000000..6b807b8c --- /dev/null +++ b/launcher/ui/dialogs/ModDownloadDialog.cpp @@ -0,0 +1,98 @@ +#include "ModDownloadDialog.h" + +#include <BaseVersion.h> +#include <icons/IconList.h> +#include <InstanceList.h> + +#include "ProgressDialog.h" + +#include <QLayout> +#include <QPushButton> +#include <QValidator> +#include <QDialogButtonBox> + +#include "ui/widgets/PageContainer.h" +#include "ui/pages/modplatform/modrinth/ModrinthPage.h" +#include "ModDownloadTask.h" + + +ModDownloadDialog::ModDownloadDialog(const std::shared_ptr<ModFolderModel> &mods, QWidget *parent, + BaseInstance *instance) + : QDialog(parent), mods(mods), m_instance(instance) +{ + setObjectName(QStringLiteral("ModDownloadDialog")); + resize(400, 347); + m_verticalLayout = new QVBoxLayout(this); + m_verticalLayout->setObjectName(QStringLiteral("verticalLayout")); + + 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); + + // 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->setDefault(true); + OkButton->setAutoDefault(true); + connect(OkButton, &QPushButton::clicked, this, &ModDownloadDialog::accept); + + 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("Download mods"); +} + +QString ModDownloadDialog::dialogTitle() +{ + return tr("Download mods"); +} + +void ModDownloadDialog::reject() +{ + QDialog::reject(); +} + +void ModDownloadDialog::accept() +{ + QDialog::accept(); +} + +QList<BasePage *> ModDownloadDialog::getPages() +{ + modrinthPage = new ModrinthPage(this, m_instance); + flameModPage = new FlameModPage(this, m_instance); + return + { + modrinthPage, + flameModPage + }; +} + +void ModDownloadDialog::setSuggestedMod(const QString& name, ModDownloadTask* task) +{ + modTask.reset(task); + m_buttons->button(QDialogButtonBox::Ok)->setEnabled(task); +} + +ModDownloadDialog::~ModDownloadDialog() +{ +} + +ModDownloadTask *ModDownloadDialog::getTask() { + return modTask.release(); +} diff --git a/launcher/ui/dialogs/ModDownloadDialog.h b/launcher/ui/dialogs/ModDownloadDialog.h new file mode 100644 index 00000000..ece8e328 --- /dev/null +++ b/launcher/ui/dialogs/ModDownloadDialog.h @@ -0,0 +1,54 @@ +#pragma once + +#include <QDialog> +#include <QVBoxLayout> + +#include "BaseVersion.h" +#include "ui/pages/BasePageProvider.h" +#include "minecraft/mod/ModFolderModel.h" +#include "ModDownloadTask.h" +#include "ui/pages/modplatform/flame/FlameModPage.h" + +namespace Ui +{ +class ModDownloadDialog; +} + +class PageContainer; +class QDialogButtonBox; +class ModrinthPage; + +class ModDownloadDialog : public QDialog, public BasePageProvider +{ + Q_OBJECT + +public: + explicit ModDownloadDialog(const std::shared_ptr<ModFolderModel> &mods, QWidget *parent, BaseInstance *instance); + ~ModDownloadDialog(); + + QString dialogTitle() override; + QList<BasePage *> getPages() override; + + void setSuggestedMod(const QString & name = QString(), ModDownloadTask * task = nullptr); + + ModDownloadTask * getTask(); + const std::shared_ptr<ModFolderModel> &mods; + +public slots: + void accept() override; + void reject() override; + +//private slots: + +private: + Ui::ModDownloadDialog *ui = nullptr; + PageContainer * m_container = nullptr; + QDialogButtonBox * m_buttons = nullptr; + QVBoxLayout *m_verticalLayout = nullptr; + + + ModrinthPage *modrinthPage = nullptr; + FlameModPage *flameModPage = nullptr; + std::unique_ptr<ModDownloadTask> modTask; + BaseInstance *m_instance; +}; diff --git a/launcher/ui/dialogs/OfflineLoginDialog.cpp b/launcher/ui/dialogs/OfflineLoginDialog.cpp new file mode 100644 index 00000000..345ed40a --- /dev/null +++ b/launcher/ui/dialogs/OfflineLoginDialog.cpp @@ -0,0 +1,98 @@ +#include "OfflineLoginDialog.h" +#include "ui_OfflineLoginDialog.h" + +#include "minecraft/auth/AccountTask.h" + +#include <QtWidgets/QPushButton> + +OfflineLoginDialog::OfflineLoginDialog(QWidget *parent) : QDialog(parent), ui(new Ui::OfflineLoginDialog) +{ + ui->setupUi(this); + ui->progressBar->setVisible(false); + ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false); + + connect(ui->buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept); + connect(ui->buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); +} + +OfflineLoginDialog::~OfflineLoginDialog() +{ + delete ui; +} + +// Stage 1: User interaction +void OfflineLoginDialog::accept() +{ + setUserInputsEnabled(false); + ui->progressBar->setVisible(true); + + // Setup the login task and start it + m_account = MinecraftAccount::createOffline(ui->userTextBox->text()); + m_loginTask = m_account->loginOffline(); + connect(m_loginTask.get(), &Task::failed, this, &OfflineLoginDialog::onTaskFailed); + connect(m_loginTask.get(), &Task::succeeded, this, &OfflineLoginDialog::onTaskSucceeded); + connect(m_loginTask.get(), &Task::status, this, &OfflineLoginDialog::onTaskStatus); + connect(m_loginTask.get(), &Task::progress, this, &OfflineLoginDialog::onTaskProgress); + m_loginTask->start(); +} + +void OfflineLoginDialog::setUserInputsEnabled(bool enable) +{ + ui->userTextBox->setEnabled(enable); + ui->buttonBox->setEnabled(enable); +} + +// Enable the OK button only when the textbox contains something. +void OfflineLoginDialog::on_userTextBox_textEdited(const QString &newText) +{ + ui->buttonBox->button(QDialogButtonBox::Ok) + ->setEnabled(!newText.isEmpty()); +} + +void OfflineLoginDialog::onTaskFailed(const QString &reason) +{ + // Set message + auto lines = reason.split('\n'); + QString processed; + for(auto line: lines) { + if(line.size()) { + processed += "<font color='red'>" + line + "</font><br />"; + } + else { + processed += "<br />"; + } + } + ui->label->setText(processed); + + // Re-enable user-interaction + setUserInputsEnabled(true); + ui->progressBar->setVisible(false); +} + +void OfflineLoginDialog::onTaskSucceeded() +{ + QDialog::accept(); +} + +void OfflineLoginDialog::onTaskStatus(const QString &status) +{ + ui->label->setText(status); +} + +void OfflineLoginDialog::onTaskProgress(qint64 current, qint64 total) +{ + ui->progressBar->setMaximum(total); + ui->progressBar->setValue(current); +} + +// Public interface +MinecraftAccountPtr OfflineLoginDialog::newAccount(QWidget *parent, QString msg) +{ + OfflineLoginDialog dlg(parent); + dlg.ui->label->setText(msg); + if (dlg.exec() == QDialog::Accepted) + { + return dlg.m_account; + } + return 0; +} diff --git a/launcher/ui/dialogs/OfflineLoginDialog.h b/launcher/ui/dialogs/OfflineLoginDialog.h new file mode 100644 index 00000000..5e608379 --- /dev/null +++ b/launcher/ui/dialogs/OfflineLoginDialog.h @@ -0,0 +1,43 @@ +#pragma once + +#include <QtWidgets/QDialog> +#include <QtCore/QEventLoop> + +#include "minecraft/auth/MinecraftAccount.h" +#include "tasks/Task.h" + +namespace Ui +{ +class OfflineLoginDialog; +} + +class OfflineLoginDialog : public QDialog +{ + Q_OBJECT + +public: + ~OfflineLoginDialog(); + + static MinecraftAccountPtr newAccount(QWidget *parent, QString message); + +private: + explicit OfflineLoginDialog(QWidget *parent = 0); + + void setUserInputsEnabled(bool enable); + +protected +slots: + void accept(); + + void onTaskFailed(const QString &reason); + void onTaskSucceeded(); + void onTaskStatus(const QString &status); + void onTaskProgress(qint64 current, qint64 total); + + void on_userTextBox_textEdited(const QString &newText); + +private: + Ui::OfflineLoginDialog *ui; + MinecraftAccountPtr m_account; + Task::Ptr m_loginTask; +}; diff --git a/launcher/ui/dialogs/OfflineLoginDialog.ui b/launcher/ui/dialogs/OfflineLoginDialog.ui new file mode 100644 index 00000000..d8964a2e --- /dev/null +++ b/launcher/ui/dialogs/OfflineLoginDialog.ui @@ -0,0 +1,67 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>OfflineLoginDialog</class> + <widget class="QDialog" name="OfflineLoginDialog"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>400</width> + <height>150</height> + </rect> + </property> + <property name="sizePolicy"> + <sizepolicy hsizetype="Fixed" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="windowTitle"> + <string>Add Account</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <widget class="QLabel" name="label"> + <property name="text"> + <string notr="true">Message label placeholder.</string> + </property> + <property name="textFormat"> + <enum>Qt::RichText</enum> + </property> + <property name="textInteractionFlags"> + <set>Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse</set> + </property> + </widget> + </item> + <item> + <widget class="QLineEdit" name="userTextBox"> + <property name="placeholderText"> + <string>Username</string> + </property> + </widget> + </item> + <item> + <widget class="QProgressBar" name="progressBar"> + <property name="value"> + <number>69</number> + </property> + <property name="textVisible"> + <bool>false</bool> + </property> + </widget> + </item> + <item> + <widget class="QDialogButtonBox" name="buttonBox"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="standardButtons"> + <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set> + </property> + </widget> + </item> + </layout> + </widget> + <resources/> + <connections/> +</ui> diff --git a/launcher/ui/dialogs/UpdateDialog.cpp b/launcher/ui/dialogs/UpdateDialog.cpp index c0f6074c..ec77d146 100644 --- a/launcher/ui/dialogs/UpdateDialog.cpp +++ b/launcher/ui/dialogs/UpdateDialog.cpp @@ -38,12 +38,12 @@ void UpdateDialog::loadChangelog() QString url; if(channel == "stable") { - url = QString("https://raw.githubusercontent.com/MultiMC/Launcher/%1/changelog.md").arg(channel); + url = QString("https://raw.githubusercontent.com/PolyMC/PolyMC/%1/changelog.md").arg(channel); m_changelogType = CHANGELOG_MARKDOWN; } else { - url = QString("https://api.github.com/repos/MultiMC/Launcher/compare/%1...%2").arg(BuildConfig.GIT_COMMIT, channel); + url = QString("https://api.github.com/repos/PolyMC/PolyMC/compare/%1...%2").arg(BuildConfig.GIT_COMMIT, channel); m_changelogType = CHANGELOG_COMMITS; } dljob->addNetAction(Net::Download::makeByteArray(QUrl(url), &changelogData)); @@ -58,7 +58,7 @@ QString reprocessMarkdown(QByteArray markdown) QString output = hoedown.process(markdown); // HACK: easier than customizing hoedown - output.replace(QRegExp("GH-([0-9]+)"), "<a href=\"https://github.com/MultiMC/Launcher/issues/\\1\">GH-\\1</a>"); + output.replace(QRegExp("GH-([0-9]+)"), "<a href=\"https://github.com/PolyMC/PolyMC/issues/\\1\">GH-\\1</a>"); qDebug() << output; return output; } @@ -100,7 +100,7 @@ QString reprocessCommits(QByteArray json) result += "<tr><td>"; if(issuenr.length()) { - result += QString("<a href=\"https://github.com/MultiMC/Launcher/issues/%1\">GH-%2</a>").arg(issuenr, issuenr); + result += QString("<a href=\"https://github.com/PolyMC/PolyMC/issues/%1\">GH-%2</a>").arg(issuenr, issuenr); } else if(prefix.length()) { diff --git a/launcher/ui/dialogs/UpdateDialog.ui b/launcher/ui/dialogs/UpdateDialog.ui index b0b3dd83..bd94a554 100644 --- a/launcher/ui/dialogs/UpdateDialog.ui +++ b/launcher/ui/dialogs/UpdateDialog.ui @@ -11,7 +11,7 @@ </rect> </property> <property name="windowTitle"> - <string>MultiMC Update</string> + <string>PolyMC Update</string> </property> <property name="windowIcon"> <iconset> diff --git a/launcher/ui/pages/global/PasteEEPage.cpp b/launcher/ui/pages/global/APIPage.cpp index 4b375d9a..ad79e00c 100644 --- a/launcher/ui/pages/global/PasteEEPage.cpp +++ b/launcher/ui/pages/global/APIPage.cpp @@ -1,4 +1,4 @@ -/* Copyright 2013-2021 MultiMC Contributors +/* Copyright 2013-2021 MultiMC & PolyMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,69 +13,55 @@ * limitations under the License. */ -#include "PasteEEPage.h" -#include "ui_PasteEEPage.h" +#include "APIPage.h" +#include "ui_APIPage.h" #include <QMessageBox> #include <QFileDialog> #include <QStandardPaths> #include <QTabBar> +#include <QVariant> #include "settings/SettingsObject.h" #include "tools/BaseProfiler.h" #include "Application.h" -PasteEEPage::PasteEEPage(QWidget *parent) : +APIPage::APIPage(QWidget *parent) : QWidget(parent), - ui(new Ui::PasteEEPage) + ui(new Ui::APIPage) { + static QRegularExpression validUrlRegExp("https?://.+"); ui->setupUi(this); + ui->urlChoices->setValidator(new QRegularExpressionValidator(validUrlRegExp, ui->urlChoices)); ui->tabWidget->tabBar()->hide();\ - connect(ui->customAPIkeyEdit, &QLineEdit::textEdited, this, &PasteEEPage::textEdited); loadSettings(); } -PasteEEPage::~PasteEEPage() +APIPage::~APIPage() { delete ui; } -void PasteEEPage::loadSettings() +void APIPage::loadSettings() { auto s = APPLICATION->settings(); - QString keyToUse = s->get("PasteEEAPIKey").toString(); - if(keyToUse == "multimc") - { - ui->multimcButton->setChecked(true); - } - else - { - ui->customButton->setChecked(true); - ui->customAPIkeyEdit->setText(keyToUse); - } + QString pastebinURL = s->get("PastebinURL").toString(); + ui->urlChoices->setCurrentText(pastebinURL); + QString msaClientID = s->get("MSAClientIDOverride").toString(); + ui->msaClientID->setText(msaClientID); } -void PasteEEPage::applySettings() +void APIPage::applySettings() { auto s = APPLICATION->settings(); - - QString pasteKeyToUse; - if (ui->customButton->isChecked()) - pasteKeyToUse = ui->customAPIkeyEdit->text(); - else - { - pasteKeyToUse = "multimc"; - } - s->set("PasteEEAPIKey", pasteKeyToUse); + QString pastebinURL = ui->urlChoices->currentText(); + s->set("PastebinURL", pastebinURL); + QString msaClientID = ui->msaClientID->text(); + s->set("MSAClientIDOverride", msaClientID); } -bool PasteEEPage::apply() +bool APIPage::apply() { applySettings(); return true; } - -void PasteEEPage::textEdited(const QString& text) -{ - ui->customButton->setChecked(true); -} diff --git a/launcher/ui/pages/global/PasteEEPage.h b/launcher/ui/pages/global/APIPage.h index a1c7d434..9474ebbb 100644 --- a/launcher/ui/pages/global/PasteEEPage.h +++ b/launcher/ui/pages/global/APIPage.h @@ -21,32 +21,32 @@ #include <Application.h> namespace Ui { -class PasteEEPage; +class APIPage; } -class PasteEEPage : public QWidget, public BasePage +class APIPage : public QWidget, public BasePage { Q_OBJECT public: - explicit PasteEEPage(QWidget *parent = 0); - ~PasteEEPage(); + explicit APIPage(QWidget *parent = 0); + ~APIPage(); QString displayName() const override { - return tr("Log Upload"); + return tr("APIs"); } QIcon icon() const override { - return APPLICATION->getThemedIcon("log"); + return APPLICATION->getThemedIcon("worlds"); } QString id() const override { - return "log-upload"; + return "apis"; } QString helpPage() const override { - return "Log-Upload"; + return "APIs"; } virtual bool apply() override; @@ -54,9 +54,7 @@ private: void loadSettings(); void applySettings(); -private slots: - void textEdited(const QString &text); - private: - Ui::PasteEEPage *ui; + Ui::APIPage *ui; }; + diff --git a/launcher/ui/pages/global/APIPage.ui b/launcher/ui/pages/global/APIPage.ui new file mode 100644 index 00000000..28c53b79 --- /dev/null +++ b/launcher/ui/pages/global/APIPage.ui @@ -0,0 +1,179 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>APIPage</class> + <widget class="QWidget" name="APIPage"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>491</width> + <height>474</height> + </rect> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <property name="leftMargin"> + <number>0</number> + </property> + <property name="topMargin"> + <number>0</number> + </property> + <property name="rightMargin"> + <number>0</number> + </property> + <property name="bottomMargin"> + <number>0</number> + </property> + <item> + <widget class="QTabWidget" name="tabWidget"> + <property name="currentIndex"> + <number>0</number> + </property> + <widget class="QWidget" name="tab"> + <attribute name="title"> + <string notr="true">Tab 1</string> + </attribute> + <layout class="QVBoxLayout" name="verticalLayout_2"> + <item> + <widget class="QGroupBox" name="groupBox_paste"> + <property name="title"> + <string>Pastebin URL</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout_3"> + <item> + <widget class="Line" name="line"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + </widget> + </item> + <item> + <widget class="QLabel" name="label_2"> + <property name="font"> + <font> + <pointsize>10</pointsize> + </font> + </property> + <property name="text"> + <string><html><head/><body><p>Note: only input that starts with <span style=" font-weight:600;">http://</span> or <span style=" font-weight:600;">https://</span> will be accepted.</p></body></html></string> + </property> + <property name="scaledContents"> + <bool>false</bool> + </property> + </widget> + </item> + <item> + <widget class="QComboBox" name="urlChoices"> + <property name="editable"> + <bool>true</bool> + </property> + <property name="insertPolicy"> + <enum>QComboBox::NoInsert</enum> + </property> + <item> + <property name="text"> + <string>https://0x0.st</string> + </property> + </item> + <item> + <property name="text"> + <string>https://paste.polymc.org</string> + </property> + </item> + </widget> + </item> + <item> + <widget class="QLabel" name="label"> + <property name="text"> + <string><html><head/><body><p>Here you can choose from a predefined list of paste services, or input the URL of a different paste service of your choice, provided it supports the same protocol as 0x0.st, that is POST a file parameter to the URL and return a link in the response body.</p></body></html></string> + </property> + <property name="textFormat"> + <enum>Qt::RichText</enum> + </property> + <property name="wordWrap"> + <bool>true</bool> + </property> + <property name="openExternalLinks"> + <bool>true</bool> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item> + <widget class="QGroupBox" name="groupBox_msa"> + <property name="title"> + <string>Microsoft Authentication</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout_4"> + <item> + <widget class="Line" name="line_2"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + </widget> + </item> + <item> + <widget class="QLabel" name="label_3"> + <property name="text"> + <string>Note: you probably don't need to set this if logging in via Microsoft Authentication already works.</string> + </property> + <property name="textFormat"> + <enum>Qt::RichText</enum> + </property> + <property name="wordWrap"> + <bool>true</bool> + </property> + </widget> + </item> + <item> + <widget class="QLineEdit" name="msaClientID"> + <property name="placeholderText"> + <string>(Default)</string> + </property> + </widget> + </item> + <item> + <widget class="QLabel" name="label_4"> + <property name="text"> + <string>Enter a custom client ID for Microsoft Authentication here. </string> + </property> + <property name="textFormat"> + <enum>Qt::RichText</enum> + </property> + <property name="wordWrap"> + <bool>true</bool> + </property> + <property name="openExternalLinks"> + <bool>true</bool> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item> + <spacer name="verticalSpacer"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>40</height> + </size> + </property> + </spacer> + </item> + </layout> + </widget> + </widget> + </item> + </layout> + </widget> + <tabstops> + <tabstop>tabWidget</tabstop> + </tabstops> + <resources/> + <connections/> +</ui> diff --git a/launcher/ui/pages/global/AccountListPage.cpp b/launcher/ui/pages/global/AccountListPage.cpp index 87fcac86..eb1ee8d3 100644 --- a/launcher/ui/pages/global/AccountListPage.cpp +++ b/launcher/ui/pages/global/AccountListPage.cpp @@ -24,6 +24,7 @@ #include "net/NetJob.h" #include "ui/dialogs/ProgressDialog.h" +#include "ui/dialogs/OfflineLoginDialog.h" #include "ui/dialogs/LoginDialog.h" #include "ui/dialogs/MSALoginDialog.h" #include "ui/dialogs/CustomMessageBox.h" @@ -72,7 +73,10 @@ AccountListPage::AccountListPage(QWidget *parent) updateButtonStates(); // Xbox authentication won't work without a client identifier, so disable the button if it is missing - ui->actionAddMicrosoft->setVisible(BuildConfig.MSA_CLIENT_ID.size() != 0); + if (APPLICATION->getMSAClientID().isEmpty()) { + ui->actionAddMicrosoft->setVisible(false); + ui->actionAddMicrosoft->setToolTip(tr("No Microsoft Authentication client ID was set.")); + } } AccountListPage::~AccountListPage() @@ -132,8 +136,8 @@ void AccountListPage::on_actionAddMicrosoft_triggered() this, tr("Microsoft Accounts not available"), tr( - "Microsoft accounts are only usable on macOS 10.13 or newer, with fully updated MultiMC.\n\n" - "Please update both your operating system and MultiMC." + "Microsoft accounts are only usable on macOS 10.13 or newer, with fully updated PolyMC.\n\n" + "Please update both your operating system and PolyMC." ), QMessageBox::Warning )->exec(); @@ -153,6 +157,35 @@ void AccountListPage::on_actionAddMicrosoft_triggered() } } +void AccountListPage::on_actionAddOffline_triggered() +{ + if (!m_accounts->anyAccountIsValid()) { + QMessageBox::warning( + this, + tr("Error"), + tr( + "You must add a Microsoft or Mojang account that owns Minecraft before you can add an offline account." + "<br><br>" + "If you have lost your account you can contact Microsoft for support." + ) + ); + return; + } + + MinecraftAccountPtr account = OfflineLoginDialog::newAccount( + this, + tr("Please enter your desired username to add your offline account.") + ); + + if (account) + { + m_accounts->addAccount(account); + if (m_accounts->count() == 1) { + m_accounts->setDefaultAccount(account); + } + } +} + void AccountListPage::on_actionRemove_triggered() { QModelIndexList selection = ui->listView->selectionModel()->selectedIndexes(); diff --git a/launcher/ui/pages/global/AccountListPage.h b/launcher/ui/pages/global/AccountListPage.h index 1c65e708..841c3fd2 100644 --- a/launcher/ui/pages/global/AccountListPage.h +++ b/launcher/ui/pages/global/AccountListPage.h @@ -62,6 +62,7 @@ public: public slots: void on_actionAddMojang_triggered(); void on_actionAddMicrosoft_triggered(); + void on_actionAddOffline_triggered(); void on_actionRemove_triggered(); void on_actionRefresh_triggered(); void on_actionSetDefault_triggered(); diff --git a/launcher/ui/pages/global/AccountListPage.ui b/launcher/ui/pages/global/AccountListPage.ui index 29738c02..d21a92e2 100644 --- a/launcher/ui/pages/global/AccountListPage.ui +++ b/launcher/ui/pages/global/AccountListPage.ui @@ -54,6 +54,7 @@ </attribute> <addaction name="actionAddMicrosoft"/> <addaction name="actionAddMojang"/> + <addaction name="actionAddOffline"/> <addaction name="actionRefresh"/> <addaction name="actionRemove"/> <addaction name="actionSetDefault"/> @@ -103,6 +104,11 @@ <string>Add Microsoft</string> </property> </action> + <action name="actionAddOffline"> + <property name="text"> + <string>Add Offline</string> + </property> + </action> <action name="actionRefresh"> <property name="text"> <string>Refresh</string> diff --git a/launcher/ui/pages/global/LauncherPage.cpp b/launcher/ui/pages/global/LauncherPage.cpp index 4d4d4e89..0ffe8050 100644 --- a/launcher/ui/pages/global/LauncherPage.cpp +++ b/launcher/ui/pages/global/LauncherPage.cpp @@ -246,32 +246,31 @@ void LauncherPage::applySettings() //FIXME: make generic switch (ui->themeComboBox->currentIndex()) { - case 1: + case 0: s->set("IconTheme", "pe_dark"); break; - case 2: + case 1: s->set("IconTheme", "pe_light"); break; - case 3: + case 2: s->set("IconTheme", "pe_blue"); break; - case 4: + case 3: s->set("IconTheme", "pe_colored"); break; - case 5: + case 4: s->set("IconTheme", "OSX"); break; - case 6: + case 5: s->set("IconTheme", "iOS"); break; - case 7: + case 6: s->set("IconTheme", "flat"); break; - case 8: + case 7: s->set("IconTheme", "custom"); break; - case 0: - default: + case 8: s->set("IconTheme", "multimc"); break; } @@ -327,40 +326,40 @@ void LauncherPage::loadSettings() auto theme = s->get("IconTheme").toString(); if (theme == "pe_dark") { - ui->themeComboBox->setCurrentIndex(1); + ui->themeComboBox->setCurrentIndex(0); } else if (theme == "pe_light") { - ui->themeComboBox->setCurrentIndex(2); + ui->themeComboBox->setCurrentIndex(1); } else if (theme == "pe_blue") { - ui->themeComboBox->setCurrentIndex(3); + ui->themeComboBox->setCurrentIndex(2); } else if (theme == "pe_colored") { - ui->themeComboBox->setCurrentIndex(4); + ui->themeComboBox->setCurrentIndex(3); } else if (theme == "OSX") { - ui->themeComboBox->setCurrentIndex(5); + ui->themeComboBox->setCurrentIndex(4); } else if (theme == "iOS") { - ui->themeComboBox->setCurrentIndex(6); + ui->themeComboBox->setCurrentIndex(5); } else if (theme == "flat") { + ui->themeComboBox->setCurrentIndex(6); + } + else if (theme == "multimc") + { ui->themeComboBox->setCurrentIndex(7); } else if (theme == "custom") { ui->themeComboBox->setCurrentIndex(8); } - else - { - ui->themeComboBox->setCurrentIndex(0); - } { auto currentTheme = s->get("ApplicationTheme").toString(); diff --git a/launcher/ui/pages/global/LauncherPage.ui b/launcher/ui/pages/global/LauncherPage.ui index 2b3729bc..47fed873 100644 --- a/launcher/ui/pages/global/LauncherPage.ui +++ b/launcher/ui/pages/global/LauncherPage.ui @@ -264,11 +264,6 @@ </property> <item> <property name="text"> - <string>Default</string> - </property> - </item> - <item> - <property name="text"> <string>Simple (Dark Icons)</string> </property> </item> @@ -307,6 +302,11 @@ <string>Custom</string> </property> </item> + <item> + <property name="text"> + <string>MultiMC</string> + </property> + </item> </widget> </item> <item row="1" column="1"> diff --git a/launcher/ui/pages/global/MinecraftPage.cpp b/launcher/ui/pages/global/MinecraftPage.cpp index c763f8ac..5470a586 100644 --- a/launcher/ui/pages/global/MinecraftPage.cpp +++ b/launcher/ui/pages/global/MinecraftPage.cpp @@ -71,6 +71,9 @@ void MinecraftPage::applySettings() s->set("ShowGameTime", ui->showGameTime->isChecked()); s->set("ShowGlobalGameTime", ui->showGlobalGameTime->isChecked()); s->set("RecordGameTime", ui->recordGameTime->isChecked()); + + // Miscellaneous + s->set("CloseAfterLaunch", ui->closeAfterLaunchCheck->isChecked()); } void MinecraftPage::loadSettings() @@ -88,4 +91,6 @@ void MinecraftPage::loadSettings() ui->showGameTime->setChecked(s->get("ShowGameTime").toBool()); ui->showGlobalGameTime->setChecked(s->get("ShowGlobalGameTime").toBool()); ui->recordGameTime->setChecked(s->get("RecordGameTime").toBool()); + + ui->closeAfterLaunchCheck->setChecked(s->get("CloseAfterLaunch").toBool()); } diff --git a/launcher/ui/pages/global/MinecraftPage.ui b/launcher/ui/pages/global/MinecraftPage.ui index 857b8cfb..a28b1f59 100644 --- a/launcher/ui/pages/global/MinecraftPage.ui +++ b/launcher/ui/pages/global/MinecraftPage.ui @@ -165,6 +165,25 @@ </widget> </item> <item> + <widget class="QGroupBox" name="groupBox"> + <property name="title"> + <string>Miscellaneous</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <widget class="QCheckBox" name="closeAfterLaunchCheck"> + <property name="toolTip"> + <string><html><head/><body><p>PolyMC will automatically reopen when the game crashes or exits.</p></body></html></string> + </property> + <property name="text"> + <string>Close PolyMC after game window opens</string> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item> <spacer name="verticalSpacerMinecraft"> <property name="orientation"> <enum>Qt::Vertical</enum> @@ -184,7 +203,6 @@ </layout> </widget> <tabstops> - <tabstop>tabWidget</tabstop> <tabstop>maximizedCheckBox</tabstop> <tabstop>windowWidthSpinBox</tabstop> <tabstop>windowHeightSpinBox</tabstop> diff --git a/launcher/ui/pages/global/PasteEEPage.ui b/launcher/ui/pages/global/PasteEEPage.ui deleted file mode 100644 index 10883781..00000000 --- a/launcher/ui/pages/global/PasteEEPage.ui +++ /dev/null @@ -1,128 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<ui version="4.0"> - <class>PasteEEPage</class> - <widget class="QWidget" name="PasteEEPage"> - <property name="geometry"> - <rect> - <x>0</x> - <y>0</y> - <width>491</width> - <height>474</height> - </rect> - </property> - <layout class="QVBoxLayout" name="verticalLayout"> - <property name="leftMargin"> - <number>0</number> - </property> - <property name="topMargin"> - <number>0</number> - </property> - <property name="rightMargin"> - <number>0</number> - </property> - <property name="bottomMargin"> - <number>0</number> - </property> - <item> - <widget class="QTabWidget" name="tabWidget"> - <property name="currentIndex"> - <number>0</number> - </property> - <widget class="QWidget" name="tab"> - <attribute name="title"> - <string notr="true">Tab 1</string> - </attribute> - <layout class="QVBoxLayout" name="verticalLayout_2"> - <item> - <widget class="QGroupBox" name="groupBox_2"> - <property name="title"> - <string>paste.ee API key</string> - </property> - <layout class="QVBoxLayout" name="verticalLayout_10"> - <item> - <widget class="QRadioButton" name="multimcButton"> - <property name="text"> - <string>MultiMC key - 12MB &upload limit</string> - </property> - <attribute name="buttonGroup"> - <string notr="true">pasteButtonGroup</string> - </attribute> - </widget> - </item> - <item> - <widget class="QRadioButton" name="customButton"> - <property name="text"> - <string>&Your own key - 12MB upload limit:</string> - </property> - <attribute name="buttonGroup"> - <string notr="true">pasteButtonGroup</string> - </attribute> - </widget> - </item> - <item> - <widget class="QLineEdit" name="customAPIkeyEdit"> - <property name="echoMode"> - <enum>QLineEdit::Password</enum> - </property> - <property name="placeholderText"> - <string>Paste your API key here!</string> - </property> - </widget> - </item> - <item> - <widget class="Line" name="line"> - <property name="orientation"> - <enum>Qt::Horizontal</enum> - </property> - </widget> - </item> - <item> - <widget class="QLabel" name="label"> - <property name="text"> - <string><html><head/><body><p><a href="https://paste.ee">paste.ee</a> is used by MultiMC for log uploads. If you have a <a href="https://paste.ee">paste.ee</a> account, you can add your API key here and have your uploaded logs paired with your account.</p></body></html></string> - </property> - <property name="textFormat"> - <enum>Qt::RichText</enum> - </property> - <property name="wordWrap"> - <bool>true</bool> - </property> - <property name="openExternalLinks"> - <bool>true</bool> - </property> - </widget> - </item> - </layout> - </widget> - </item> - <item> - <spacer name="verticalSpacer"> - <property name="orientation"> - <enum>Qt::Vertical</enum> - </property> - <property name="sizeHint" stdset="0"> - <size> - <width>20</width> - <height>216</height> - </size> - </property> - </spacer> - </item> - </layout> - </widget> - </widget> - </item> - </layout> - </widget> - <tabstops> - <tabstop>tabWidget</tabstop> - <tabstop>multimcButton</tabstop> - <tabstop>customButton</tabstop> - <tabstop>customAPIkeyEdit</tabstop> - </tabstops> - <resources/> - <connections/> - <buttongroups> - <buttongroup name="pasteButtonGroup"/> - </buttongroups> -</ui> diff --git a/launcher/ui/pages/instance/LegacyUpgradePage.ui b/launcher/ui/pages/instance/LegacyUpgradePage.ui index 085919e3..b22c03e5 100644 --- a/launcher/ui/pages/instance/LegacyUpgradePage.ui +++ b/launcher/ui/pages/instance/LegacyUpgradePage.ui @@ -26,7 +26,14 @@ <item> <widget class="QTextBrowser" name="textBrowser"> <property name="html"> - <string><html><body><h1>Upgrade is required</h1><p>MultiMC now supports old Minecraft versions and all the required features in the new (OneSix) instance format. As a consequence, the old (Legacy) format has been entirely disabled and old instances need to be upgraded.</p><p>The upgrade will create a new instance with the same contents as the current one, in the new format. The original instance will remain untouched, in case anything goes wrong in the process.</p><p>Please report any issues on our <a href="https://github.com/MultiMC/Launcher/issues">github issues page</a>.</p><p>There is also a <a href="https://discord.gg/GtPmv93">discord channel for testing here</a>.</p></body></html></string> + <string><!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> +<html><head><meta name="qrichtext" content="1" /><style type="text/css"> +p, li { white-space: pre-wrap; } +</style></head><body style=" font-family:'Noto Sans'; font-size:11pt; font-weight:400; font-style:normal;"> +<h1 style=" margin-top:18px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-size:xx-large; font-weight:600;">Upgrade is required</span></h1> +<p style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">PolyMC now supports old Minecraft versions and all the required features in the new (OneSix) instance format. As a consequence, the old (Legacy) format has been entirely disabled and old instances need to be upgraded.</p> +<p style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">The upgrade will create a new instance with the same contents as the current one, in the new format. The original instance will remain untouched, in case anything goes wrong in the process.</p> +<p style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Please report any issues on our <a href="https://github.com/PolyMC/PolyMC/issues"><span style=" text-decoration: underline; color:#3584e4;">github issues page</span></a>.</p></body></html></string> </property> <property name="openExternalLinks"> <bool>true</bool> diff --git a/launcher/ui/pages/instance/LogPage.ui b/launcher/ui/pages/instance/LogPage.ui index ccfc1551..31bb368c 100644 --- a/launcher/ui/pages/instance/LogPage.ui +++ b/launcher/ui/pages/instance/LogPage.ui @@ -100,7 +100,7 @@ <item> <widget class="QPushButton" name="btnPaste"> <property name="toolTip"> - <string>Upload the log to paste.ee - it will stay online for a month</string> + <string>Upload the log to the paste service configured in preferences</string> </property> <property name="text"> <string>Upload</string> diff --git a/launcher/ui/pages/instance/ModFolderPage.cpp b/launcher/ui/pages/instance/ModFolderPage.cpp index e63b1434..494d32f0 100644 --- a/launcher/ui/pages/instance/ModFolderPage.cpp +++ b/launcher/ui/pages/instance/ModFolderPage.cpp @@ -26,6 +26,7 @@ #include "Application.h" #include "ui/dialogs/CustomMessageBox.h" +#include "ui/dialogs/ModDownloadDialog.h" #include "ui/GuiUtil.h" #include "DesktopServices.h" @@ -36,6 +37,7 @@ #include "minecraft/PackProfile.h" #include "Version.h" +#include "ui/dialogs/ProgressDialog.h" namespace { // FIXME: wasteful @@ -141,6 +143,11 @@ ModFolderPage::ModFolderPage( ui(new Ui::ModFolderPage) { ui->setupUi(this); + if(id == "mods") { + auto act = new QAction(tr("Install Mods"), this); + ui->actionsToolbar->insertActionBefore(ui->actionView_configs,act); + connect(act, &QAction::triggered, this, &ModFolderPage::on_actionInstall_mods_triggered); + } ui->actionsToolbar->insertSpacer(ui->actionView_configs); m_inst = inst; @@ -342,6 +349,44 @@ void ModFolderPage::on_actionRemove_triggered() m_mods->deleteMods(selection.indexes()); } +void ModFolderPage::on_actionInstall_mods_triggered() +{ + if(!m_controlsEnabled) { + return; + } + if(m_inst->typeName() != "Minecraft"){ + return; //this is a null instance or a legacy instance + } + bool hasFabric = !((MinecraftInstance *)m_inst)->getPackProfile()->getComponentVersion("net.fabricmc.fabric-loader").isEmpty(); + bool hasForge = !((MinecraftInstance *)m_inst)->getPackProfile()->getComponentVersion("net.minecraftforge").isEmpty(); + if (!hasFabric && !hasForge) { + QMessageBox::critical(this,tr("Error"),tr("Please install a mod loader first!")); + return; + } + ModDownloadDialog mdownload(m_mods, this, m_inst); + if(mdownload.exec()) { + ModDownloadTask *task = mdownload.getTask(); + if (task) { + connect(task, &Task::failed, [this, task](QString reason) { + task->deleteLater(); + CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->show(); + }); + connect(task, &Task::succeeded, [this, task]() { + QStringList warnings = task->warnings(); + if (warnings.count()) { + CustomMessageBox::selectable(this, tr("Warnings"), warnings.join('\n'), + QMessageBox::Warning)->show(); + } + task->deleteLater(); + }); + ProgressDialog loadDialog(this); + loadDialog.setSkipButton(true, tr("Abort")); + loadDialog.execWithTask(task); + m_mods->update(); + } + } +} + void ModFolderPage::on_actionView_configs_triggered() { DesktopServices::openDirectory(m_inst->instanceConfigFolder(), true); diff --git a/launcher/ui/pages/instance/ModFolderPage.h b/launcher/ui/pages/instance/ModFolderPage.h index 8ef7559b..fbda3cd8 100644 --- a/launcher/ui/pages/instance/ModFolderPage.h +++ b/launcher/ui/pages/instance/ModFolderPage.h @@ -102,6 +102,7 @@ slots: void on_actionRemove_triggered(); void on_actionEnable_triggered(); void on_actionDisable_triggered(); + void on_actionInstall_mods_triggered(); void on_actionView_Folder_triggered(); void on_actionView_configs_triggered(); void ShowContextMenu(const QPoint &pos); diff --git a/launcher/ui/pages/instance/OtherLogsPage.ui b/launcher/ui/pages/instance/OtherLogsPage.ui index 56ff3b62..77f3e647 100644 --- a/launcher/ui/pages/instance/OtherLogsPage.ui +++ b/launcher/ui/pages/instance/OtherLogsPage.ui @@ -84,7 +84,7 @@ <item row="3" column="2"> <widget class="QPushButton" name="btnPaste"> <property name="toolTip"> - <string>Upload the log to paste.ee - it will stay online for a month</string> + <string>Upload the log to the paste service configured in preferences.</string> </property> <property name="text"> <string>Upload</string> diff --git a/launcher/ui/pages/instance/ScreenshotsPage.cpp b/launcher/ui/pages/instance/ScreenshotsPage.cpp index f568ef0d..4011d88c 100644 --- a/launcher/ui/pages/instance/ScreenshotsPage.cpp +++ b/launcher/ui/pages/instance/ScreenshotsPage.cpp @@ -250,6 +250,12 @@ bool ScreenshotsPage::eventFilter(QObject *obj, QEvent *evt) return QWidget::eventFilter(obj, evt); } QKeyEvent *keyEvent = static_cast<QKeyEvent *>(evt); + + if (keyEvent->matches(QKeySequence::Copy)) { + on_actionCopy_File_s_triggered(); + return true; + } + switch (keyEvent->key()) { case Qt::Key_Delete: @@ -272,6 +278,11 @@ ScreenshotsPage::~ScreenshotsPage() void ScreenshotsPage::ShowContextMenu(const QPoint& pos) { auto menu = ui->toolBar->createContextMenu(this, tr("Context menu")); + + if (ui->listView->selectionModel()->selectedRows().size() > 1) { + menu->removeAction( ui->actionCopy_Image ); + } + menu->exec(ui->listView->mapToGlobal(pos)); delete menu; } @@ -377,6 +388,42 @@ void ScreenshotsPage::on_actionUpload_triggered() m_uploadActive = false; } +void ScreenshotsPage::on_actionCopy_Image_triggered() +{ + auto selection = ui->listView->selectionModel()->selectedRows(); + if(selection.size() < 1) + { + return; + } + + // You can only copy one image to the clipboard. In the case of multiple selected files, only the first one gets copied. + auto item = selection[0]; + auto info = m_model->fileInfo(item); + QImage image(info.absoluteFilePath()); + Q_ASSERT(!image.isNull()); + QApplication::clipboard()->setImage(image, QClipboard::Clipboard); +} + +void ScreenshotsPage::on_actionCopy_File_s_triggered() +{ + auto selection = ui->listView->selectionModel()->selectedRows(); + if(selection.size() < 1) + { + // Don't do anything so we don't empty the users clipboard + return; + } + + QString buf = ""; + for (auto item : selection) + { + auto info = m_model->fileInfo(item); + buf += "file:///" + info.absoluteFilePath() + "\r\n"; + } + QMimeData* mimeData = new QMimeData(); + mimeData->setData("text/uri-list", buf.toLocal8Bit()); + QApplication::clipboard()->setMimeData(mimeData); +} + void ScreenshotsPage::on_actionDelete_triggered() { auto mbox = CustomMessageBox::selectable( diff --git a/launcher/ui/pages/instance/ScreenshotsPage.h b/launcher/ui/pages/instance/ScreenshotsPage.h index d2f44837..2a1fdeee 100644 --- a/launcher/ui/pages/instance/ScreenshotsPage.h +++ b/launcher/ui/pages/instance/ScreenshotsPage.h @@ -73,6 +73,8 @@ protected: private slots: void on_actionUpload_triggered(); + void on_actionCopy_Image_triggered(); + void on_actionCopy_File_s_triggered(); void on_actionDelete_triggered(); void on_actionRename_triggered(); void on_actionView_Folder_triggered(); diff --git a/launcher/ui/pages/instance/ScreenshotsPage.ui b/launcher/ui/pages/instance/ScreenshotsPage.ui index ec461087..2e2227a2 100644 --- a/launcher/ui/pages/instance/ScreenshotsPage.ui +++ b/launcher/ui/pages/instance/ScreenshotsPage.ui @@ -50,6 +50,8 @@ <bool>false</bool> </attribute> <addaction name="actionUpload"/> + <addaction name="actionCopy_Image"/> + <addaction name="actionCopy_File_s"/> <addaction name="actionDelete"/> <addaction name="actionRename"/> <addaction name="actionView_Folder"/> @@ -74,6 +76,22 @@ <string>View Folder</string> </property> </action> + <action name="actionCopy_Image"> + <property name="text"> + <string>Copy Image</string> + </property> + <property name="toolTip"> + <string>Copy Image</string> + </property> + </action> + <action name="actionCopy_File_s"> + <property name="text"> + <string>Copy File(s)</string> + </property> + <property name="toolTip"> + <string>Copy File(s)</string> + </property> + </action> </widget> <customwidgets> <customwidget> diff --git a/launcher/ui/pages/instance/VersionPage.cpp b/launcher/ui/pages/instance/VersionPage.cpp index 6e57909b..0fa5f68d 100644 --- a/launcher/ui/pages/instance/VersionPage.cpp +++ b/launcher/ui/pages/instance/VersionPage.cpp @@ -395,7 +395,7 @@ void VersionPage::on_actionDownload_All_triggered() { CustomMessageBox::selectable( this, tr("Error"), - tr("MultiMC cannot download Minecraft or update instances unless you have at least " + tr("PolyMC cannot download Minecraft or update instances unless you have at least " "one account added.\nPlease add your Mojang or Minecraft account."), QMessageBox::Warning)->show(); return; @@ -635,4 +635,3 @@ void VersionPage::onFilterTextChanged(const QString &newContents) } #include "VersionPage.moc" - diff --git a/launcher/ui/pages/modplatform/flame/FlameModModel.cpp b/launcher/ui/pages/modplatform/flame/FlameModModel.cpp new file mode 100644 index 00000000..2cf83261 --- /dev/null +++ b/launcher/ui/pages/modplatform/flame/FlameModModel.cpp @@ -0,0 +1,273 @@ +#include "FlameModModel.h" +#include "Application.h" +#include "minecraft/MinecraftInstance.h" +#include "minecraft/PackProfile.h" +#include "FlameModPage.h" +#include <Json.h> + +#include <MMCStrings.h> +#include <Version.h> + +#include <QtMath> + + +namespace FlameMod { + +ListModel::ListModel(FlameModPage *parent) : QAbstractListModel(parent) +{ +} + +ListModel::~ListModel() +{ +} + +int ListModel::rowCount(const QModelIndex &parent) const +{ + return modpacks.size(); +} + +int ListModel::columnCount(const QModelIndex &parent) const +{ + return 1; +} + +QVariant ListModel::data(const QModelIndex &index, int role) const +{ + int pos = index.row(); + if(pos >= modpacks.size() || pos < 0 || !index.isValid()) + { + return QString("INVALID INDEX %1").arg(pos); + } + + IndexedPack pack = modpacks.at(pos); + if(role == Qt::DisplayRole) + { + return pack.name; + } + else if (role == Qt::ToolTipRole) + { + if(pack.description.length() > 100) + { + //some magic to prevent to long tooltips and replace html linebreaks + QString edit = pack.description.left(97); + edit = edit.left(edit.lastIndexOf("<br>")).left(edit.lastIndexOf(" ")).append("..."); + return edit; + + } + return pack.description; + } + else if(role == Qt::DecorationRole) + { + if(m_logoMap.contains(pack.logoName)) + { + return (m_logoMap.value(pack.logoName)); + } + QIcon icon = APPLICATION->getThemedIcon("screenshot-placeholder"); + ((ListModel *)this)->requestLogo(pack.logoName, pack.logoUrl); + return icon; + } + else if(role == Qt::UserRole) + { + QVariant v; + v.setValue(pack); + return v; + } + + return QVariant(); +} + +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::requestLogo(QString logo, QString url) +{ + if(m_loadingLogos.contains(logo) || m_failedLogos.contains(logo)) + { + return; + } + + MetaEntryPtr entry = APPLICATION->metacache()->resolveEntry("FlameMods", QString("logos/%1").arg(logo.section(".", 0, 0))); + auto job = new NetJob(QString("Flame Icon Download %1").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); +} + +void ListModel::getLogo(const QString &logo, const QString &logoUrl, LogoCallback callback) +{ + if(m_logoMap.contains(logo)) + { + callback(APPLICATION->metacache()->resolveEntry("FlameMods", QString("logos/%1").arg(logo.section(".", 0, 0)))->getFullPath()); + } + else + { + requestLogo(logo, logoUrl); + } +} + +Qt::ItemFlags ListModel::flags(const QModelIndex &index) const +{ + return QAbstractListModel::flags(index); +} + +bool ListModel::canFetchMore(const QModelIndex& parent) const +{ + return searchState == CanPossiblyFetchMore; +} + +void ListModel::fetchMore(const QModelIndex& parent) +{ + if (parent.isValid()) + return; + if(nextSearchOffset == 0) { + qWarning() << "fetchMore with 0 offset is wrong..."; + return; + } + performPaginatedSearch(); +} +const char* sorts[6]{"Featured","Popularity","LastUpdated","Name","Author","TotalDownloads"}; + +void ListModel::performPaginatedSearch() +{ + + QString mcVersion = ((MinecraftInstance *)((FlameModPage *)parent())->m_instance)->getPackProfile()->getComponentVersion("net.minecraft"); + bool hasFabric = !((MinecraftInstance *)((FlameModPage *)parent())->m_instance)->getPackProfile()->getComponentVersion("net.fabricmc.fabric-loader").isEmpty(); + auto netJob = new NetJob("Flame::Search", APPLICATION->network()); + auto searchUrl = QString( + "https://addons-ecs.forgesvc.net/api/v2/addon/search?" + "gameId=432&" + "categoryId=0&" + "sectionId=6&" + + "index=%1&" + "pageSize=25&" + "searchFilter=%2&" + "sort=%3&" + "%4" + "gameVersion=%5" + ) + .arg(nextSearchOffset) + .arg(currentSearchTerm) + .arg(sorts[currentSort]) + .arg(hasFabric ? "modLoaderType=4&" : "") + .arg(mcVersion); + + 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); +} + +void ListModel::searchWithTerm(const QString &term, const int sort) +{ + if(currentSearchTerm == term && currentSearchTerm.isNull() == term.isNull() && currentSort == sort) { + return; + } + currentSearchTerm = term; + currentSort = sort; + if(jobPtr) { + jobPtr->abort(); + searchState = ResetRequested; + return; + } + else { + beginResetModel(); + modpacks.clear(); + endResetModel(); + searchState = None; + } + nextSearchOffset = 0; + performPaginatedSearch(); +} + +void ListModel::searchRequestFinished() +{ + jobPtr.reset(); + + QJsonParseError parse_error; + QJsonDocument doc = QJsonDocument::fromJson(response, &parse_error); + if(parse_error.error != QJsonParseError::NoError) { + qWarning() << "Error while parsing JSON response from Flame at " << parse_error.offset << " reason: " << parse_error.errorString(); + qWarning() << response; + return; + } + + QList<FlameMod::IndexedPack> newList; + auto packs = doc.array(); + for(auto packRaw : packs) { + auto packObj = packRaw.toObject(); + + FlameMod::IndexedPack pack; + try + { + FlameMod::loadIndexedPack(pack, packObj); + newList.append(pack); + } + catch(const JSONValidationError &e) + { + qWarning() << "Error while loading mod from Flame: " << e.cause(); + continue; + } + } + if(packs.size() < 25) { + searchState = Finished; + } else { + nextSearchOffset += 25; + searchState = CanPossiblyFetchMore; + } + beginInsertRows(QModelIndex(), modpacks.size(), modpacks.size() + newList.size() - 1); + modpacks.append(newList); + endInsertRows(); +} + +void ListModel::searchRequestFailed(QString reason) +{ + jobPtr.reset(); + + if(searchState == ResetRequested) { + beginResetModel(); + modpacks.clear(); + endResetModel(); + + nextSearchOffset = 0; + performPaginatedSearch(); + } else { + searchState = Finished; + } +} + +} + diff --git a/launcher/ui/pages/modplatform/flame/FlameModModel.h b/launcher/ui/pages/modplatform/flame/FlameModModel.h new file mode 100644 index 00000000..0c1cb95e --- /dev/null +++ b/launcher/ui/pages/modplatform/flame/FlameModModel.h @@ -0,0 +1,79 @@ +#pragma once + +#include <RWStorage.h> + +#include <QAbstractListModel> +#include <QSortFilterProxyModel> +#include <QThreadPool> +#include <QIcon> +#include <QStyledItemDelegate> +#include <QList> +#include <QString> +#include <QStringList> +#include <QMetaType> + +#include <functional> +#include <net/NetJob.h> + +#include <modplatform/flame/FlamePackIndex.h> +#include "modplatform/flame/FlameModIndex.h" +#include "BaseInstance.h" +#include "FlameModPage.h" + +namespace FlameMod { + + +typedef QMap<QString, QIcon> LogoMap; +typedef std::function<void(QString)> LogoCallback; + +class ListModel : public QAbstractListModel +{ + Q_OBJECT + +public: + ListModel(FlameModPage *parent); + virtual ~ListModel(); + + int rowCount(const QModelIndex &parent) const override; + int columnCount(const QModelIndex &parent) const override; + QVariant data(const QModelIndex &index, int role) const override; + Qt::ItemFlags flags(const QModelIndex &index) const override; + bool canFetchMore(const QModelIndex & parent) const override; + void fetchMore(const QModelIndex & parent) override; + + void getLogo(const QString &logo, const QString &logoUrl, LogoCallback callback); + void searchWithTerm(const QString &term, const int sort); + +private slots: + void performPaginatedSearch(); + + void logoFailed(QString logo); + void logoLoaded(QString logo, QIcon out); + + void searchRequestFinished(); + void searchRequestFailed(QString reason); + +private: + void requestLogo(QString file, QString url); + +private: + QList<IndexedPack> modpacks; + QStringList m_failedLogos; + QStringList m_loadingLogos; + LogoMap m_logoMap; + QMap<QString, LogoCallback> waitingCallbacks; + + QString currentSearchTerm; + int currentSort = 0; + int nextSearchOffset = 0; + enum SearchState { + None, + CanPossiblyFetchMore, + ResetRequested, + Finished + } searchState = None; + NetJob::Ptr jobPtr; + QByteArray response; +}; + +} diff --git a/launcher/ui/pages/modplatform/flame/FlameModPage.cpp b/launcher/ui/pages/modplatform/flame/FlameModPage.cpp new file mode 100644 index 00000000..a816c681 --- /dev/null +++ b/launcher/ui/pages/modplatform/flame/FlameModPage.cpp @@ -0,0 +1,196 @@ +#include "FlameModPage.h" +#include "ui_FlameModPage.h" + +#include <QKeyEvent> + +#include "Application.h" +#include "Json.h" +#include "ui/dialogs/ModDownloadDialog.h" +#include "InstanceImportTask.h" +#include "FlameModModel.h" +#include "ModDownloadTask.h" +#include "minecraft/MinecraftInstance.h" +#include "minecraft/PackProfile.h" + +FlameModPage::FlameModPage(ModDownloadDialog *dialog, BaseInstance *instance) + : QWidget(dialog), m_instance(instance), ui(new Ui::FlameModPage), dialog(dialog) +{ + ui->setupUi(this); + connect(ui->searchButton, &QPushButton::clicked, this, &FlameModPage::triggerSearch); + ui->searchEdit->installEventFilter(this); + listModel = new FlameMod::ListModel(this); + ui->packView->setModel(listModel); + + ui->versionSelectionBox->view()->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); + ui->versionSelectionBox->view()->parentWidget()->setMaximumHeight(300); + + // 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")); + + 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); +} + +FlameModPage::~FlameModPage() +{ + delete ui; +} + +bool FlameModPage::eventFilter(QObject* watched, QEvent* event) +{ + if (watched == ui->searchEdit && event->type() == QEvent::KeyPress) { + QKeyEvent* keyEvent = static_cast<QKeyEvent*>(event); + if (keyEvent->key() == Qt::Key_Return) { + triggerSearch(); + keyEvent->accept(); + return true; + } + } + return QWidget::eventFilter(watched, event); +} + +bool FlameModPage::shouldDisplay() const +{ + return true; +} + +void FlameModPage::openedImpl() +{ + suggestCurrent(); + triggerSearch(); +} + +void FlameModPage::triggerSearch() +{ + listModel->searchWithTerm(ui->searchEdit->text(), ui->sortByBox->currentIndex()); +} + +void FlameModPage::onSelectionChanged(QModelIndex first, QModelIndex second) +{ + ui->versionSelectionBox->clear(); + + if(!first.isValid()) + { + if(isOpened) + { + dialog->setSuggestedMod(); + } + return; + } + + current = listModel->data(first, Qt::UserRole).value<FlameMod::IndexedPack>(); + QString text = ""; + QString name = current.name; + + if (current.websiteUrl.isEmpty()) + text = name; + else + text = "<a href=\"" + current.websiteUrl + "\">" + name + "</a>"; + if (!current.authors.empty()) { + auto authorToStr = [](FlameMod::ModpackAuthor & author) { + if(author.url.isEmpty()) { + return author.name; + } + return QString("<a href=\"%1\">%2</a>").arg(author.url, author.name); + }; + QStringList authorStrs; + for(auto & author: current.authors) { + authorStrs.push_back(authorToStr(author)); + } + text += "<br>" + tr(" by ") + authorStrs.join(", "); + } + text += "<br><br>"; + + ui->packDescription->setHtml(text + current.description); + + if (!current.versionsLoaded) + { + qDebug() << "Loading flame mod versions"; + auto netJob = new NetJob(QString("Flame::ModVersions(%1)").arg(current.name), APPLICATION->network()); + std::shared_ptr<QByteArray> response = std::make_shared<QByteArray>(); + int addonId = current.addonId; + netJob->addNetAction(Net::Download::makeByteArray(QString("https://addons-ecs.forgesvc.net/api/v2/addon/%1/files").arg(addonId), response.get())); + + QObject::connect(netJob, &NetJob::succeeded, this, [this, response, netJob] + { + netJob->deleteLater(); + QJsonParseError parse_error; + QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); + if(parse_error.error != QJsonParseError::NoError) { + qWarning() << "Error while parsing JSON response from Flame at " << parse_error.offset << " reason: " << parse_error.errorString(); + qWarning() << *response; + return; + } + QJsonArray arr = doc.array(); + try + { + FlameMod::loadIndexedPackVersions(current, arr, APPLICATION->network(), m_instance); + } + catch(const JSONValidationError &e) + { + qDebug() << *response; + qWarning() << "Error while reading Flame mod version: " << e.cause(); + } + auto packProfile = ((MinecraftInstance *)m_instance)->getPackProfile(); + QString mcVersion = packProfile->getComponentVersion("net.minecraft"); + QString loaderString = (packProfile->getComponentVersion("net.minecraftforge").isEmpty()) ? "fabric" : "forge"; + for(int i = 0; i < current.versions.size(); i++) { + auto version = current.versions[i]; + if(!version.mcVersion.contains(mcVersion)){ + continue; + } + ui->versionSelectionBox->addItem(version.version, QVariant(i)); + } + if(ui->versionSelectionBox->count() == 0){ + ui->versionSelectionBox->addItem(tr("No Valid Version found!"), QVariant(-1)); + } + + suggestCurrent(); + }); + netJob->start(); + } + 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)); + } + suggestCurrent(); + } +} + +void FlameModPage::suggestCurrent() +{ + if(!isOpened) + { + return; + } + + if (selectedVersion == -1) + { + dialog->setSuggestedMod(); + return; + } + + auto version = current.versions[selectedVersion]; + dialog->setSuggestedMod(current.name, new ModDownloadTask(version.downloadUrl, version.fileName , dialog->mods)); +} + +void FlameModPage::onVersionSelectionChanged(QString data) +{ + if(data.isNull() || data.isEmpty()) + { + selectedVersion = -1; + return; + } + selectedVersion = ui->versionSelectionBox->currentData().toInt(); + suggestCurrent(); +} diff --git a/launcher/ui/pages/modplatform/flame/FlameModPage.h b/launcher/ui/pages/modplatform/flame/FlameModPage.h new file mode 100644 index 00000000..8fa3248a --- /dev/null +++ b/launcher/ui/pages/modplatform/flame/FlameModPage.h @@ -0,0 +1,67 @@ +#pragma once + +#include <QWidget> + +#include "ui/pages/BasePage.h" +#include <Application.h> +#include "tasks/Task.h" +#include "modplatform/flame/FlameModIndex.h" + +namespace Ui +{ +class FlameModPage; +} + +class ModDownloadDialog; + +namespace FlameMod { + class ListModel; +} + +class FlameModPage : public QWidget, public BasePage +{ + Q_OBJECT + +public: + explicit FlameModPage(ModDownloadDialog *dialog, BaseInstance *instance); + virtual ~FlameModPage(); + virtual QString displayName() const override + { + return tr("CurseForge"); + } + virtual QIcon icon() const override + { + return APPLICATION->getThemedIcon("flame"); + } + virtual QString id() const override + { + return "curseforge"; + } + virtual QString helpPage() const override + { + return "Flame-platform"; + } + virtual bool shouldDisplay() const override; + + void openedImpl() override; + + bool eventFilter(QObject * watched, QEvent * event) override; + + BaseInstance *m_instance; + +private: + void suggestCurrent(); + +private slots: + void triggerSearch(); + void onSelectionChanged(QModelIndex first, QModelIndex second); + void onVersionSelectionChanged(QString data); + +private: + Ui::FlameModPage *ui = nullptr; + ModDownloadDialog* dialog = nullptr; + FlameMod::ListModel* listModel = nullptr; + FlameMod::IndexedPack current; + + int selectedVersion = -1; +}; diff --git a/launcher/ui/pages/modplatform/flame/FlameModPage.ui b/launcher/ui/pages/modplatform/flame/FlameModPage.ui new file mode 100644 index 00000000..7da0bb4a --- /dev/null +++ b/launcher/ui/pages/modplatform/flame/FlameModPage.ui @@ -0,0 +1,90 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>FlameModPage</class> + <widget class="QWidget" name="FlameModPage"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>837</width> + <height>685</height> + </rect> + </property> + <layout class="QGridLayout" name="gridLayout"> + <item row="1" column="0" colspan="2"> + <layout class="QGridLayout" name="gridLayout_3"> + <item row="1" column="0"> + <widget class="QListView" name="packView"> + <property name="iconSize"> + <size> + <width>48</width> + <height>48</height> + </size> + </property> + <property name="horizontalScrollBarPolicy"> + <enum>Qt::ScrollBarAlwaysOff</enum> + </property> + <property name="alternatingRowColors"> + <bool>true</bool> + </property> + </widget> + </item> + <item row="1" column="1"> + <widget class="QTextBrowser" name="packDescription"> + <property name="openExternalLinks"> + <bool>true</bool> + </property> + <property name="openLinks"> + <bool>true</bool> + </property> + </widget> + </item> + </layout> + </item> + <item row="2" column="0" colspan="2"> + <layout class="QGridLayout" name="gridLayout_4" columnstretch="0,0,0" rowminimumheight="0" columnminimumwidth="0,0,0"> + <item row="0" column="2"> + <widget class="QComboBox" name="versionSelectionBox"/> + </item> + <item row="0" column="1"> + <widget class="QLabel" name="label"> + <property name="text"> + <string>Version selected:</string> + </property> + <property name="alignment"> + <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set> + </property> + </widget> + </item> + <item row="0" column="0"> + <widget class="QComboBox" name="sortByBox"/> + </item> + </layout> + </item> + <item row="0" column="1"> + <widget class="QPushButton" name="searchButton"> + <property name="text"> + <string>Search</string> + </property> + </widget> + </item> + <item row="0" column="0"> + <widget class="QLineEdit" name="searchEdit"> + <property name="placeholderText"> + <string>Search and filter ...</string> + </property> + </widget> + </item> + </layout> + </widget> + <tabstops> + <tabstop>searchEdit</tabstop> + <tabstop>searchButton</tabstop> + <tabstop>packView</tabstop> + <tabstop>packDescription</tabstop> + <tabstop>sortByBox</tabstop> + <tabstop>versionSelectionBox</tabstop> + </tabstops> + <resources/> + <connections/> +</ui> diff --git a/launcher/ui/pages/modplatform/flame/FlameModel.cpp b/launcher/ui/pages/modplatform/flame/FlameModel.cpp index 891676cf..fe163cae 100644 --- a/launcher/ui/pages/modplatform/flame/FlameModel.cpp +++ b/launcher/ui/pages/modplatform/flame/FlameModel.cpp @@ -6,9 +6,6 @@ #include <Version.h> #include <QtMath> -#include <QLabel> - -#include <RWStorage.h> namespace Flame { @@ -100,12 +97,13 @@ void ListModel::requestLogo(QString logo, QString url) } MetaEntryPtr entry = APPLICATION->metacache()->resolveEntry("FlamePacks", QString("logos/%1").arg(logo.section(".", 0, 0))); - NetJob *job = new NetJob(QString("Flame Icon Download %1").arg(logo), APPLICATION->network()); + auto job = new NetJob(QString("Flame Icon Download %1").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] + QObject::connect(job, &NetJob::succeeded, this, [this, logo, fullPath, job] { + job->deleteLater(); emit logoLoaded(logo, QIcon(fullPath)); if(waitingCallbacks.contains(logo)) { @@ -113,8 +111,9 @@ void ListModel::requestLogo(QString logo, QString url) } }); - QObject::connect(job, &NetJob::failed, this, [this, logo] + QObject::connect(job, &NetJob::failed, this, [this, logo, job] { + job->deleteLater(); emit logoFailed(logo); }); diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp new file mode 100644 index 00000000..5a18830a --- /dev/null +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp @@ -0,0 +1,276 @@ +#include "ModrinthModel.h" +#include "Application.h" +#include "minecraft/MinecraftInstance.h" +#include "minecraft/PackProfile.h" +#include "ModrinthPage.h" +#include "ui/dialogs/ModDownloadDialog.h" +#include <Json.h> + +#include <MMCStrings.h> +#include <Version.h> + +#include <QtMath> +#include <QMessageBox> + + +namespace Modrinth { + +ListModel::ListModel(ModrinthPage *parent) : QAbstractListModel(parent) +{ +} + +ListModel::~ListModel() +{ +} + +int ListModel::rowCount(const QModelIndex &parent) const +{ + return modpacks.size(); +} + +int ListModel::columnCount(const QModelIndex &parent) const +{ + return 1; +} + +QVariant ListModel::data(const QModelIndex &index, int role) const +{ + int pos = index.row(); + if(pos >= modpacks.size() || pos < 0 || !index.isValid()) + { + return QString("INVALID INDEX %1").arg(pos); + } + + IndexedPack pack = modpacks.at(pos); + if(role == Qt::DisplayRole) + { + return pack.name; + } + else if (role == Qt::ToolTipRole) + { + if(pack.description.length() > 100) + { + //some magic to prevent to long tooltips and replace html linebreaks + QString edit = pack.description.left(97); + edit = edit.left(edit.lastIndexOf("<br>")).left(edit.lastIndexOf(" ")).append("..."); + return edit; + + } + return pack.description; + } + else if(role == Qt::DecorationRole) + { + if(m_logoMap.contains(pack.logoName)) + { + return (m_logoMap.value(pack.logoName)); + } + QIcon icon = APPLICATION->getThemedIcon("screenshot-placeholder"); + ((ListModel *)this)->requestLogo(pack.logoName, pack.logoUrl); + return icon; + } + else if(role == Qt::UserRole) + { + QVariant v; + v.setValue(pack); + return v; + } + + return QVariant(); +} + +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::requestLogo(QString logo, QString url) +{ + if(m_loadingLogos.contains(logo) || m_failedLogos.contains(logo)) + { + return; + } + + MetaEntryPtr entry = APPLICATION->metacache()->resolveEntry("ModrinthPacks", QString("logos/%1").arg(logo.section(".", 0, 0))); + auto job = new NetJob(QString("Modrinth Icon Download %1").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); +} + +void ListModel::getLogo(const QString &logo, const QString &logoUrl, LogoCallback callback) +{ + if(m_logoMap.contains(logo)) + { + callback(APPLICATION->metacache()->resolveEntry("ModrinthPacks", QString("logos/%1").arg(logo.section(".", 0, 0)))->getFullPath()); + } + else + { + requestLogo(logo, logoUrl); + } +} + +Qt::ItemFlags ListModel::flags(const QModelIndex &index) const +{ + return QAbstractListModel::flags(index); +} + +bool ListModel::canFetchMore(const QModelIndex& parent) const +{ + return searchState == CanPossiblyFetchMore; +} + +void ListModel::fetchMore(const QModelIndex& parent) +{ + if (parent.isValid()) + return; + if(nextSearchOffset == 0) { + qWarning() << "fetchMore with 0 offset is wrong..."; + return; + } + performPaginatedSearch(); +} +const char* sorts[5]{"relevance","downloads","follows","updated","newest"}; + +void ListModel::performPaginatedSearch() +{ + + QString mcVersion = ((MinecraftInstance *)((ModrinthPage *)parent())->m_instance)->getPackProfile()->getComponentVersion("net.minecraft"); + bool hasFabric = !((MinecraftInstance *)((ModrinthPage *)parent())->m_instance)->getPackProfile()->getComponentVersion("net.fabricmc.fabric-loader").isEmpty(); + auto netJob = new NetJob("Modrinth::Search", APPLICATION->network()); + auto searchUrl = QString( + "https://api.modrinth.com/v2/search?" + "offset=%1&" + "limit=25&" + "query=%2&" + "index=%3&" + "facets=[[\"categories:%4\"],[\"versions:%5\"],[\"project_type:mod\"]]" + ) + .arg(nextSearchOffset) + .arg(currentSearchTerm) + .arg(sorts[currentSort]) + .arg(hasFabric ? "fabric" : "forge") + .arg(mcVersion); + + 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); +} + +void ListModel::searchWithTerm(const QString &term, const int sort) +{ + if(currentSearchTerm == term && currentSearchTerm.isNull() == term.isNull() && currentSort == sort) { + return; + } + currentSearchTerm = term; + currentSort = sort; + if(jobPtr) { + jobPtr->abort(); + searchState = ResetRequested; + return; + } + else { + beginResetModel(); + modpacks.clear(); + endResetModel(); + searchState = None; + } + nextSearchOffset = 0; + performPaginatedSearch(); +} + +void Modrinth::ListModel::searchRequestFinished() +{ + jobPtr.reset(); + + QJsonParseError parse_error; + QJsonDocument doc = QJsonDocument::fromJson(response, &parse_error); + if(parse_error.error != QJsonParseError::NoError) { + qWarning() << "Error while parsing JSON response from Modrinth at " << parse_error.offset << " reason: " << parse_error.errorString(); + qWarning() << response; + return; + } + + QList<Modrinth::IndexedPack> newList; + auto packs = doc.object().value("hits").toArray(); + for(auto packRaw : packs) { + auto packObj = packRaw.toObject(); + + Modrinth::IndexedPack pack; + try + { + Modrinth::loadIndexedPack(pack, packObj); + newList.append(pack); + } + catch(const JSONValidationError &e) + { + qWarning() << "Error while loading mod from Modrinth: " << e.cause(); + continue; + } + } + if(packs.size() < 25) { + searchState = Finished; + } else { + nextSearchOffset += 25; + searchState = CanPossiblyFetchMore; + } + beginInsertRows(QModelIndex(), modpacks.size(), modpacks.size() + newList.size() - 1); + modpacks.append(newList); + endInsertRows(); +} + +void Modrinth::ListModel::searchRequestFailed(QString reason) +{ + if(jobPtr->first()->m_reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 409){ + //409 Gone, notify user to update + QMessageBox::critical(nullptr, tr("Error"), tr("Modrinth API version too old!\nPlease update PolyMC!")); + //self-destruct + ((ModDownloadDialog *)((ModrinthPage *)parent())->parentWidget())->reject(); + } + jobPtr.reset(); + + if(searchState == ResetRequested) { + beginResetModel(); + modpacks.clear(); + endResetModel(); + + nextSearchOffset = 0; + performPaginatedSearch(); + } else { + searchState = Finished; + } +} + +} + diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h new file mode 100644 index 00000000..53f1f134 --- /dev/null +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h @@ -0,0 +1,79 @@ +#pragma once + +#include <RWStorage.h> + +#include <QAbstractListModel> +#include <QSortFilterProxyModel> +#include <QThreadPool> +#include <QIcon> +#include <QStyledItemDelegate> +#include <QList> +#include <QString> +#include <QStringList> +#include <QMetaType> + +#include <functional> +#include <net/NetJob.h> + +#include <modplatform/flame/FlamePackIndex.h> +#include "modplatform/modrinth/ModrinthPackIndex.h" +#include "BaseInstance.h" +#include "ModrinthPage.h" + +namespace Modrinth { + + +typedef QMap<QString, QIcon> LogoMap; +typedef std::function<void(QString)> LogoCallback; + +class ListModel : public QAbstractListModel +{ + Q_OBJECT + +public: + ListModel(ModrinthPage *parent); + virtual ~ListModel(); + + int rowCount(const QModelIndex &parent) const override; + int columnCount(const QModelIndex &parent) const override; + QVariant data(const QModelIndex &index, int role) const override; + Qt::ItemFlags flags(const QModelIndex &index) const override; + bool canFetchMore(const QModelIndex & parent) const override; + void fetchMore(const QModelIndex & parent) override; + + void getLogo(const QString &logo, const QString &logoUrl, LogoCallback callback); + void searchWithTerm(const QString &term, const int sort); + +private slots: + void performPaginatedSearch(); + + void logoFailed(QString logo); + void logoLoaded(QString logo, QIcon out); + + void searchRequestFinished(); + void searchRequestFailed(QString reason); + +private: + void requestLogo(QString file, QString url); + +private: + QList<IndexedPack> modpacks; + QStringList m_failedLogos; + QStringList m_loadingLogos; + LogoMap m_logoMap; + QMap<QString, LogoCallback> waitingCallbacks; + + QString currentSearchTerm; + int currentSort = 0; + int nextSearchOffset = 0; + enum SearchState { + None, + CanPossiblyFetchMore, + ResetRequested, + Finished + } searchState = None; + NetJob::Ptr jobPtr; + QByteArray response; +}; + +} diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp new file mode 100644 index 00000000..c5a54c29 --- /dev/null +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp @@ -0,0 +1,180 @@ +#include "ModrinthPage.h" +#include "ui_ModrinthPage.h" + +#include <QKeyEvent> + +#include "Application.h" +#include "Json.h" +#include "ui/dialogs/ModDownloadDialog.h" +#include "InstanceImportTask.h" +#include "ModrinthModel.h" +#include "ModDownloadTask.h" +#include "minecraft/MinecraftInstance.h" +#include "minecraft/PackProfile.h" + +ModrinthPage::ModrinthPage(ModDownloadDialog *dialog, BaseInstance *instance) + : QWidget(dialog), m_instance(instance), ui(new Ui::ModrinthPage), dialog(dialog) +{ + ui->setupUi(this); + connect(ui->searchButton, &QPushButton::clicked, this, &ModrinthPage::triggerSearch); + ui->searchEdit->installEventFilter(this); + listModel = new Modrinth::ListModel(this); + ui->packView->setModel(listModel); + + ui->versionSelectionBox->view()->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); + ui->versionSelectionBox->view()->parentWidget()->setMaximumHeight(300); + + // index is used to set the sorting with the modrinth api + ui->sortByBox->addItem(tr("Sort by Relevence")); + 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")); + + connect(ui->sortByBox, SIGNAL(currentIndexChanged(int)), this, SLOT(triggerSearch())); + connect(ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &ModrinthPage::onSelectionChanged); + connect(ui->versionSelectionBox, &QComboBox::currentTextChanged, this, &ModrinthPage::onVersionSelectionChanged); +} + +ModrinthPage::~ModrinthPage() +{ + delete ui; +} + +bool ModrinthPage::eventFilter(QObject* watched, QEvent* event) +{ + if (watched == ui->searchEdit && event->type() == QEvent::KeyPress) { + QKeyEvent* keyEvent = static_cast<QKeyEvent*>(event); + if (keyEvent->key() == Qt::Key_Return) { + triggerSearch(); + keyEvent->accept(); + return true; + } + } + return QWidget::eventFilter(watched, event); +} + +bool ModrinthPage::shouldDisplay() const +{ + return true; +} + +void ModrinthPage::openedImpl() +{ + suggestCurrent(); + triggerSearch(); +} + +void ModrinthPage::triggerSearch() +{ + listModel->searchWithTerm(ui->searchEdit->text(), ui->sortByBox->currentIndex()); +} + +void ModrinthPage::onSelectionChanged(QModelIndex first, QModelIndex second) +{ + ui->versionSelectionBox->clear(); + + if(!first.isValid()) + { + if(isOpened) + { + dialog->setSuggestedMod(); + } + return; + } + + current = listModel->data(first, Qt::UserRole).value<Modrinth::IndexedPack>(); + QString text = ""; + QString name = current.name; + + if (current.websiteUrl.isEmpty()) + text = name; + else + text = "<a href=\"" + current.websiteUrl + "\">" + name + "</a>"; + text += "<br>"+ tr(" by ") + "<a href=\""+current.author.url+"\">"+current.author.name+"</a><br><br>"; + ui->packDescription->setHtml(text + current.description); + + if (!current.versionsLoaded) + { + qDebug() << "Loading Modrinth mod versions"; + auto netJob = new NetJob(QString("Modrinth::ModVersions(%1)").arg(current.name), APPLICATION->network()); + std::shared_ptr<QByteArray> response = std::make_shared<QByteArray>(); + QString addonId = current.addonId; + netJob->addNetAction(Net::Download::makeByteArray(QString("https://api.modrinth.com/v2/project/%1/version").arg(addonId), response.get())); + + QObject::connect(netJob, &NetJob::succeeded, this, [this, response, netJob] + { + netJob->deleteLater(); + QJsonParseError parse_error; + QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); + if(parse_error.error != QJsonParseError::NoError) { + qWarning() << "Error while parsing JSON response from Modrinth at " << parse_error.offset << " reason: " << parse_error.errorString(); + qWarning() << *response; + return; + } + QJsonArray arr = doc.array(); + try + { + Modrinth::loadIndexedPackVersions(current, arr, APPLICATION->network(), m_instance); + } + catch(const JSONValidationError &e) + { + qDebug() << *response; + qWarning() << "Error while reading Modrinth mod version: " << e.cause(); + } + auto packProfile = ((MinecraftInstance *)m_instance)->getPackProfile(); + QString mcVersion = packProfile->getComponentVersion("net.minecraft"); + QString loaderString = (packProfile->getComponentVersion("net.minecraftforge").isEmpty()) ? "fabric" : "forge"; + for(int i = 0; i < current.versions.size(); i++) { + auto version = current.versions[i]; + if(!version.mcVersion.contains(mcVersion) || !version.loaders.contains(loaderString)){ + continue; + } + ui->versionSelectionBox->addItem(version.version, QVariant(i)); + } + if(ui->versionSelectionBox->count() == 0){ + ui->versionSelectionBox->addItem(tr("No Valid Version found !"), QVariant(-1)); + } + + suggestCurrent(); + }); + netJob->start(); + } + 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)); + } + suggestCurrent(); + } +} + +void ModrinthPage::suggestCurrent() +{ + if(!isOpened) + { + return; + } + + if (selectedVersion == -1) + { + dialog->setSuggestedMod(); + return; + } + auto version = current.versions[selectedVersion]; + dialog->setSuggestedMod(current.name, new ModDownloadTask(version.downloadUrl, version.fileName , dialog->mods)); +} + +void ModrinthPage::onVersionSelectionChanged(QString data) +{ + if(data.isNull() || data.isEmpty()) + { + selectedVersion = -1; + return; + } + selectedVersion = ui->versionSelectionBox->currentData().toInt(); + suggestCurrent(); +} diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h new file mode 100644 index 00000000..3c517069 --- /dev/null +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h @@ -0,0 +1,67 @@ +#pragma once + +#include <QWidget> + +#include "ui/pages/BasePage.h" +#include <Application.h> +#include "tasks/Task.h" +#include "modplatform/modrinth/ModrinthPackIndex.h" + +namespace Ui +{ +class ModrinthPage; +} + +class ModDownloadDialog; + +namespace Modrinth { + class ListModel; +} + +class ModrinthPage : public QWidget, public BasePage +{ + Q_OBJECT + +public: + explicit ModrinthPage(ModDownloadDialog *dialog, BaseInstance *instance); + virtual ~ModrinthPage(); + virtual QString displayName() const override + { + return tr("Modrinth"); + } + virtual QIcon icon() const override + { + return APPLICATION->getThemedIcon("modrinth"); + } + virtual QString id() const override + { + return "modrinth"; + } + virtual QString helpPage() const override + { + return "Modrinth-platform"; + } + virtual bool shouldDisplay() const override; + + void openedImpl() override; + + bool eventFilter(QObject * watched, QEvent * event) override; + + BaseInstance *m_instance; + +private: + void suggestCurrent(); + +private slots: + void triggerSearch(); + void onSelectionChanged(QModelIndex first, QModelIndex second); + void onVersionSelectionChanged(QString data); + +private: + Ui::ModrinthPage *ui = nullptr; + ModDownloadDialog* dialog = nullptr; + Modrinth::ListModel* listModel = nullptr; + Modrinth::IndexedPack current; + + int selectedVersion = -1; +}; diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.ui b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.ui new file mode 100644 index 00000000..6d183de5 --- /dev/null +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.ui @@ -0,0 +1,90 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>ModrinthPage</class> + <widget class="QWidget" name="ModrinthPage"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>837</width> + <height>685</height> + </rect> + </property> + <layout class="QGridLayout" name="gridLayout"> + <item row="1" column="0" colspan="2"> + <layout class="QGridLayout" name="gridLayout_3"> + <item row="1" column="0"> + <widget class="QListView" name="packView"> + <property name="iconSize"> + <size> + <width>48</width> + <height>48</height> + </size> + </property> + <property name="horizontalScrollBarPolicy"> + <enum>Qt::ScrollBarAlwaysOff</enum> + </property> + <property name="alternatingRowColors"> + <bool>true</bool> + </property> + </widget> + </item> + <item row="1" column="1"> + <widget class="QTextBrowser" name="packDescription"> + <property name="openExternalLinks"> + <bool>true</bool> + </property> + <property name="openLinks"> + <bool>true</bool> + </property> + </widget> + </item> + </layout> + </item> + <item row="2" column="0" colspan="2"> + <layout class="QGridLayout" name="gridLayout_4" columnstretch="0,0,0" rowminimumheight="0" columnminimumwidth="0,0,0"> + <item row="0" column="2"> + <widget class="QComboBox" name="versionSelectionBox"/> + </item> + <item row="0" column="1"> + <widget class="QLabel" name="label"> + <property name="text"> + <string>Version selected:</string> + </property> + <property name="alignment"> + <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set> + </property> + </widget> + </item> + <item row="0" column="0"> + <widget class="QComboBox" name="sortByBox"/> + </item> + </layout> + </item> + <item row="0" column="1"> + <widget class="QPushButton" name="searchButton"> + <property name="text"> + <string>Search</string> + </property> + </widget> + </item> + <item row="0" column="0"> + <widget class="QLineEdit" name="searchEdit"> + <property name="placeholderText"> + <string>Search and filter ...</string> + </property> + </widget> + </item> + </layout> + </widget> + <tabstops> + <tabstop>searchEdit</tabstop> + <tabstop>searchButton</tabstop> + <tabstop>packView</tabstop> + <tabstop>packDescription</tabstop> + <tabstop>sortByBox</tabstop> + <tabstop>versionSelectionBox</tabstop> + </tabstops> + <resources/> + <connections/> +</ui> diff --git a/launcher/ui/widgets/LanguageSelectionWidget.cpp b/launcher/ui/widgets/LanguageSelectionWidget.cpp index cf70c7b4..256b09da 100644 --- a/launcher/ui/widgets/LanguageSelectionWidget.cpp +++ b/launcher/ui/widgets/LanguageSelectionWidget.cpp @@ -5,7 +5,9 @@ #include <QHeaderView> #include <QLabel> #include "Application.h" +#include "BuildConfig.h" #include "translations/TranslationsModel.h" +#include "settings/Setting.h" LanguageSelectionWidget::LanguageSelectionWidget(QWidget *parent) : QWidget(parent) @@ -37,6 +39,9 @@ LanguageSelectionWidget::LanguageSelectionWidget(QWidget *parent) : languageView->header()->setSectionResizeMode(0, QHeaderView::Stretch); connect(languageView->selectionModel(), &QItemSelectionModel::currentRowChanged, this, &LanguageSelectionWidget::languageRowChanged); verticalLayout->setContentsMargins(0,0,0,0); + + auto language_setting = APPLICATION->settings()->getSetting("Language"); + connect(language_setting.get(), &Setting::SettingChanged, this, &LanguageSelectionWidget::languageSettingChanged); } QString LanguageSelectionWidget::getSelectedLanguageKey() const @@ -48,7 +53,7 @@ QString LanguageSelectionWidget::getSelectedLanguageKey() const void LanguageSelectionWidget::retranslate() { QString text = tr("Don't see your language or the quality is poor?<br/><a href=\"%1\">Help us with translations!</a>") - .arg("https://github.com/MultiMC/Launcher/wiki/Translating-MultiMC"); + .arg(BuildConfig.TRANSLATIONS_URL); helpUsLabel->setText(text); } @@ -64,3 +69,10 @@ void LanguageSelectionWidget::languageRowChanged(const QModelIndex& current, con translations->selectLanguage(key); translations->updateLanguage(key); } + +void LanguageSelectionWidget::languageSettingChanged(const Setting &, const QVariant) +{ + auto translations = APPLICATION->translations(); + auto index = translations->selectedIndex(); + languageView->setCurrentIndex(index); +} diff --git a/launcher/ui/widgets/LanguageSelectionWidget.h b/launcher/ui/widgets/LanguageSelectionWidget.h index e65936db..4a88924c 100644 --- a/launcher/ui/widgets/LanguageSelectionWidget.h +++ b/launcher/ui/widgets/LanguageSelectionWidget.h @@ -20,6 +20,7 @@ class QVBoxLayout; class QTreeView; class QLabel; +class Setting; class LanguageSelectionWidget: public QWidget { @@ -33,6 +34,7 @@ public: protected slots: void languageRowChanged(const QModelIndex ¤t, const QModelIndex &previous); + void languageSettingChanged(const Setting &, const QVariant); private: QVBoxLayout *verticalLayout = nullptr; diff --git a/launcher/ui/widgets/PageContainer.cpp b/launcher/ui/widgets/PageContainer.cpp index 74a6dff3..6de49467 100644 --- a/launcher/ui/widgets/PageContainer.cpp +++ b/launcher/ui/widgets/PageContainer.cpp @@ -207,7 +207,7 @@ void PageContainer::help() QString pageId = m_currentPage->helpPage(); if (pageId.isEmpty()) return; - DesktopServices::openUrl(QUrl("https://github.com/MultiMC/Launcher/wiki/" + pageId)); + DesktopServices::openUrl(QUrl("https://github.com/PolyMC/PolyMC/wiki/" + pageId)); } } diff --git a/launcher/ui/widgets/WideBar.cpp b/launcher/ui/widgets/WideBar.cpp index cbd6c617..8d5bd12d 100644 --- a/launcher/ui/widgets/WideBar.cpp +++ b/launcher/ui/widgets/WideBar.cpp @@ -76,6 +76,20 @@ void WideBar::addSeparator() m_entries.push_back(entry); } +void WideBar::insertActionBefore(QAction* before, QAction* action){ + auto iter = std::find_if(m_entries.begin(), m_entries.end(), [before](BarEntry * entry) { + return entry->wideAction == before; + }); + if(iter == m_entries.end()) { + return; + } + auto entry = new BarEntry(); + entry->qAction = insertWidget((*iter)->qAction, new ActionButton(action, this)); + entry->wideAction = action; + entry->type = BarEntry::Action; + m_entries.insert(iter, entry); +} + void WideBar::insertSpacer(QAction* action) { auto iter = std::find_if(m_entries.begin(), m_entries.end(), [action](BarEntry * entry) { diff --git a/launcher/ui/widgets/WideBar.h b/launcher/ui/widgets/WideBar.h index d1b8cbe7..2b676a8c 100644 --- a/launcher/ui/widgets/WideBar.h +++ b/launcher/ui/widgets/WideBar.h @@ -18,6 +18,7 @@ public: void addAction(QAction *action); void addSeparator(); void insertSpacer(QAction *action); + void insertActionBefore(QAction *before, QAction *action); QMenu *createContextMenu(QWidget *parent = nullptr, const QString & title = QString()); private: diff --git a/launcher/updater/DownloadTask.h b/launcher/updater/DownloadTask.h index eac26238..f47a3048 100644 --- a/launcher/updater/DownloadTask.h +++ b/launcher/updater/DownloadTask.h @@ -54,7 +54,7 @@ protected: /*! * 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, MultiMC will not delete files that + * If the current version's info file can't be found, PolyMC 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. @@ -97,3 +97,4 @@ private: }; } + diff --git a/launcher/updater/GoUpdate.cpp b/launcher/updater/GoUpdate.cpp index 76f68b55..91f30b5d 100644 --- a/launcher/updater/GoUpdate.cpp +++ b/launcher/updater/GoUpdate.cpp @@ -104,7 +104,7 @@ bool processFileLists } } - // Next, check each file in MultiMC's folder and see if we need to update them. + // Next, check each file in PolyMC'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 diff --git a/launcher/updater/testdata/tst_DownloadTask-test_writeInstallScript.xml b/launcher/updater/testdata/tst_DownloadTask-test_writeInstallScript.xml index 09c162ca..38ecc809 100644 --- a/launcher/updater/testdata/tst_DownloadTask-test_writeInstallScript.xml +++ b/launcher/updater/testdata/tst_DownloadTask-test_writeInstallScript.xml @@ -6,8 +6,8 @@ <mode>0777</mode> </file> <file> - <source>MultiMC.exe</source> - <dest>M/u/l/t/i/M/C/e/x/e</dest> + <source>PolyMC.exe</source> + <dest>P/o/l/y/M/C/e/x/e</dest> <mode>0644</mode> </file> </install> |