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

This commit is contained in:
Trial97 2024-06-09 20:13:09 +03:00
commit 689b76885c
No known key found for this signature in database
GPG Key ID: 55EF5DA53DB36318
75 changed files with 1104 additions and 431 deletions

View File

@ -163,7 +163,6 @@ class Config {
QString RESOURCE_BASE = "https://resources.download.minecraft.net/"; QString RESOURCE_BASE = "https://resources.download.minecraft.net/";
QString LIBRARY_BASE = "https://libraries.minecraft.net/"; QString LIBRARY_BASE = "https://libraries.minecraft.net/";
QString AUTH_BASE = "https://authserver.mojang.com/";
QString IMGUR_BASE_URL = "https://api.imgur.com/3/"; QString IMGUR_BASE_URL = "https://api.imgur.com/3/";
QString FMLLIBS_BASE_URL = "https://files.prismlauncher.org/fmllibs/"; // FIXME: move into CMakeLists QString FMLLIBS_BASE_URL = "https://files.prismlauncher.org/fmllibs/"; // FIXME: move into CMakeLists
QString TRANSLATIONS_BASE_URL = "https://i18n.prismlauncher.org/"; // FIXME: move into CMakeLists QString TRANSLATIONS_BASE_URL = "https://i18n.prismlauncher.org/"; // FIXME: move into CMakeLists

18
flake.lock generated
View File

@ -23,11 +23,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1715865404, "lastModified": 1717285511,
"narHash": "sha256-/GJvTdTpuDjNn84j82cU6bXztE0MSkdnTWClUCRub78=", "narHash": "sha256-iKzJcpdXih14qYVcZ9QC9XuZYnPc6T8YImb6dX166kw=",
"owner": "hercules-ci", "owner": "hercules-ci",
"repo": "flake-parts", "repo": "flake-parts",
"rev": "8dc45382d5206bd292f9c2768b8058a8fd8311d9", "rev": "2a55567fcf15b1b1c7ed712a2c6fadaec7412ea8",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -75,11 +75,11 @@
}, },
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1716619601, "lastModified": 1717774105,
"narHash": "sha256-9dUxZf8MOqJH3vjbhrz7LH4qTcnRsPSBU1Q50T7q/X8=", "narHash": "sha256-HV97wqUQv9wvptiHCb3Y0/YH0lJ60uZ8FYfEOIzYEqI=",
"owner": "nixos", "owner": "nixos",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "47e03a624662ce399e55c45a5f6da698fc72c797", "rev": "d226935fd75012939397c83f6c385e4d6d832288",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -103,11 +103,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1716213921, "lastModified": 1717664902,
"narHash": "sha256-xrsYFST8ij4QWaV6HEokCUNIZLjjLP1bYC60K8XiBVA=", "narHash": "sha256-7XfBuLULizXjXfBYy/VV+SpYMHreNRHk9nKMsm1bgb4=",
"owner": "cachix", "owner": "cachix",
"repo": "pre-commit-hooks.nix", "repo": "pre-commit-hooks.nix",
"rev": "0e8fcc54b842ad8428c9e705cb5994eaf05c26a0", "rev": "cc4d466cb1254af050ff7bdf47f6d404a7c646d1",
"type": "github" "type": "github"
}, },
"original": { "original": {

View File

@ -395,20 +395,15 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv)
{ {
static const QString baseLogFile = BuildConfig.LAUNCHER_NAME + "-%0.log"; static const QString baseLogFile = BuildConfig.LAUNCHER_NAME + "-%0.log";
static const QString logBase = FS::PathCombine("logs", baseLogFile); static const QString logBase = FS::PathCombine("logs", baseLogFile);
auto moveFile = [](const QString& oldName, const QString& newName) {
QFile::remove(newName);
QFile::copy(oldName, newName);
QFile::remove(oldName);
};
if (FS::ensureFolderPathExists("logs")) { // if this did not fail if (FS::ensureFolderPathExists("logs")) { // if this did not fail
for (auto i = 0; i <= 4; i++) for (auto i = 0; i <= 4; i++)
if (auto oldName = baseLogFile.arg(i); if (auto oldName = baseLogFile.arg(i);
QFile::exists(oldName)) // do not pointlessly delete new files if the old ones are not there QFile::exists(oldName)) // do not pointlessly delete new files if the old ones are not there
moveFile(oldName, logBase.arg(i)); FS::move(oldName, logBase.arg(i));
} }
for (auto i = 4; i > 0; i--) for (auto i = 4; i > 0; i--)
moveFile(logBase.arg(i - 1), logBase.arg(i)); FS::move(logBase.arg(i - 1), logBase.arg(i));
logFile = std::unique_ptr<QFile>(new QFile(logBase.arg(0))); logFile = std::unique_ptr<QFile>(new QFile(logBase.arg(0)));
if (!logFile->open(QIODevice::WriteOnly | QIODevice::Text | QIODevice::Truncate)) { if (!logFile->open(QIODevice::WriteOnly | QIODevice::Text | QIODevice::Truncate)) {
@ -1212,6 +1207,12 @@ void Application::performMainStartupAction()
qDebug() << "<> Updater started."; qDebug() << "<> Updater started.";
} }
{ // delete instances tmp dirctory
auto instDir = m_settings->get("InstanceDir").toString();
const QString tempRoot = FS::PathCombine(instDir, ".tmp");
FS::deletePath(tempRoot);
}
if (!m_urlsToImport.isEmpty()) { if (!m_urlsToImport.isEmpty()) {
qDebug() << "<> Importing from url:" << m_urlsToImport; qDebug() << "<> Importing from url:" << m_urlsToImport;
m_mainWindow->processURLs(m_urlsToImport); m_mainWindow->processURLs(m_urlsToImport);

View File

@ -16,6 +16,7 @@
#include <QFile> #include <QFile>
#include "BaseInstaller.h" #include "BaseInstaller.h"
#include "FileSystem.h"
#include "minecraft/MinecraftInstance.h" #include "minecraft/MinecraftInstance.h"
BaseInstaller::BaseInstaller() {} BaseInstaller::BaseInstaller() {}
@ -42,7 +43,7 @@ bool BaseInstaller::add(MinecraftInstance* to)
bool BaseInstaller::remove(MinecraftInstance* from) bool BaseInstaller::remove(MinecraftInstance* from)
{ {
return QFile::remove(filename(from->instanceRoot())); return FS::deletePath(filename(from->instanceRoot()));
} }
QString BaseInstaller::filename(const QString& root) const QString BaseInstaller::filename(const QString& root) const

View File

@ -647,6 +647,19 @@ void ExternalLinkFileProcess::runLinkFile()
qDebug() << "Process exited"; qDebug() << "Process exited";
} }
bool moveByCopy(const QString& source, const QString& dest)
{
if (!copy(source, dest)()) { // copy
qDebug() << "Copy of" << source << "to" << dest << "failed!";
return false;
}
if (!deletePath(source)) { // remove original
qDebug() << "Deletion of" << source << "failed!";
return false;
};
return true;
}
bool move(const QString& source, const QString& dest) bool move(const QString& source, const QString& dest)
{ {
std::error_code err; std::error_code err;
@ -654,13 +667,14 @@ bool move(const QString& source, const QString& dest)
ensureFilePathExists(dest); ensureFilePathExists(dest);
fs::rename(StringUtils::toStdString(source), StringUtils::toStdString(dest), err); fs::rename(StringUtils::toStdString(source), StringUtils::toStdString(dest), err);
if (err) { if (err.value() != 0) {
qWarning() << "Failed to move file:" << QString::fromStdString(err.message()); if (moveByCopy(source, dest))
qDebug() << "Source file:" << source; return true;
qDebug() << "Destination file:" << dest; qDebug() << "Move of" << source << "to" << dest << "failed!";
qWarning() << "Failed to move file:" << QString::fromStdString(err.message()) << QString::number(err.value());
return false;
} }
return true;
return err.value() == 0;
} }
bool deletePath(QString path) bool deletePath(QString path)

View File

@ -240,6 +240,7 @@ class create_link : public QObject {
bool operator()(bool dryRun = false) { return operator()(QString(), dryRun); } bool operator()(bool dryRun = false) { return operator()(QString(), dryRun); }
int totalLinked() { return m_linked; } int totalLinked() { return m_linked; }
int totalToLink() { return static_cast<int>(m_links_to_make.size()); }
void runPrivileged() { runPrivileged(QString()); } void runPrivileged() { runPrivileged(QString()); }
void runPrivileged(const QString& offset); void runPrivileged(const QString& offset);
@ -378,6 +379,7 @@ enum class FilesystemType {
HFSX, HFSX,
FUSEBLK, FUSEBLK,
F2FS, F2FS,
BCACHEFS,
UNKNOWN UNKNOWN
}; };
@ -406,6 +408,7 @@ static const QMap<FilesystemType, QStringList> s_filesystem_type_names = { { Fil
{ FilesystemType::HFSX, { "HFSX" } }, { FilesystemType::HFSX, { "HFSX" } },
{ FilesystemType::FUSEBLK, { "FUSEBLK" } }, { FilesystemType::FUSEBLK, { "FUSEBLK" } },
{ FilesystemType::F2FS, { "F2FS" } }, { FilesystemType::F2FS, { "F2FS" } },
{ FilesystemType::BCACHEFS, { "BCACHEFS" } },
{ FilesystemType::UNKNOWN, { "UNKNOWN" } } }; { FilesystemType::UNKNOWN, { "UNKNOWN" } } };
/** /**
@ -458,7 +461,7 @@ QString nearestExistentAncestor(const QString& path);
FilesystemInfo statFS(const QString& path); FilesystemInfo statFS(const QString& path);
static const QList<FilesystemType> s_clone_filesystems = { FilesystemType::BTRFS, FilesystemType::APFS, FilesystemType::ZFS, static const QList<FilesystemType> s_clone_filesystems = { FilesystemType::BTRFS, FilesystemType::APFS, FilesystemType::ZFS,
FilesystemType::XFS, FilesystemType::REFS }; FilesystemType::XFS, FilesystemType::REFS, FilesystemType::BCACHEFS };
/** /**
* @brief if the Filesystem is reflink/clone capable * @brief if the Filesystem is reflink/clone capable

View File

@ -1,10 +1,12 @@
#include "InstanceCopyTask.h" #include "InstanceCopyTask.h"
#include <QDebug> #include <QDebug>
#include <QtConcurrentRun> #include <QtConcurrentRun>
#include <memory>
#include "FileSystem.h" #include "FileSystem.h"
#include "NullInstance.h" #include "NullInstance.h"
#include "pathmatcher/RegexpMatcher.h" #include "pathmatcher/RegexpMatcher.h"
#include "settings/INISettingsObject.h" #include "settings/INISettingsObject.h"
#include "tasks/Task.h"
InstanceCopyTask::InstanceCopyTask(InstancePtr origInstance, const InstanceCopyPrefs& prefs) InstanceCopyTask::InstanceCopyTask(InstancePtr origInstance, const InstanceCopyPrefs& prefs)
{ {
@ -38,38 +40,50 @@ void InstanceCopyTask::executeTask()
{ {
setStatus(tr("Copying instance %1").arg(m_origInstance->name())); setStatus(tr("Copying instance %1").arg(m_origInstance->name()));
auto copySaves = [&]() { m_copyFuture = QtConcurrent::run(QThreadPool::globalInstance(), [this] {
QFileInfo mcDir(FS::PathCombine(m_stagingPath, "minecraft"));
QFileInfo dotMCDir(FS::PathCombine(m_stagingPath, ".minecraft"));
QString staging_mc_dir;
if (dotMCDir.exists() && !mcDir.exists())
staging_mc_dir = dotMCDir.filePath();
else
staging_mc_dir = mcDir.filePath();
FS::copy savesCopy(FS::PathCombine(m_origInstance->gameRoot(), "saves"), FS::PathCombine(staging_mc_dir, "saves"));
savesCopy.followSymlinks(true);
return savesCopy();
};
m_copyFuture = QtConcurrent::run(QThreadPool::globalInstance(), [this, copySaves] {
if (m_useClone) { if (m_useClone) {
FS::clone folderClone(m_origInstance->instanceRoot(), m_stagingPath); FS::clone folderClone(m_origInstance->instanceRoot(), m_stagingPath);
folderClone.matcher(m_matcher.get()); folderClone.matcher(m_matcher.get());
folderClone(true);
setProgress(0, folderClone.totalCloned());
connect(&folderClone, &FS::clone::fileCloned,
[this](QString src, QString dst) { setProgress(m_progress + 1, m_progressTotal); });
return folderClone(); return folderClone();
} else if (m_useLinks || m_useHardLinks) { }
if (m_useLinks || m_useHardLinks) {
std::unique_ptr<FS::copy> savesCopy;
if (m_copySaves) {
QFileInfo mcDir(FS::PathCombine(m_stagingPath, "minecraft"));
QFileInfo dotMCDir(FS::PathCombine(m_stagingPath, ".minecraft"));
QString staging_mc_dir;
if (dotMCDir.exists() && !mcDir.exists())
staging_mc_dir = dotMCDir.filePath();
else
staging_mc_dir = mcDir.filePath();
savesCopy = std::make_unique<FS::copy>(FS::PathCombine(m_origInstance->gameRoot(), "saves"),
FS::PathCombine(staging_mc_dir, "saves"));
savesCopy->followSymlinks(true);
(*savesCopy)(true);
setProgress(0, savesCopy->totalCopied());
connect(savesCopy.get(), &FS::copy::fileCopied, [this](QString src) { setProgress(m_progress + 1, m_progressTotal); });
}
FS::create_link folderLink(m_origInstance->instanceRoot(), m_stagingPath); FS::create_link folderLink(m_origInstance->instanceRoot(), m_stagingPath);
int depth = m_linkRecursively ? -1 : 0; // we need to at least link the top level instead of the instance folder int depth = m_linkRecursively ? -1 : 0; // we need to at least link the top level instead of the instance folder
folderLink.linkRecursively(true).setMaxDepth(depth).useHardLinks(m_useHardLinks).matcher(m_matcher.get()); folderLink.linkRecursively(true).setMaxDepth(depth).useHardLinks(m_useHardLinks).matcher(m_matcher.get());
folderLink(true);
setProgress(0, m_progressTotal + folderLink.totalToLink());
connect(&folderLink, &FS::create_link::fileLinked,
[this](QString src, QString dst) { setProgress(m_progress + 1, m_progressTotal); });
bool there_were_errors = false; bool there_were_errors = false;
if (!folderLink()) { if (!folderLink()) {
#if defined Q_OS_WIN32 #if defined Q_OS_WIN32
if (!m_useHardLinks) { if (!m_useHardLinks) {
setProgress(0, m_progressTotal);
qDebug() << "EXPECTED: Link failure, Windows requires permissions for symlinks"; qDebug() << "EXPECTED: Link failure, Windows requires permissions for symlinks";
qDebug() << "attempting to run with privelage"; qDebug() << "attempting to run with privelage";
@ -94,13 +108,11 @@ void InstanceCopyTask::executeTask()
} }
} }
if (m_copySaves) { if (savesCopy) {
there_were_errors |= !copySaves(); there_were_errors |= !(*savesCopy)();
} }
return got_priv_results && !there_were_errors; return got_priv_results && !there_were_errors;
} else {
qDebug() << "Link Failed!" << folderLink.getOSError().value() << folderLink.getOSError().message().c_str();
} }
#else #else
qDebug() << "Link Failed!" << folderLink.getOSError().value() << folderLink.getOSError().message().c_str(); qDebug() << "Link Failed!" << folderLink.getOSError().value() << folderLink.getOSError().message().c_str();
@ -108,17 +120,19 @@ void InstanceCopyTask::executeTask()
return false; return false;
} }
if (m_copySaves) { if (savesCopy) {
there_were_errors |= !copySaves(); there_were_errors |= !(*savesCopy)();
} }
return !there_were_errors; return !there_were_errors;
} else {
FS::copy folderCopy(m_origInstance->instanceRoot(), m_stagingPath);
folderCopy.followSymlinks(false).matcher(m_matcher.get());
return folderCopy();
} }
FS::copy folderCopy(m_origInstance->instanceRoot(), m_stagingPath);
folderCopy.followSymlinks(false).matcher(m_matcher.get());
folderCopy(true);
setProgress(0, folderCopy.totalCopied());
connect(&folderCopy, &FS::copy::fileCopied, [this](QString src) { setProgress(m_progress + 1, m_progressTotal); });
return folderCopy();
}); });
connect(&m_copyFutureWatcher, &QFutureWatcher<bool>::finished, this, &InstanceCopyTask::copyFinished); connect(&m_copyFutureWatcher, &QFutureWatcher<bool>::finished, this, &InstanceCopyTask::copyFinished);
connect(&m_copyFutureWatcher, &QFutureWatcher<bool>::canceled, this, &InstanceCopyTask::copyAborted); connect(&m_copyFutureWatcher, &QFutureWatcher<bool>::canceled, this, &InstanceCopyTask::copyAborted);
@ -170,3 +184,14 @@ void InstanceCopyTask::copyAborted()
emitFailed(tr("Instance folder copy has been aborted.")); emitFailed(tr("Instance folder copy has been aborted."));
return; return;
} }
bool InstanceCopyTask::abort()
{
if (m_copyFutureWatcher.isRunning()) {
m_copyFutureWatcher.cancel();
// NOTE: Here we don't do `emitAborted()` because it will be done when `m_copyFutureWatcher` actually cancels, which may not occur
// immediately.
return true;
}
return false;
}

View File

@ -19,6 +19,7 @@ class InstanceCopyTask : public InstanceTask {
protected: protected:
//! Entry point for tasks. //! Entry point for tasks.
virtual void executeTask() override; virtual void executeTask() override;
bool abort() override;
void copyFinished(); void copyFinished();
void copyAborted(); void copyAborted();

View File

@ -2,6 +2,7 @@
#include <QDebug> #include <QDebug>
#include <QFile> #include <QFile>
#include "FileSystem.h"
void InstanceCreationTask::executeTask() void InstanceCreationTask::executeTask()
{ {
@ -45,7 +46,7 @@ void InstanceCreationTask::executeTask()
if (!QFile::exists(path)) if (!QFile::exists(path))
continue; continue;
qDebug() << "Removing" << path; qDebug() << "Removing" << path;
if (!QFile::remove(path)) { if (!FS::deletePath(path)) {
qCritical() << "Couldn't remove the old conflicting files."; qCritical() << "Couldn't remove the old conflicting files.";
emitFailed(tr("Failed to remove old conflicting files.")); emitFailed(tr("Failed to remove old conflicting files."));
return; return;

View File

@ -56,6 +56,7 @@
#include <QtConcurrentRun> #include <QtConcurrentRun>
#include <algorithm> #include <algorithm>
#include <memory>
#include <quazip/quazipdir.h> #include <quazip/quazipdir.h>
@ -68,15 +69,8 @@ bool InstanceImportTask::abort()
if (!canAbort()) if (!canAbort())
return false; return false;
if (m_filesNetJob) if (task)
m_filesNetJob->abort(); task->abort();
if (m_extractFuture.isRunning()) {
// NOTE: The tasks created by QtConcurrent::run() can't actually get cancelled,
// but we can use this call to check the state when the extraction finishes.
m_extractFuture.cancel();
m_extractFuture.waitForFinished();
}
return Task::abort(); return Task::abort();
} }
@ -89,7 +83,6 @@ void InstanceImportTask::executeTask()
processZipPack(); processZipPack();
} else { } else {
setStatus(tr("Downloading modpack:\n%1").arg(m_sourceUrl.toString())); setStatus(tr("Downloading modpack:\n%1").arg(m_sourceUrl.toString()));
m_downloadRequired = true;
downloadFromUrl(); downloadFromUrl();
} }
@ -97,115 +90,132 @@ void InstanceImportTask::executeTask()
void InstanceImportTask::downloadFromUrl() void InstanceImportTask::downloadFromUrl()
{ {
const QString path = m_sourceUrl.host() + '/' + m_sourceUrl.path(); const QString path(m_sourceUrl.host() + '/' + m_sourceUrl.path());
auto entry = APPLICATION->metacache()->resolveEntry("general", path); auto entry = APPLICATION->metacache()->resolveEntry("general", path);
entry->setStale(true); entry->setStale(true);
m_filesNetJob.reset(new NetJob(tr("Modpack download"), APPLICATION->network()));
m_filesNetJob->addNetAction(Net::ApiDownload::makeCached(m_sourceUrl, entry));
m_archivePath = entry->getFullPath(); m_archivePath = entry->getFullPath();
connect(m_filesNetJob.get(), &NetJob::succeeded, this, &InstanceImportTask::downloadSucceeded); auto filesNetJob = makeShared<NetJob>(tr("Modpack download"), APPLICATION->network());
connect(m_filesNetJob.get(), &NetJob::progress, this, &InstanceImportTask::downloadProgressChanged); filesNetJob->addNetAction(Net::ApiDownload::makeCached(m_sourceUrl, entry));
connect(m_filesNetJob.get(), &NetJob::stepProgress, this, &InstanceImportTask::propagateStepProgress);
connect(m_filesNetJob.get(), &NetJob::failed, this, &InstanceImportTask::downloadFailed); connect(filesNetJob.get(), &NetJob::succeeded, this, &InstanceImportTask::processZipPack);
connect(m_filesNetJob.get(), &NetJob::aborted, this, &InstanceImportTask::downloadAborted); connect(filesNetJob.get(), &NetJob::progress, this, &InstanceImportTask::setProgress);
m_filesNetJob->start(); connect(filesNetJob.get(), &NetJob::stepProgress, this, &InstanceImportTask::propagateStepProgress);
connect(filesNetJob.get(), &NetJob::failed, this, &InstanceImportTask::emitFailed);
connect(filesNetJob.get(), &NetJob::aborted, this, &InstanceImportTask::emitAborted);
task.reset(filesNetJob);
filesNetJob->start();
} }
void InstanceImportTask::downloadSucceeded() QString InstanceImportTask::getRootFromZip(QuaZip* zip, const QString& root)
{ {
processZipPack(); if (!isRunning()) {
m_filesNetJob.reset(); return {};
} }
QuaZipDir rootDir(zip, root);
for (auto&& fileName : rootDir.entryList(QDir::Files)) {
setDetails(fileName);
if (fileName == "instance.cfg") {
qDebug() << "MultiMC:" << true;
m_modpackType = ModpackType::MultiMC;
return root;
}
if (fileName == "manifest.json") {
qDebug() << "Flame:" << true;
m_modpackType = ModpackType::Flame;
return root;
}
void InstanceImportTask::downloadFailed(QString reason) QCoreApplication::processEvents();
{ }
emitFailed(reason);
m_filesNetJob.reset();
}
void InstanceImportTask::downloadProgressChanged(qint64 current, qint64 total) // Recurse the search to non-ignored subfolders
{ for (auto&& fileName : rootDir.entryList(QDir::Dirs)) {
setProgress(current, total); if ("overrides/" == fileName)
} continue;
void InstanceImportTask::downloadAborted() QString result = getRootFromZip(zip, root + fileName);
{ if (!result.isEmpty())
emitAborted(); return result;
m_filesNetJob.reset(); }
return {};
} }
void InstanceImportTask::processZipPack() void InstanceImportTask::processZipPack()
{ {
setStatus(tr("Extracting modpack")); setStatus(tr("Attempting to determine instance type"));
QDir extractDir(m_stagingPath); QDir extractDir(m_stagingPath);
qDebug() << "Attempting to create instance from" << m_archivePath; qDebug() << "Attempting to create instance from" << m_archivePath;
// open the zip and find relevant files in it // open the zip and find relevant files in it
m_packZip.reset(new QuaZip(m_archivePath)); auto packZip = std::make_shared<QuaZip>(m_archivePath);
if (!m_packZip->open(QuaZip::mdUnzip)) { if (!packZip->open(QuaZip::mdUnzip)) {
emitFailed(tr("Unable to open supplied modpack zip file.")); emitFailed(tr("Unable to open supplied modpack zip file."));
return; return;
} }
QuaZipDir packZipDir(m_packZip.get()); QuaZipDir packZipDir(packZip.get());
qDebug() << "Attempting to determine instance type";
// https://docs.modrinth.com/docs/modpacks/format_definition/#storage
bool modrinthFound = packZipDir.exists("/modrinth.index.json");
bool technicFound = packZipDir.exists("/bin/modpack.jar") || packZipDir.exists("/bin/version.json");
QString root; QString root;
// NOTE: Prioritize modpack platforms that aren't searched for recursively. // NOTE: Prioritize modpack platforms that aren't searched for recursively.
// Especially Flame has a very common filename for its manifest, which may appear inside overrides for example // Especially Flame has a very common filename for its manifest, which may appear inside overrides for example
if (modrinthFound) { // https://docs.modrinth.com/docs/modpacks/format_definition/#storage
if (packZipDir.exists("/modrinth.index.json")) {
// process as Modrinth pack // process as Modrinth pack
qDebug() << "Modrinth:" << modrinthFound; qDebug() << "Modrinth:" << true;
m_modpackType = ModpackType::Modrinth; m_modpackType = ModpackType::Modrinth;
} else if (technicFound) { } else if (packZipDir.exists("/bin/modpack.jar") || packZipDir.exists("/bin/version.json")) {
// process as Technic pack // process as Technic pack
qDebug() << "Technic:" << technicFound; qDebug() << "Technic:" << true;
extractDir.mkpath("minecraft"); extractDir.mkpath("minecraft");
extractDir.cd("minecraft"); extractDir.cd("minecraft");
m_modpackType = ModpackType::Technic; m_modpackType = ModpackType::Technic;
} else { } else {
QStringList paths_to_ignore{ "overrides/" }; root = getRootFromZip(packZip.get());
setDetails("");
if (QString mmcRoot = MMCZip::findFolderOfFileInZip(m_packZip.get(), "instance.cfg", paths_to_ignore); !mmcRoot.isNull()) {
// process as MultiMC instance/pack
qDebug() << "MultiMC:" << mmcRoot;
root = mmcRoot;
m_modpackType = ModpackType::MultiMC;
} else if (QString flameRoot = MMCZip::findFolderOfFileInZip(m_packZip.get(), "manifest.json", paths_to_ignore);
!flameRoot.isNull()) {
// process as Flame pack
qDebug() << "Flame:" << flameRoot;
root = flameRoot;
m_modpackType = ModpackType::Flame;
}
} }
if (m_modpackType == ModpackType::Unknown) { if (m_modpackType == ModpackType::Unknown) {
emitFailed(tr("Archive does not contain a recognized modpack type.")); emitFailed(tr("Archive does not contain a recognized modpack type."));
return; return;
} }
setStatus(tr("Extracting modpack"));
// make sure we extract just the pack // make sure we extract just the pack
m_extractFuture = auto zipTask = makeShared<MMCZip::ExtractZipTask>(packZip, extractDir, root);
QtConcurrent::run(QThreadPool::globalInstance(), MMCZip::extractSubDir, m_packZip.get(), root, extractDir.absolutePath());
connect(&m_extractFutureWatcher, &QFutureWatcher<QStringList>::finished, this, &InstanceImportTask::extractFinished); auto progressStep = std::make_shared<TaskStepProgress>();
m_extractFutureWatcher.setFuture(m_extractFuture); connect(zipTask.get(), &Task::finished, this, [this, progressStep] {
progressStep->state = TaskStepState::Succeeded;
stepProgress(*progressStep);
});
connect(zipTask.get(), &Task::succeeded, this, &InstanceImportTask::extractFinished);
connect(zipTask.get(), &Task::aborted, this, &InstanceImportTask::emitAborted);
connect(zipTask.get(), &Task::failed, this, [this, progressStep](QString reason) {
progressStep->state = TaskStepState::Failed;
stepProgress(*progressStep);
emitFailed(reason);
});
connect(zipTask.get(), &Task::stepProgress, this, &InstanceImportTask::propagateStepProgress);
connect(zipTask.get(), &Task::progress, this, [this, progressStep](qint64 current, qint64 total) {
progressStep->update(current, total);
stepProgress(*progressStep);
});
connect(zipTask.get(), &Task::status, this, [this, progressStep](QString status) {
progressStep->status = status;
stepProgress(*progressStep);
});
task.reset(zipTask);
zipTask->start();
} }
void InstanceImportTask::extractFinished() void InstanceImportTask::extractFinished()
{ {
m_packZip.reset();
if (m_extractFuture.isCanceled())
return;
if (!m_extractFuture.result().has_value()) {
emitFailed(tr("Failed to extract modpack"));
return;
}
QDir extractDir(m_stagingPath); QDir extractDir(m_stagingPath);
qDebug() << "Fixing permissions for extracted pack files..."; qDebug() << "Fixing permissions for extracted pack files...";
@ -324,13 +334,15 @@ void InstanceImportTask::processMultiMC()
m_instIcon = instance.iconKey(); m_instIcon = instance.iconKey();
auto importIconPath = IconUtils::findBestIconIn(instance.instanceRoot(), m_instIcon); auto importIconPath = IconUtils::findBestIconIn(instance.instanceRoot(), m_instIcon);
if (importIconPath.isNull() || !QFile::exists(importIconPath))
importIconPath = IconUtils::findBestIconIn(instance.instanceRoot(), "icon.png");
if (!importIconPath.isNull() && QFile::exists(importIconPath)) { if (!importIconPath.isNull() && QFile::exists(importIconPath)) {
// import icon // import icon
auto iconList = APPLICATION->icons(); auto iconList = APPLICATION->icons();
if (iconList->iconFileExists(m_instIcon)) { if (iconList->iconFileExists(m_instIcon)) {
iconList->deleteIcon(m_instIcon); iconList->deleteIcon(m_instIcon);
} }
iconList->installIcons({ importIconPath }); iconList->installIcon(importIconPath, m_instIcon);
} }
} }
emitSucceeded(); emitSucceeded();

View File

@ -39,11 +39,8 @@
#include <QFutureWatcher> #include <QFutureWatcher>
#include <QUrl> #include <QUrl>
#include "InstanceTask.h" #include "InstanceTask.h"
#include "QObjectPtr.h"
#include "modplatform/flame/PackManifest.h"
#include "net/NetJob.h"
#include "settings/SettingsObject.h"
#include <memory>
#include <optional> #include <optional>
class QuaZip; class QuaZip;
@ -54,35 +51,26 @@ class InstanceImportTask : public InstanceTask {
explicit InstanceImportTask(const QUrl& sourceUrl, QWidget* parent = nullptr, QMap<QString, QString>&& extra_info = {}); explicit InstanceImportTask(const QUrl& sourceUrl, QWidget* parent = nullptr, QMap<QString, QString>&& extra_info = {});
bool abort() override; bool abort() override;
const QVector<Flame::File>& getBlockedFiles() const { return m_blockedMods; }
protected: protected:
//! Entry point for tasks. //! Entry point for tasks.
virtual void executeTask() override; virtual void executeTask() override;
private: private:
void processZipPack();
void processMultiMC(); void processMultiMC();
void processTechnic(); void processTechnic();
void processFlame(); void processFlame();
void processModrinth(); void processModrinth();
QString getRootFromZip(QuaZip* zip, const QString& root = "");
private slots: private slots:
void downloadSucceeded(); void processZipPack();
void downloadFailed(QString reason);
void downloadProgressChanged(qint64 current, qint64 total);
void downloadAborted();
void extractFinished(); void extractFinished();
private: /* data */ private: /* data */
NetJob::Ptr m_filesNetJob;
QUrl m_sourceUrl; QUrl m_sourceUrl;
QString m_archivePath; QString m_archivePath;
bool m_downloadRequired = false; Task::Ptr task;
std::unique_ptr<QuaZip> m_packZip;
QFuture<std::optional<QStringList>> m_extractFuture;
QFutureWatcher<std::optional<QStringList>> m_extractFutureWatcher;
QVector<Flame::File> m_blockedMods;
enum class ModpackType { enum class ModpackType {
Unknown, Unknown,
MultiMC, MultiMC,

View File

@ -972,7 +972,6 @@ bool InstanceList::commitStagedInstance(const QString& path,
if (groupName.isEmpty() && !groupName.isNull()) if (groupName.isEmpty() && !groupName.isNull())
groupName = QString(); groupName = QString();
QDir dir;
QString instID; QString instID;
InstancePtr inst; InstancePtr inst;
@ -996,7 +995,7 @@ bool InstanceList::commitStagedInstance(const QString& path,
return false; return false;
} }
} else { } else {
if (!dir.rename(path, destination)) { if (!FS::move(path, destination)) {
qWarning() << "Failed to move" << path << "to" << destination; qWarning() << "Failed to move" << path << "to" << destination;
return false; return false;
} }

View File

@ -315,7 +315,7 @@ void LaunchController::launchInstance()
online_mode = "online"; online_mode = "online";
// Prepend Server Status // Prepend Server Status
QStringList servers = { "authserver.mojang.com", "session.minecraft.net", "textures.minecraft.net", "api.mojang.com" }; QStringList servers = { "login.microsoftonline.com", "session.minecraft.net", "textures.minecraft.net", "api.mojang.com" };
QString resolved_servers = ""; QString resolved_servers = "";
QHostInfo host_info; QHostInfo host_info;

View File

@ -42,6 +42,7 @@
#include <QCoreApplication> #include <QCoreApplication>
#include <QDebug> #include <QDebug>
#include <QFileInfo>
#include <QUrl> #include <QUrl>
#if defined(LAUNCHER_APPLICATION) #if defined(LAUNCHER_APPLICATION)
@ -122,7 +123,7 @@ bool compressDirFiles(QString fileCompressed, QString dir, QFileInfoList files,
zip.setUtf8Enabled(true); zip.setUtf8Enabled(true);
QDir().mkpath(QFileInfo(fileCompressed).absolutePath()); QDir().mkpath(QFileInfo(fileCompressed).absolutePath());
if (!zip.open(QuaZip::mdCreate)) { if (!zip.open(QuaZip::mdCreate)) {
QFile::remove(fileCompressed); FS::deletePath(fileCompressed);
return false; return false;
} }
@ -130,7 +131,7 @@ bool compressDirFiles(QString fileCompressed, QString dir, QFileInfoList files,
zip.close(); zip.close();
if (zip.getZipError() != 0) { if (zip.getZipError() != 0) {
QFile::remove(fileCompressed); FS::deletePath(fileCompressed);
return false; return false;
} }
@ -144,7 +145,7 @@ bool createModdedJar(QString sourceJarPath, QString targetJarPath, const QList<M
QuaZip zipOut(targetJarPath); QuaZip zipOut(targetJarPath);
zipOut.setUtf8Enabled(true); zipOut.setUtf8Enabled(true);
if (!zipOut.open(QuaZip::mdCreate)) { if (!zipOut.open(QuaZip::mdCreate)) {
QFile::remove(targetJarPath); FS::deletePath(targetJarPath);
qCritical() << "Failed to open the minecraft.jar for modding"; qCritical() << "Failed to open the minecraft.jar for modding";
return false; return false;
} }
@ -162,7 +163,7 @@ bool createModdedJar(QString sourceJarPath, QString targetJarPath, const QList<M
if (mod->type() == ResourceType::ZIPFILE) { if (mod->type() == ResourceType::ZIPFILE) {
if (!mergeZipFiles(&zipOut, mod->fileinfo(), addedFiles)) { if (!mergeZipFiles(&zipOut, mod->fileinfo(), addedFiles)) {
zipOut.close(); zipOut.close();
QFile::remove(targetJarPath); FS::deletePath(targetJarPath);
qCritical() << "Failed to add" << mod->fileinfo().fileName() << "to the jar."; qCritical() << "Failed to add" << mod->fileinfo().fileName() << "to the jar.";
return false; return false;
} }
@ -171,7 +172,7 @@ bool createModdedJar(QString sourceJarPath, QString targetJarPath, const QList<M
auto filename = mod->fileinfo(); auto filename = mod->fileinfo();
if (!JlCompress::compressFile(&zipOut, filename.absoluteFilePath(), filename.fileName())) { if (!JlCompress::compressFile(&zipOut, filename.absoluteFilePath(), filename.fileName())) {
zipOut.close(); zipOut.close();
QFile::remove(targetJarPath); FS::deletePath(targetJarPath);
qCritical() << "Failed to add" << mod->fileinfo().fileName() << "to the jar."; qCritical() << "Failed to add" << mod->fileinfo().fileName() << "to the jar.";
return false; return false;
} }
@ -194,7 +195,7 @@ bool createModdedJar(QString sourceJarPath, QString targetJarPath, const QList<M
if (!compressDirFiles(&zipOut, parent_dir, files)) { if (!compressDirFiles(&zipOut, parent_dir, files)) {
zipOut.close(); zipOut.close();
QFile::remove(targetJarPath); FS::deletePath(targetJarPath);
qCritical() << "Failed to add" << mod->fileinfo().fileName() << "to the jar."; qCritical() << "Failed to add" << mod->fileinfo().fileName() << "to the jar.";
return false; return false;
} }
@ -202,7 +203,7 @@ bool createModdedJar(QString sourceJarPath, QString targetJarPath, const QList<M
} else { } else {
// Make sure we do not continue launching when something is missing or undefined... // Make sure we do not continue launching when something is missing or undefined...
zipOut.close(); zipOut.close();
QFile::remove(targetJarPath); FS::deletePath(targetJarPath);
qCritical() << "Failed to add unknown mod type" << mod->fileinfo().fileName() << "to the jar."; qCritical() << "Failed to add unknown mod type" << mod->fileinfo().fileName() << "to the jar.";
return false; return false;
} }
@ -210,7 +211,7 @@ bool createModdedJar(QString sourceJarPath, QString targetJarPath, const QList<M
if (!mergeZipFiles(&zipOut, QFileInfo(sourceJarPath), addedFiles, [](const QString key) { return !key.contains("META-INF"); })) { if (!mergeZipFiles(&zipOut, QFileInfo(sourceJarPath), addedFiles, [](const QString key) { return !key.contains("META-INF"); })) {
zipOut.close(); zipOut.close();
QFile::remove(targetJarPath); FS::deletePath(targetJarPath);
qCritical() << "Failed to insert minecraft.jar contents."; qCritical() << "Failed to insert minecraft.jar contents.";
return false; return false;
} }
@ -218,7 +219,7 @@ bool createModdedJar(QString sourceJarPath, QString targetJarPath, const QList<M
// Recompress the jar // Recompress the jar
zipOut.close(); zipOut.close();
if (zipOut.getZipError() != 0) { if (zipOut.getZipError() != 0) {
QFile::remove(targetJarPath); FS::deletePath(targetJarPath);
qCritical() << "Failed to finalize minecraft.jar!"; qCritical() << "Failed to finalize minecraft.jar!";
return false; return false;
} }
@ -332,9 +333,20 @@ std::optional<QStringList> extractSubDir(QuaZip* zip, const QString& subdir, con
} }
extracted.append(target_file_path); extracted.append(target_file_path);
QFile::setPermissions(target_file_path, auto fileInfo = QFileInfo(target_file_path);
QFileDevice::Permission::ReadUser | QFileDevice::Permission::WriteUser | QFileDevice::Permission::ExeUser); if (fileInfo.isFile()) {
auto permissions = fileInfo.permissions();
auto maxPermisions = QFileDevice::Permission::ReadUser | QFileDevice::Permission::WriteUser | QFileDevice::Permission::ExeUser |
QFileDevice::Permission::ReadGroup | QFileDevice::Permission::ReadOther;
auto minPermisions = QFileDevice::Permission::ReadUser | QFileDevice::Permission::WriteUser;
auto newPermisions = (permissions & maxPermisions) | minPermisions;
if (newPermisions != permissions) {
if (!QFile::setPermissions(target_file_path, newPermisions)) {
qWarning() << (QObject::tr("Could not fix permissions for %1").arg(target_file_path));
}
}
}
qDebug() << "Extracted file" << relative_file_name << "to" << target_file_path; qDebug() << "Extracted file" << relative_file_name << "to" << target_file_path;
} while (zip->goToNextFile()); } while (zip->goToNextFile());
@ -492,10 +504,10 @@ auto ExportToZipTask::exportZip() -> ZipResult
void ExportToZipTask::finish() void ExportToZipTask::finish()
{ {
if (m_build_zip_future.isCanceled()) { if (m_build_zip_future.isCanceled()) {
QFile::remove(m_output_path); FS::deletePath(m_output_path);
emitAborted(); emitAborted();
} else if (auto result = m_build_zip_future.result(); result.has_value()) { } else if (auto result = m_build_zip_future.result(); result.has_value()) {
QFile::remove(m_output_path); FS::deletePath(m_output_path);
emitFailed(result.value()); emitFailed(result.value());
} else { } else {
emitSucceeded(); emitSucceeded();
@ -512,6 +524,123 @@ bool ExportToZipTask::abort()
} }
return false; return false;
} }
#endif
void ExtractZipTask::executeTask()
{
m_zip_future = QtConcurrent::run(QThreadPool::globalInstance(), [this]() { return extractZip(); });
connect(&m_zip_watcher, &QFutureWatcher<ZipResult>::finished, this, &ExtractZipTask::finish);
m_zip_watcher.setFuture(m_zip_future);
}
auto ExtractZipTask::extractZip() -> ZipResult
{
auto target = m_output_dir.absolutePath();
auto target_top_dir = QUrl::fromLocalFile(target);
QStringList extracted;
qDebug() << "Extracting subdir" << m_subdirectory << "from" << m_input->getZipName() << "to" << target;
auto numEntries = m_input->getEntriesCount();
if (numEntries < 0) {
return ZipResult(tr("Failed to enumerate files in archive"));
}
if (numEntries == 0) {
logWarning(tr("Extracting empty archives seems odd..."));
return ZipResult();
}
if (!m_input->goToFirstFile()) {
return ZipResult(tr("Failed to seek to first file in zip"));
}
setStatus("Extracting files...");
setProgress(0, numEntries);
do {
if (m_zip_future.isCanceled())
return ZipResult();
setProgress(m_progress + 1, m_progressTotal);
QString file_name = m_input->getCurrentFileName();
if (!file_name.startsWith(m_subdirectory))
continue;
auto relative_file_name = QDir::fromNativeSeparators(file_name.remove(0, m_subdirectory.size()));
auto original_name = relative_file_name;
setStatus("Unziping: " + relative_file_name);
// Fix subdirs/files ending with a / getting transformed into absolute paths
if (relative_file_name.startsWith('/'))
relative_file_name = relative_file_name.mid(1);
// Fix weird "folders with a single file get squashed" thing
QString sub_path;
if (relative_file_name.contains('/') && !relative_file_name.endsWith('/')) {
sub_path = relative_file_name.section('/', 0, -2) + '/';
FS::ensureFolderPathExists(FS::PathCombine(target, sub_path));
relative_file_name = relative_file_name.split('/').last();
}
QString target_file_path;
if (relative_file_name.isEmpty()) {
target_file_path = target + '/';
} else {
target_file_path = FS::PathCombine(target_top_dir.toLocalFile(), sub_path, relative_file_name);
if (relative_file_name.endsWith('/') && !target_file_path.endsWith('/'))
target_file_path += '/';
}
if (!target_top_dir.isParentOf(QUrl::fromLocalFile(target_file_path))) {
return ZipResult(tr("Extracting %1 was cancelled, because it was effectively outside of the target path %2")
.arg(relative_file_name, target));
}
if (!JlCompress::extractFile(m_input.get(), "", target_file_path)) {
JlCompress::removeFile(extracted);
return ZipResult(tr("Failed to extract file %1 to %2").arg(original_name, target_file_path));
}
extracted.append(target_file_path);
auto fileInfo = QFileInfo(target_file_path);
if (fileInfo.isFile()) {
auto permissions = fileInfo.permissions();
auto maxPermisions = QFileDevice::Permission::ReadUser | QFileDevice::Permission::WriteUser | QFileDevice::Permission::ExeUser |
QFileDevice::Permission::ReadGroup | QFileDevice::Permission::ReadOther;
auto minPermisions = QFileDevice::Permission::ReadUser | QFileDevice::Permission::WriteUser;
auto newPermisions = (permissions & maxPermisions) | minPermisions;
if (newPermisions != permissions) {
if (!QFile::setPermissions(target_file_path, newPermisions)) {
logWarning(tr("Could not fix permissions for %1").arg(target_file_path));
}
}
}
qDebug() << "Extracted file" << relative_file_name << "to" << target_file_path;
} while (m_input->goToNextFile());
return ZipResult();
}
void ExtractZipTask::finish()
{
if (m_zip_future.isCanceled()) {
emitAborted();
} else if (auto result = m_zip_future.result(); result.has_value()) {
emitFailed(result.value());
} else {
emitSucceeded();
}
}
bool ExtractZipTask::abort()
{
if (m_zip_future.isRunning()) {
m_zip_future.cancel();
// NOTE: Here we don't do `emitAborted()` because it will be done when `m_build_zip_future` actually cancels, which may not occur
// immediately.
return true;
}
return false;
}
#endif
} // namespace MMCZip } // namespace MMCZip

View File

@ -205,5 +205,30 @@ class ExportToZipTask : public Task {
QFuture<ZipResult> m_build_zip_future; QFuture<ZipResult> m_build_zip_future;
QFutureWatcher<ZipResult> m_build_zip_watcher; QFutureWatcher<ZipResult> m_build_zip_watcher;
}; };
class ExtractZipTask : public Task {
public:
ExtractZipTask(std::shared_ptr<QuaZip> input, QDir outputDir, QString subdirectory = "")
: m_input(input), m_output_dir(outputDir), m_subdirectory(subdirectory)
{}
virtual ~ExtractZipTask() = default;
typedef std::optional<QString> ZipResult;
protected:
virtual void executeTask() override;
bool abort() override;
ZipResult extractZip();
void finish();
private:
std::shared_ptr<QuaZip> m_input;
QDir m_output_dir;
QString m_subdirectory;
QFuture<ZipResult> m_zip_future;
QFutureWatcher<ZipResult> m_zip_watcher;
};
#endif #endif
} // namespace MMCZip } // namespace MMCZip

View File

@ -212,3 +212,25 @@ QPair<QString, QString> StringUtils::splitFirst(const QString& s, const QRegular
right = s.mid(end); right = s.mid(end);
return qMakePair(left, right); return qMakePair(left, right);
} }
static const QRegularExpression ulMatcher("<\\s*/\\s*ul\\s*>");
QString StringUtils::htmlListPatch(QString htmlStr)
{
int pos = htmlStr.indexOf(ulMatcher);
int imgPos;
while (pos != -1) {
pos = htmlStr.indexOf(">", pos) + 1; // Get the size of the </ul> tag. Add one for zeroeth index
imgPos = htmlStr.indexOf("<img ", pos);
if (imgPos == -1)
break; // no image after the tag
auto textBetween = htmlStr.mid(pos, imgPos - pos).trimmed(); // trim all white spaces
if (textBetween.isEmpty())
htmlStr.insert(pos, "<br>");
pos = htmlStr.indexOf(ulMatcher, pos);
}
return htmlStr;
}

View File

@ -85,4 +85,6 @@ QPair<QString, QString> splitFirst(const QString& s, const QString& sep, Qt::Cas
QPair<QString, QString> splitFirst(const QString& s, QChar sep, Qt::CaseSensitivity cs = Qt::CaseSensitive); QPair<QString, QString> splitFirst(const QString& s, QChar sep, Qt::CaseSensitivity cs = Qt::CaseSensitive);
QPair<QString, QString> splitFirst(const QString& s, const QRegularExpression& re); QPair<QString, QString> splitFirst(const QString& s, const QRegularExpression& re);
QString htmlListPatch(QString htmlStr);
} // namespace StringUtils } // namespace StringUtils

View File

@ -322,7 +322,7 @@ const MMCIcon* IconList::icon(const QString& key) const
bool IconList::deleteIcon(const QString& key) bool IconList::deleteIcon(const QString& key)
{ {
return iconFileExists(key) && QFile::remove(icon(key)->getFilePath()); return iconFileExists(key) && FS::deletePath(icon(key)->getFilePath());
} }
bool IconList::trashIcon(const QString& key) bool IconList::trashIcon(const QString& key)

View File

@ -52,8 +52,7 @@ QString findBestIconIn(const QString& folder, const QString& iconKey)
while (it.hasNext()) { while (it.hasNext()) {
it.next(); it.next();
auto fileInfo = it.fileInfo(); auto fileInfo = it.fileInfo();
if ((fileInfo.completeBaseName() == iconKey || fileInfo.fileName() == iconKey) && isIconSuffix(fileInfo.suffix()))
if (fileInfo.completeBaseName() == iconKey && isIconSuffix(fileInfo.suffix()))
return fileInfo.absoluteFilePath(); return fileInfo.absoluteFilePath();
} }
return {}; return {};

View File

@ -207,7 +207,7 @@ QList<JavaInstallPtr> JavaUtils::FindJavaFromRegistryKey(DWORD keyType, QString
QString newKeyName = keyName + "\\" + newSubkeyName + subkeySuffix; QString newKeyName = keyName + "\\" + newSubkeyName + subkeySuffix;
HKEY newKey; HKEY newKey;
if (RegOpenKeyExW(HKEY_LOCAL_MACHINE, newKeyName.toStdWString().c_str(), 0, KEY_READ | KEY_WOW64_64KEY, &newKey) == if (RegOpenKeyExW(HKEY_LOCAL_MACHINE, newKeyName.toStdWString().c_str(), 0, KEY_READ | keyType, &newKey) ==
ERROR_SUCCESS) { ERROR_SUCCESS) {
// Read the JavaHome value to find where Java is installed. // Read the JavaHome value to find where Java is installed.
DWORD valueSz = 0; DWORD valueSz = 0;
@ -283,6 +283,12 @@ QList<QString> JavaUtils::FindJavaPaths()
QList<JavaInstallPtr> ADOPTIUMJDK64s = QList<JavaInstallPtr> ADOPTIUMJDK64s =
this->FindJavaFromRegistryKey(KEY_WOW64_64KEY, "SOFTWARE\\Eclipse Adoptium\\JDK", "Path", "\\hotspot\\MSI"); this->FindJavaFromRegistryKey(KEY_WOW64_64KEY, "SOFTWARE\\Eclipse Adoptium\\JDK", "Path", "\\hotspot\\MSI");
// IBM Semeru
QList<JavaInstallPtr> SEMERUJRE32s = this->FindJavaFromRegistryKey(KEY_WOW64_32KEY, "SOFTWARE\\Semeru\\JRE", "Path", "\\openj9\\MSI");
QList<JavaInstallPtr> SEMERUJRE64s = this->FindJavaFromRegistryKey(KEY_WOW64_64KEY, "SOFTWARE\\Semeru\\JRE", "Path", "\\openj9\\MSI");
QList<JavaInstallPtr> SEMERUJDK32s = this->FindJavaFromRegistryKey(KEY_WOW64_32KEY, "SOFTWARE\\Semeru\\JDK", "Path", "\\openj9\\MSI");
QList<JavaInstallPtr> SEMERUJDK64s = this->FindJavaFromRegistryKey(KEY_WOW64_64KEY, "SOFTWARE\\Semeru\\JDK", "Path", "\\openj9\\MSI");
// Microsoft // Microsoft
QList<JavaInstallPtr> MICROSOFTJDK64s = QList<JavaInstallPtr> MICROSOFTJDK64s =
this->FindJavaFromRegistryKey(KEY_WOW64_64KEY, "SOFTWARE\\Microsoft\\JDK", "Path", "\\hotspot\\MSI"); this->FindJavaFromRegistryKey(KEY_WOW64_64KEY, "SOFTWARE\\Microsoft\\JDK", "Path", "\\hotspot\\MSI");
@ -300,6 +306,7 @@ QList<QString> JavaUtils::FindJavaPaths()
java_candidates.append(NEWJRE64s); java_candidates.append(NEWJRE64s);
java_candidates.append(ADOPTOPENJRE64s); java_candidates.append(ADOPTOPENJRE64s);
java_candidates.append(ADOPTIUMJRE64s); java_candidates.append(ADOPTIUMJRE64s);
java_candidates.append(SEMERUJRE64s);
java_candidates.append(MakeJavaPtr("C:/Program Files/Java/jre8/bin/javaw.exe")); java_candidates.append(MakeJavaPtr("C:/Program Files/Java/jre8/bin/javaw.exe"));
java_candidates.append(MakeJavaPtr("C:/Program Files/Java/jre7/bin/javaw.exe")); java_candidates.append(MakeJavaPtr("C:/Program Files/Java/jre7/bin/javaw.exe"));
java_candidates.append(MakeJavaPtr("C:/Program Files/Java/jre6/bin/javaw.exe")); java_candidates.append(MakeJavaPtr("C:/Program Files/Java/jre6/bin/javaw.exe"));
@ -308,6 +315,7 @@ QList<QString> JavaUtils::FindJavaPaths()
java_candidates.append(ADOPTOPENJDK64s); java_candidates.append(ADOPTOPENJDK64s);
java_candidates.append(FOUNDATIONJDK64s); java_candidates.append(FOUNDATIONJDK64s);
java_candidates.append(ADOPTIUMJDK64s); java_candidates.append(ADOPTIUMJDK64s);
java_candidates.append(SEMERUJDK64s);
java_candidates.append(MICROSOFTJDK64s); java_candidates.append(MICROSOFTJDK64s);
java_candidates.append(ZULU64s); java_candidates.append(ZULU64s);
java_candidates.append(LIBERICA64s); java_candidates.append(LIBERICA64s);
@ -316,6 +324,7 @@ QList<QString> JavaUtils::FindJavaPaths()
java_candidates.append(NEWJRE32s); java_candidates.append(NEWJRE32s);
java_candidates.append(ADOPTOPENJRE32s); java_candidates.append(ADOPTOPENJRE32s);
java_candidates.append(ADOPTIUMJRE32s); java_candidates.append(ADOPTIUMJRE32s);
java_candidates.append(SEMERUJRE32s);
java_candidates.append(MakeJavaPtr("C:/Program Files (x86)/Java/jre8/bin/javaw.exe")); java_candidates.append(MakeJavaPtr("C:/Program Files (x86)/Java/jre8/bin/javaw.exe"));
java_candidates.append(MakeJavaPtr("C:/Program Files (x86)/Java/jre7/bin/javaw.exe")); java_candidates.append(MakeJavaPtr("C:/Program Files (x86)/Java/jre7/bin/javaw.exe"));
java_candidates.append(MakeJavaPtr("C:/Program Files (x86)/Java/jre6/bin/javaw.exe")); java_candidates.append(MakeJavaPtr("C:/Program Files (x86)/Java/jre6/bin/javaw.exe"));
@ -324,6 +333,7 @@ QList<QString> JavaUtils::FindJavaPaths()
java_candidates.append(ADOPTOPENJDK32s); java_candidates.append(ADOPTOPENJDK32s);
java_candidates.append(FOUNDATIONJDK32s); java_candidates.append(FOUNDATIONJDK32s);
java_candidates.append(ADOPTIUMJDK32s); java_candidates.append(ADOPTIUMJDK32s);
java_candidates.append(SEMERUJDK32s);
java_candidates.append(ZULU32s); java_candidates.append(ZULU32s);
java_candidates.append(LIBERICA32s); java_candidates.append(LIBERICA32s);
@ -410,6 +420,7 @@ QList<QString> JavaUtils::FindJavaPaths()
// manually installed JDKs in /opt // manually installed JDKs in /opt
scanJavaDirs("/opt/jdk"); scanJavaDirs("/opt/jdk");
scanJavaDirs("/opt/jdks"); scanJavaDirs("/opt/jdks");
scanJavaDirs("/opt/ibm"); // IBM Semeru Certified Edition
// flatpak // flatpak
scanJavaDirs("/app/jdk"); scanJavaDirs("/app/jdk");

View File

@ -15,6 +15,7 @@
#include "BaseEntity.h" #include "BaseEntity.h"
#include "FileSystem.h"
#include "Json.h" #include "Json.h"
#include "net/ApiDownload.h" #include "net/ApiDownload.h"
#include "net/HttpMetaCache.h" #include "net/HttpMetaCache.h"
@ -83,8 +84,7 @@ bool Meta::BaseEntity::loadLocalFile()
} catch (const Exception& e) { } catch (const Exception& e) {
qDebug() << QString("Unable to parse file %1: %2").arg(fname, e.cause()); qDebug() << QString("Unable to parse file %1: %2").arg(fname, e.cause());
// just make sure it's gone and we never consider it again. // just make sure it's gone and we never consider it again.
QFile::remove(fname); return !FS::deletePath(fname);
return false;
} }
} }

View File

@ -336,7 +336,7 @@ bool Component::revert()
bool result = true; bool result = true;
// just kill the file and reload // just kill the file and reload
if (QFile::exists(filename)) { if (QFile::exists(filename)) {
result = QFile::remove(filename); result = FS::deletePath(filename);
} }
if (result) { if (result) {
// file gone... // file gone...

View File

@ -1000,7 +1000,7 @@ QString MinecraftInstance::getStatusbarDescription()
QString description; QString description;
description.append(tr("Minecraft %1").arg(mcVersion)); description.append(tr("Minecraft %1").arg(mcVersion));
if (m_settings->get("ShowGameTime").toBool()) { if (m_settings->get("ShowGameTime").toBool()) {
if (lastTimePlayed() > 0) { if (lastTimePlayed() > 0 && lastLaunch() > 0) {
QDateTime lastLaunchTime = QDateTime::fromMSecsSinceEpoch(lastLaunch()); QDateTime lastLaunchTime = QDateTime::fromMSecsSinceEpoch(lastLaunch());
description.append( description.append(
tr(", last played on %1 for %2") tr(", last played on %1 for %2")

View File

@ -839,7 +839,7 @@ bool PackProfile::installCustomJar_internal(QString filepath)
QFileInfo jarInfo(finalPath); QFileInfo jarInfo(finalPath);
if (jarInfo.exists()) { if (jarInfo.exists()) {
if (!QFile::remove(finalPath)) { if (!FS::deletePath(finalPath)) {
return false; return false;
} }
} }

View File

@ -111,7 +111,7 @@ bool ResourceFolderModel::installResource(QString original_path)
case ResourceType::ZIPFILE: case ResourceType::ZIPFILE:
case ResourceType::LITEMOD: { case ResourceType::LITEMOD: {
if (QFile::exists(new_path) || QFile::exists(new_path + QString(".disabled"))) { if (QFile::exists(new_path) || QFile::exists(new_path + QString(".disabled"))) {
if (!QFile::remove(new_path)) { if (!FS::deletePath(new_path)) {
qCritical() << "Cleaning up new location (" << new_path << ") was unsuccessful!"; qCritical() << "Cleaning up new location (" << new_path << ") was unsuccessful!";
return false; return false;
} }

View File

@ -178,6 +178,88 @@ bool processZIP(ResourcePack& pack, ProcessingLevel level)
return true; return true;
} }
QString buildStyle(const QJsonObject& obj)
{
QStringList styles;
if (auto color = Json::ensureString(obj, "color"); !color.isEmpty()) {
styles << QString("color: %1;").arg(color);
}
if (obj.contains("bold")) {
QString weight = "normal";
if (Json::ensureBoolean(obj, "bold", false)) {
weight = "bold";
}
styles << QString("font-weight: %1;").arg(weight);
}
if (obj.contains("italic")) {
QString style = "normal";
if (Json::ensureBoolean(obj, "italic", false)) {
style = "italic";
}
styles << QString("font-style: %1;").arg(style);
}
return styles.isEmpty() ? "" : QString("style=\"%1\"").arg(styles.join(" "));
}
QString processComponent(const QJsonArray& value, bool strikethrough, bool underline)
{
QString result;
for (auto current : value)
result += processComponent(current, strikethrough, underline);
return result;
}
QString processComponent(const QJsonObject& obj, bool strikethrough, bool underline)
{
underline = Json::ensureBoolean(obj, "underlined", underline);
strikethrough = Json::ensureBoolean(obj, "strikethrough", strikethrough);
QString result = Json::ensureString(obj, "text");
if (underline) {
result = QString("<u>%1</u>").arg(result);
}
if (strikethrough) {
result = QString("<s>%1</s>").arg(result);
}
// the extra needs to be a array
result += processComponent(Json::ensureArray(obj, "extra"), strikethrough, underline);
if (auto style = buildStyle(obj); !style.isEmpty()) {
result = QString("<span %1>%2</span>").arg(style, result);
}
if (obj.contains("clickEvent")) {
auto click_event = Json::ensureObject(obj, "clickEvent");
auto action = Json::ensureString(click_event, "action");
auto value = Json::ensureString(click_event, "value");
if (action == "open_url" && !value.isEmpty()) {
result = QString("<a href=\"%1\">%2</a>").arg(value, result);
}
}
return result;
}
QString processComponent(const QJsonValue& value, bool strikethrough, bool underline)
{
if (value.isString()) {
return value.toString();
}
if (value.isBool()) {
return value.toBool() ? "true" : "false";
}
if (value.isDouble()) {
return QString::number(value.toDouble());
}
if (value.isArray()) {
return processComponent(value.toArray(), strikethrough, underline);
}
if (value.isObject()) {
return processComponent(value.toObject(), strikethrough, underline);
}
qWarning() << "Invalid component type!";
return {};
}
// https://minecraft.wiki/w/Raw_JSON_text_format
// https://minecraft.wiki/w/Tutorials/Creating_a_resource_pack#Formatting_pack.mcmeta // https://minecraft.wiki/w/Tutorials/Creating_a_resource_pack#Formatting_pack.mcmeta
bool processMCMeta(ResourcePack& pack, QByteArray&& raw_data) bool processMCMeta(ResourcePack& pack, QByteArray&& raw_data)
{ {
@ -186,7 +268,9 @@ bool processMCMeta(ResourcePack& pack, QByteArray&& raw_data)
auto pack_obj = Json::requireObject(json_doc.object(), "pack", {}); auto pack_obj = Json::requireObject(json_doc.object(), "pack", {});
pack.setPackFormat(Json::ensureInteger(pack_obj, "pack_format", 0)); pack.setPackFormat(Json::ensureInteger(pack_obj, "pack_format", 0));
pack.setDescription(Json::ensureString(pack_obj, "description", ""));
pack.setDescription(processComponent(pack_obj.value("description")));
} catch (Json::JsonException& e) { } catch (Json::JsonException& e) {
qWarning() << "JsonException: " << e.what() << e.cause(); qWarning() << "JsonException: " << e.what() << e.cause();
return false; return false;

View File

@ -34,6 +34,7 @@ bool process(ResourcePack& pack, ProcessingLevel level = ProcessingLevel::Full);
bool processZIP(ResourcePack& pack, ProcessingLevel level = ProcessingLevel::Full); bool processZIP(ResourcePack& pack, ProcessingLevel level = ProcessingLevel::Full);
bool processFolder(ResourcePack& pack, ProcessingLevel level = ProcessingLevel::Full); bool processFolder(ResourcePack& pack, ProcessingLevel level = ProcessingLevel::Full);
QString processComponent(const QJsonValue& value, bool strikethrough = false, bool underline = false);
bool processMCMeta(ResourcePack& pack, QByteArray&& raw_data); bool processMCMeta(ResourcePack& pack, QByteArray&& raw_data);
bool processPackPNG(const ResourcePack& pack, QByteArray&& raw_data); bool processPackPNG(const ResourcePack& pack, QByteArray&& raw_data);

View File

@ -282,7 +282,7 @@ void PackInstallTask::deleteExistingFiles()
// Delete the files // Delete the files
for (const auto& item : filesToDelete) { for (const auto& item : filesToDelete) {
QFile::remove(item); FS::deletePath(item);
} }
} }
@ -987,7 +987,7 @@ bool PackInstallTask::extractMods(const QMap<QString, VersionMod>& toExtract,
// the copy from the Configs.zip // the copy from the Configs.zip
QFileInfo fileInfo(to); QFileInfo fileInfo(to);
if (fileInfo.exists()) { if (fileInfo.exists()) {
if (!QFile::remove(to)) { if (!FS::deletePath(to)) {
qWarning() << "Failed to delete" << to; qWarning() << "Failed to delete" << to;
return false; return false;
} }

View File

@ -322,7 +322,7 @@ bool FlameCreationTask::createInstance()
// Keep index file in case we need it some other time (like when changing versions) // Keep index file in case we need it some other time (like when changing versions)
QString new_index_place(FS::PathCombine(parent_folder, "manifest.json")); QString new_index_place(FS::PathCombine(parent_folder, "manifest.json"));
FS::ensureFilePathExists(new_index_place); FS::ensureFilePathExists(new_index_place);
QFile::rename(index_path, new_index_place); FS::move(index_path, new_index_place);
} catch (const JSONValidationError& e) { } catch (const JSONValidationError& e) {
setError(tr("Could not understand pack manifest:\n") + e.cause()); setError(tr("Could not understand pack manifest:\n") + e.cause());
@ -336,7 +336,7 @@ bool FlameCreationTask::createInstance()
Override::createOverrides("overrides", parent_folder, overridePath); Override::createOverrides("overrides", parent_folder, overridePath);
QString mcPath = FS::PathCombine(m_stagingPath, "minecraft"); QString mcPath = FS::PathCombine(m_stagingPath, "minecraft");
if (!QFile::rename(overridePath, mcPath)) { if (!FS::move(overridePath, mcPath)) {
setError(tr("Could not rename the overrides folder:\n") + m_pack.overrides); setError(tr("Could not rename the overrides folder:\n") + m_pack.overrides);
return false; return false;
} }

View File

@ -42,17 +42,28 @@ QString toHTML(QList<Mod*> mods, OptionalData extraData)
} }
if (extraData & Authors && !mod->authors().isEmpty()) if (extraData & Authors && !mod->authors().isEmpty())
line += " by " + mod->authors().join(", ").toHtmlEscaped(); line += " by " + mod->authors().join(", ").toHtmlEscaped();
if (extraData & FileName)
line += QString(" (%1)").arg(mod->fileinfo().fileName().toHtmlEscaped());
lines.append(QString("<li>%1</li>").arg(line)); lines.append(QString("<li>%1</li>").arg(line));
} }
return QString("<html><body><ul>\n\t%1\n</ul></body></html>").arg(lines.join("\n\t")); return QString("<html><body><ul>\n\t%1\n</ul></body></html>").arg(lines.join("\n\t"));
} }
QString toMarkdownEscaped(QString src)
{
for (auto ch : "\\`*_{}[]<>()#+-.!|")
src.replace(ch, QString("\\%1").arg(ch));
return src;
}
QString toMarkdown(QList<Mod*> mods, OptionalData extraData) QString toMarkdown(QList<Mod*> mods, OptionalData extraData)
{ {
QStringList lines; QStringList lines;
for (auto mod : mods) { for (auto mod : mods) {
auto meta = mod->metadata(); auto meta = mod->metadata();
auto modName = mod->name(); auto modName = toMarkdownEscaped(mod->name());
if (extraData & Url) { if (extraData & Url) {
auto url = mod->metaurl(); auto url = mod->metaurl();
if (!url.isEmpty()) if (!url.isEmpty())
@ -60,14 +71,16 @@ QString toMarkdown(QList<Mod*> mods, OptionalData extraData)
} }
auto line = modName; auto line = modName;
if (extraData & Version) { if (extraData & Version) {
auto ver = mod->version(); auto ver = toMarkdownEscaped(mod->version());
if (ver.isEmpty() && meta != nullptr) if (ver.isEmpty() && meta != nullptr)
ver = meta->version().toString(); ver = toMarkdownEscaped(meta->version().toString());
if (!ver.isEmpty()) if (!ver.isEmpty())
line += QString(" [%1]").arg(ver); line += QString(" [%1]").arg(ver);
} }
if (extraData & Authors && !mod->authors().isEmpty()) if (extraData & Authors && !mod->authors().isEmpty())
line += " by " + mod->authors().join(", "); line += " by " + toMarkdownEscaped(mod->authors().join(", "));
if (extraData & FileName)
line += QString(" (%1)").arg(toMarkdownEscaped(mod->fileinfo().fileName()));
lines << "- " + line; lines << "- " + line;
} }
return lines.join("\n"); return lines.join("\n");
@ -95,6 +108,8 @@ QString toPlainTXT(QList<Mod*> mods, OptionalData extraData)
} }
if (extraData & Authors && !mod->authors().isEmpty()) if (extraData & Authors && !mod->authors().isEmpty())
line += " by " + mod->authors().join(", "); line += " by " + mod->authors().join(", ");
if (extraData & FileName)
line += QString(" (%1)").arg(mod->fileinfo().fileName());
lines << line; lines << line;
} }
return lines.join("\n"); return lines.join("\n");
@ -122,6 +137,8 @@ QString toJSON(QList<Mod*> mods, OptionalData extraData)
} }
if (extraData & Authors && !mod->authors().isEmpty()) if (extraData & Authors && !mod->authors().isEmpty())
line["authors"] = QJsonArray::fromStringList(mod->authors()); line["authors"] = QJsonArray::fromStringList(mod->authors());
if (extraData & FileName)
line["filename"] = mod->fileinfo().fileName();
lines << line; lines << line;
} }
QJsonDocument doc; QJsonDocument doc;
@ -154,6 +171,8 @@ QString toCSV(QList<Mod*> mods, OptionalData extraData)
authors = QString("\"%1\"").arg(mod->authors().join(",")); authors = QString("\"%1\"").arg(mod->authors().join(","));
data << authors; data << authors;
} }
if (extraData & FileName)
data << mod->fileinfo().fileName();
lines << data.join(","); lines << data.join(",");
} }
return lines.join("\n"); return lines.join("\n");
@ -189,11 +208,13 @@ QString exportToModList(QList<Mod*> mods, QString lineTemplate)
if (ver.isEmpty() && meta != nullptr) if (ver.isEmpty() && meta != nullptr)
ver = meta->version().toString(); ver = meta->version().toString();
auto authors = mod->authors().join(", "); auto authors = mod->authors().join(", ");
auto filename = mod->fileinfo().fileName();
lines << QString(lineTemplate) lines << QString(lineTemplate)
.replace("{name}", modName) .replace("{name}", modName)
.replace("{url}", url) .replace("{url}", url)
.replace("{version}", ver) .replace("{version}", ver)
.replace("{authors}", authors); .replace("{authors}", authors)
.replace("{filename}", filename);
} }
return lines.join("\n"); return lines.join("\n");
} }

View File

@ -23,11 +23,7 @@
namespace ExportToModList { namespace ExportToModList {
enum Formats { HTML, MARKDOWN, PLAINTXT, JSON, CSV, CUSTOM }; enum Formats { HTML, MARKDOWN, PLAINTXT, JSON, CSV, CUSTOM };
enum OptionalData { enum OptionalData { Authors = 1 << 0, Url = 1 << 1, Version = 1 << 2, FileName = 1 << 3 };
Authors = 1 << 0,
Url = 1 << 1,
Version = 1 << 2,
};
QString exportToModList(QList<Mod*> mods, Formats format, OptionalData extraData); QString exportToModList(QList<Mod*> mods, Formats format, OptionalData extraData);
QString exportToModList(QList<Mod*> mods, QString lineTemplate); QString exportToModList(QList<Mod*> mods, QString lineTemplate);
} // namespace ExportToModList } // namespace ExportToModList

View File

@ -10,7 +10,7 @@ void createOverrides(const QString& name, const QString& parent_folder, const QS
{ {
QString file_path(FS::PathCombine(parent_folder, name + ".txt")); QString file_path(FS::PathCombine(parent_folder, name + ".txt"));
if (QFile::exists(file_path)) if (QFile::exists(file_path))
QFile::remove(file_path); FS::deletePath(file_path);
FS::ensureFilePathExists(file_path); FS::ensureFilePathExists(file_path);

View File

@ -137,7 +137,7 @@ void PackInstallTask::install()
QDir unzipMcDir(m_stagingPath + "/unzip/minecraft"); QDir unzipMcDir(m_stagingPath + "/unzip/minecraft");
if (unzipMcDir.exists()) { if (unzipMcDir.exists()) {
// ok, found minecraft dir, move contents to instance dir // ok, found minecraft dir, move contents to instance dir
if (!QDir().rename(m_stagingPath + "/unzip/minecraft", m_stagingPath + "/minecraft")) { if (!FS::move(m_stagingPath + "/unzip/minecraft", m_stagingPath + "/minecraft")) {
emitFailed(tr("Failed to move unzipped Minecraft!")); emitFailed(tr("Failed to move unzipped Minecraft!"));
return; return;
} }

View File

@ -173,7 +173,7 @@ bool ModrinthCreationTask::createInstance()
// Keep index file in case we need it some other time (like when changing versions) // Keep index file in case we need it some other time (like when changing versions)
QString new_index_place(FS::PathCombine(parent_folder, "modrinth.index.json")); QString new_index_place(FS::PathCombine(parent_folder, "modrinth.index.json"));
FS::ensureFilePathExists(new_index_place); FS::ensureFilePathExists(new_index_place);
QFile::rename(index_path, new_index_place); FS::move(index_path, new_index_place);
auto mcPath = FS::PathCombine(m_stagingPath, m_root_path); auto mcPath = FS::PathCombine(m_stagingPath, m_root_path);
@ -183,7 +183,7 @@ bool ModrinthCreationTask::createInstance()
Override::createOverrides("overrides", parent_folder, override_path); Override::createOverrides("overrides", parent_folder, override_path);
// Apply the overrides // Apply the overrides
if (!QFile::rename(override_path, mcPath)) { if (!FS::move(override_path, mcPath)) {
setError(tr("Could not rename the overrides folder:\n") + "overrides"); setError(tr("Could not rename the overrides folder:\n") + "overrides");
return false; return false;
} }

View File

@ -131,6 +131,10 @@ auto loadIndexedVersion(QJsonObject& obj) -> ModpackVersion
file.name = Json::requireString(obj, "name"); file.name = Json::requireString(obj, "name");
file.version = Json::requireString(obj, "version_number"); file.version = Json::requireString(obj, "version_number");
auto gameVersions = Json::ensureArray(obj, "game_versions");
if (!gameVersions.isEmpty()) {
file.gameVersion = Json::ensureString(gameVersions[0]);
}
file.version_type = ModPlatform::IndexedVersionType(Json::requireString(obj, "version_type")); file.version_type = ModPlatform::IndexedVersionType(Json::requireString(obj, "version_type"));
file.changelog = Json::ensureString(obj, "changelog"); file.changelog = Json::ensureString(obj, "changelog");

View File

@ -84,6 +84,7 @@ struct ModpackExtra {
struct ModpackVersion { struct ModpackVersion {
QString name; QString name;
QString version; QString version;
QString gameVersion;
ModPlatform::IndexedVersionType version_type; ModPlatform::IndexedVersionType version_type;
QString changelog; QString changelog;

View File

@ -95,7 +95,6 @@
#include "ui/dialogs/CustomMessageBox.h" #include "ui/dialogs/CustomMessageBox.h"
#include "ui/dialogs/ExportInstanceDialog.h" #include "ui/dialogs/ExportInstanceDialog.h"
#include "ui/dialogs/ExportPackDialog.h" #include "ui/dialogs/ExportPackDialog.h"
#include "ui/dialogs/ExportToModListDialog.h"
#include "ui/dialogs/IconPickerDialog.h" #include "ui/dialogs/IconPickerDialog.h"
#include "ui/dialogs/ImportResourceDialog.h" #include "ui/dialogs/ImportResourceDialog.h"
#include "ui/dialogs/NewInstanceDialog.h" #include "ui/dialogs/NewInstanceDialog.h"
@ -208,7 +207,6 @@ MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent), ui(new Ui::MainWi
exportInstanceMenu->addAction(ui->actionExportInstanceZip); exportInstanceMenu->addAction(ui->actionExportInstanceZip);
exportInstanceMenu->addAction(ui->actionExportInstanceMrPack); exportInstanceMenu->addAction(ui->actionExportInstanceMrPack);
exportInstanceMenu->addAction(ui->actionExportInstanceFlamePack); exportInstanceMenu->addAction(ui->actionExportInstanceFlamePack);
exportInstanceMenu->addAction(ui->actionExportInstanceToModList);
ui->actionExportInstance->setMenu(exportInstanceMenu); ui->actionExportInstance->setMenu(exportInstanceMenu);
} }
@ -1420,14 +1418,6 @@ void MainWindow::on_actionExportInstanceMrPack_triggered()
} }
} }
void MainWindow::on_actionExportInstanceToModList_triggered()
{
if (m_selectedInstance) {
ExportToModListDialog dlg(m_selectedInstance, this);
dlg.exec();
}
}
void MainWindow::on_actionExportInstanceFlamePack_triggered() void MainWindow::on_actionExportInstanceFlamePack_triggered()
{ {
if (m_selectedInstance) { if (m_selectedInstance) {

View File

@ -160,7 +160,6 @@ class MainWindow : public QMainWindow {
void on_actionExportInstanceZip_triggered(); void on_actionExportInstanceZip_triggered();
void on_actionExportInstanceMrPack_triggered(); void on_actionExportInstanceMrPack_triggered();
void on_actionExportInstanceFlamePack_triggered(); void on_actionExportInstanceFlamePack_triggered();
void on_actionExportInstanceToModList_triggered();
void on_actionRenameInstance_triggered(); void on_actionRenameInstance_triggered();

View File

@ -492,15 +492,6 @@
<string>CurseForge (zip)</string> <string>CurseForge (zip)</string>
</property> </property>
</action> </action>
<action name="actionExportInstanceToModList">
<property name="icon">
<iconset theme="new">
<normaloff>.</normaloff>.</iconset>
</property>
<property name="text">
<string>Mod List</string>
</property>
</action>
<action name="actionCreateInstanceShortcut"> <action name="actionCreateInstanceShortcut">
<property name="icon"> <property name="icon">
<iconset theme="shortcut"> <iconset theme="shortcut">

View File

@ -38,6 +38,7 @@
#include "Application.h" #include "Application.h"
#include "BuildConfig.h" #include "BuildConfig.h"
#include "Markdown.h" #include "Markdown.h"
#include "StringUtils.h"
#include "ui_AboutDialog.h" #include "ui_AboutDialog.h"
#include <net/NetJob.h> #include <net/NetJob.h>
@ -139,10 +140,10 @@ AboutDialog::AboutDialog(QWidget* parent) : QDialog(parent), ui(new Ui::AboutDia
setWindowTitle(tr("About %1").arg(launcherName)); setWindowTitle(tr("About %1").arg(launcherName));
QString chtml = getCreditsHtml(); QString chtml = getCreditsHtml();
ui->creditsText->setHtml(chtml); ui->creditsText->setHtml(StringUtils::htmlListPatch(chtml));
QString lhtml = getLicenseHtml(); QString lhtml = getLicenseHtml();
ui->licenseText->setHtml(lhtml); ui->licenseText->setHtml(StringUtils::htmlListPatch(lhtml));
ui->urlLabel->setOpenExternalLinks(true); ui->urlLabel->setOpenExternalLinks(true);

View File

@ -22,8 +22,7 @@
#include <QTextEdit> #include <QTextEdit>
#include "FileSystem.h" #include "FileSystem.h"
#include "Markdown.h" #include "Markdown.h"
#include "minecraft/MinecraftInstance.h" #include "StringUtils.h"
#include "minecraft/mod/ModFolderModel.h"
#include "modplatform/helpers/ExportToModList.h" #include "modplatform/helpers/ExportToModList.h"
#include "ui_ExportToModListDialog.h" #include "ui_ExportToModListDialog.h"
@ -41,38 +40,31 @@ const QHash<ExportToModList::Formats, QString> ExportToModListDialog::exampleLin
{ ExportToModList::CSV, "{name},{url},{version},\"{authors}\"" }, { ExportToModList::CSV, "{name},{url},{version},\"{authors}\"" },
}; };
ExportToModListDialog::ExportToModListDialog(InstancePtr instance, QWidget* parent) ExportToModListDialog::ExportToModListDialog(QString name, QList<Mod*> mods, QWidget* parent)
: QDialog(parent), m_template_changed(false), name(instance->name()), ui(new Ui::ExportToModListDialog) : QDialog(parent), m_mods(mods), m_template_changed(false), m_name(name), ui(new Ui::ExportToModListDialog)
{ {
ui->setupUi(this); ui->setupUi(this);
enableCustom(false); enableCustom(false);
MinecraftInstance* mcInstance = dynamic_cast<MinecraftInstance*>(instance.get());
if (mcInstance) {
mcInstance->loaderModList()->update();
connect(mcInstance->loaderModList().get(), &ModFolderModel::updateFinished, this, [this, mcInstance]() {
m_allMods = mcInstance->loaderModList()->allMods();
triggerImp();
});
}
connect(ui->formatComboBox, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &ExportToModListDialog::formatChanged); connect(ui->formatComboBox, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &ExportToModListDialog::formatChanged);
connect(ui->authorsCheckBox, &QCheckBox::stateChanged, this, &ExportToModListDialog::trigger); connect(ui->authorsCheckBox, &QCheckBox::stateChanged, this, &ExportToModListDialog::trigger);
connect(ui->versionCheckBox, &QCheckBox::stateChanged, this, &ExportToModListDialog::trigger); connect(ui->versionCheckBox, &QCheckBox::stateChanged, this, &ExportToModListDialog::trigger);
connect(ui->urlCheckBox, &QCheckBox::stateChanged, this, &ExportToModListDialog::trigger); connect(ui->urlCheckBox, &QCheckBox::stateChanged, this, &ExportToModListDialog::trigger);
connect(ui->filenameCheckBox, &QCheckBox::stateChanged, this, &ExportToModListDialog::trigger);
connect(ui->authorsButton, &QPushButton::clicked, this, [this](bool) { addExtra(ExportToModList::Authors); }); connect(ui->authorsButton, &QPushButton::clicked, this, [this](bool) { addExtra(ExportToModList::Authors); });
connect(ui->versionButton, &QPushButton::clicked, this, [this](bool) { addExtra(ExportToModList::Version); }); connect(ui->versionButton, &QPushButton::clicked, this, [this](bool) { addExtra(ExportToModList::Version); });
connect(ui->urlButton, &QPushButton::clicked, this, [this](bool) { addExtra(ExportToModList::Url); }); connect(ui->urlButton, &QPushButton::clicked, this, [this](bool) { addExtra(ExportToModList::Url); });
connect(ui->filenameButton, &QPushButton::clicked, this, [this](bool) { addExtra(ExportToModList::FileName); });
connect(ui->templateText, &QTextEdit::textChanged, this, [this] { connect(ui->templateText, &QTextEdit::textChanged, this, [this] {
if (ui->templateText->toPlainText() != exampleLines[format]) if (ui->templateText->toPlainText() != exampleLines[m_format])
ui->formatComboBox->setCurrentIndex(5); ui->formatComboBox->setCurrentIndex(5);
else triggerImp();
triggerImp();
}); });
connect(ui->copyButton, &QPushButton::clicked, this, [this](bool) { connect(ui->copyButton, &QPushButton::clicked, this, [this](bool) {
this->ui->finalText->selectAll(); this->ui->finalText->selectAll();
this->ui->finalText->copy(); this->ui->finalText->copy();
}); });
triggerImp();
} }
ExportToModListDialog::~ExportToModListDialog() ExportToModListDialog::~ExportToModListDialog()
@ -86,38 +78,38 @@ void ExportToModListDialog::formatChanged(int index)
case 0: { case 0: {
enableCustom(false); enableCustom(false);
ui->resultText->show(); ui->resultText->show();
format = ExportToModList::HTML; m_format = ExportToModList::HTML;
break; break;
} }
case 1: { case 1: {
enableCustom(false); enableCustom(false);
ui->resultText->show(); ui->resultText->show();
format = ExportToModList::MARKDOWN; m_format = ExportToModList::MARKDOWN;
break; break;
} }
case 2: { case 2: {
enableCustom(false); enableCustom(false);
ui->resultText->hide(); ui->resultText->hide();
format = ExportToModList::PLAINTXT; m_format = ExportToModList::PLAINTXT;
break; break;
} }
case 3: { case 3: {
enableCustom(false); enableCustom(false);
ui->resultText->hide(); ui->resultText->hide();
format = ExportToModList::JSON; m_format = ExportToModList::JSON;
break; break;
} }
case 4: { case 4: {
enableCustom(false); enableCustom(false);
ui->resultText->hide(); ui->resultText->hide();
format = ExportToModList::CSV; m_format = ExportToModList::CSV;
break; break;
} }
case 5: { case 5: {
m_template_changed = true; m_template_changed = true;
enableCustom(true); enableCustom(true);
ui->resultText->hide(); ui->resultText->hide();
format = ExportToModList::CUSTOM; m_format = ExportToModList::CUSTOM;
break; break;
} }
} }
@ -126,8 +118,8 @@ void ExportToModListDialog::formatChanged(int index)
void ExportToModListDialog::triggerImp() void ExportToModListDialog::triggerImp()
{ {
if (format == ExportToModList::CUSTOM) { if (m_format == ExportToModList::CUSTOM) {
ui->finalText->setPlainText(ExportToModList::exportToModList(m_allMods, ui->templateText->toPlainText())); ui->finalText->setPlainText(ExportToModList::exportToModList(m_mods, ui->templateText->toPlainText()));
return; return;
} }
auto opt = 0; auto opt = 0;
@ -137,16 +129,18 @@ void ExportToModListDialog::triggerImp()
opt |= ExportToModList::Version; opt |= ExportToModList::Version;
if (ui->urlCheckBox->isChecked()) if (ui->urlCheckBox->isChecked())
opt |= ExportToModList::Url; opt |= ExportToModList::Url;
auto txt = ExportToModList::exportToModList(m_allMods, format, static_cast<ExportToModList::OptionalData>(opt)); if (ui->filenameCheckBox->isChecked())
opt |= ExportToModList::FileName;
auto txt = ExportToModList::exportToModList(m_mods, m_format, static_cast<ExportToModList::OptionalData>(opt));
ui->finalText->setPlainText(txt); ui->finalText->setPlainText(txt);
switch (format) { switch (m_format) {
case ExportToModList::CUSTOM: case ExportToModList::CUSTOM:
return; return;
case ExportToModList::HTML: case ExportToModList::HTML:
ui->resultText->setHtml(txt); ui->resultText->setHtml(StringUtils::htmlListPatch(txt));
break; break;
case ExportToModList::MARKDOWN: case ExportToModList::MARKDOWN:
ui->resultText->setHtml(markdownToHTML(txt)); ui->resultText->setHtml(StringUtils::htmlListPatch(markdownToHTML(txt)));
break; break;
case ExportToModList::PLAINTXT: case ExportToModList::PLAINTXT:
break; break;
@ -155,7 +149,7 @@ void ExportToModListDialog::triggerImp()
case ExportToModList::CSV: case ExportToModList::CSV:
break; break;
} }
auto exampleLine = exampleLines[format]; auto exampleLine = exampleLines[m_format];
if (!m_template_changed && ui->templateText->toPlainText() != exampleLine) if (!m_template_changed && ui->templateText->toPlainText() != exampleLine)
ui->templateText->setPlainText(exampleLine); ui->templateText->setPlainText(exampleLine);
} }
@ -163,9 +157,9 @@ void ExportToModListDialog::triggerImp()
void ExportToModListDialog::done(int result) void ExportToModListDialog::done(int result)
{ {
if (result == Accepted) { if (result == Accepted) {
const QString filename = FS::RemoveInvalidFilenameChars(name); const QString filename = FS::RemoveInvalidFilenameChars(m_name);
const QString output = const QString output =
QFileDialog::getSaveFileName(this, tr("Export %1").arg(name), FS::PathCombine(QDir::homePath(), filename + extension()), QFileDialog::getSaveFileName(this, tr("Export %1").arg(m_name), FS::PathCombine(QDir::homePath(), filename + extension()),
"File (*.txt *.html *.md *.json *.csv)", nullptr); "File (*.txt *.html *.md *.json *.csv)", nullptr);
if (output.isEmpty()) if (output.isEmpty())
@ -178,7 +172,7 @@ void ExportToModListDialog::done(int result)
QString ExportToModListDialog::extension() QString ExportToModListDialog::extension()
{ {
switch (format) { switch (m_format) {
case ExportToModList::HTML: case ExportToModList::HTML:
return ".html"; return ".html";
case ExportToModList::MARKDOWN: case ExportToModList::MARKDOWN:
@ -197,7 +191,7 @@ QString ExportToModListDialog::extension()
void ExportToModListDialog::addExtra(ExportToModList::OptionalData option) void ExportToModListDialog::addExtra(ExportToModList::OptionalData option)
{ {
if (format != ExportToModList::CUSTOM) if (m_format != ExportToModList::CUSTOM)
return; return;
switch (option) { switch (option) {
case ExportToModList::Authors: case ExportToModList::Authors:
@ -209,6 +203,9 @@ void ExportToModListDialog::addExtra(ExportToModList::OptionalData option)
case ExportToModList::Version: case ExportToModList::Version:
ui->templateText->insertPlainText("{version}"); ui->templateText->insertPlainText("{version}");
break; break;
case ExportToModList::FileName:
ui->templateText->insertPlainText("{filename}");
break;
} }
} }
void ExportToModListDialog::enableCustom(bool enabled) void ExportToModListDialog::enableCustom(bool enabled)
@ -221,4 +218,7 @@ void ExportToModListDialog::enableCustom(bool enabled)
ui->urlCheckBox->setHidden(enabled); ui->urlCheckBox->setHidden(enabled);
ui->urlButton->setHidden(!enabled); ui->urlButton->setHidden(!enabled);
ui->filenameCheckBox->setHidden(enabled);
ui->filenameButton->setHidden(!enabled);
} }

View File

@ -20,7 +20,6 @@
#include <QDialog> #include <QDialog>
#include <QList> #include <QList>
#include "BaseInstance.h"
#include "minecraft/mod/Mod.h" #include "minecraft/mod/Mod.h"
#include "modplatform/helpers/ExportToModList.h" #include "modplatform/helpers/ExportToModList.h"
@ -32,7 +31,7 @@ class ExportToModListDialog : public QDialog {
Q_OBJECT Q_OBJECT
public: public:
explicit ExportToModListDialog(InstancePtr instance, QWidget* parent = nullptr); explicit ExportToModListDialog(QString name, QList<Mod*> mods, QWidget* parent = nullptr);
~ExportToModListDialog(); ~ExportToModListDialog();
void done(int result) override; void done(int result) override;
@ -46,10 +45,11 @@ class ExportToModListDialog : public QDialog {
private: private:
QString extension(); QString extension();
void enableCustom(bool enabled); void enableCustom(bool enabled);
QList<Mod*> m_allMods;
QList<Mod*> m_mods;
bool m_template_changed; bool m_template_changed;
QString name; QString m_name;
ExportToModList::Formats format = ExportToModList::Formats::HTML; ExportToModList::Formats m_format = ExportToModList::Formats::HTML;
Ui::ExportToModListDialog* ui; Ui::ExportToModListDialog* ui;
static const QHash<ExportToModList::Formats, QString> exampleLines; static const QHash<ExportToModList::Formats, QString> exampleLines;
}; };

View File

@ -117,6 +117,13 @@
</property> </property>
</widget> </widget>
</item> </item>
<item>
<widget class="QCheckBox" name="filenameCheckBox">
<property name="text">
<string>Filename</string>
</property>
</widget>
</item>
<item> <item>
<widget class="QPushButton" name="versionButton"> <widget class="QPushButton" name="versionButton">
<property name="text"> <property name="text">
@ -138,6 +145,13 @@
</property> </property>
</widget> </widget>
</item> </item>
<item>
<widget class="QPushButton" name="filenameButton">
<property name="text">
<string>Filename</string>
</property>
</widget>
</item>
</layout> </layout>
</widget> </widget>
</item> </item>

View File

@ -3,6 +3,7 @@
#include "CustomMessageBox.h" #include "CustomMessageBox.h"
#include "ProgressDialog.h" #include "ProgressDialog.h"
#include "ScrollMessageBox.h" #include "ScrollMessageBox.h"
#include "StringUtils.h"
#include "minecraft/mod/tasks/GetModDependenciesTask.h" #include "minecraft/mod/tasks/GetModDependenciesTask.h"
#include "modplatform/ModIndex.h" #include "modplatform/ModIndex.h"
#include "modplatform/flame/FlameAPI.h" #include "modplatform/flame/FlameAPI.h"
@ -473,7 +474,7 @@ void ModUpdateDialog::appendMod(CheckUpdateTask::UpdatableMod const& info, QStri
break; break;
} }
changelog_area->setHtml(text); changelog_area->setHtml(StringUtils::htmlListPatch(text));
changelog_area->setOpenExternalLinks(true); changelog_area->setOpenExternalLinks(true);
changelog_area->setLineWrapMode(QTextBrowser::LineWrapMode::WidgetWidth); changelog_area->setLineWrapMode(QTextBrowser::LineWrapMode::WidgetWidth);
changelog_area->setVerticalScrollBarPolicy(Qt::ScrollBarPolicy::ScrollBarAsNeeded); changelog_area->setVerticalScrollBarPolicy(Qt::ScrollBarPolicy::ScrollBarAsNeeded);

View File

@ -52,6 +52,7 @@
#include <QFileDialog> #include <QFileDialog>
#include <QLayout> #include <QLayout>
#include <QPushButton> #include <QPushButton>
#include <QScreen>
#include <QValidator> #include <QValidator>
#include <utility> #include <utility>
@ -63,6 +64,7 @@
#include "ui/pages/modplatform/modrinth/ModrinthPage.h" #include "ui/pages/modplatform/modrinth/ModrinthPage.h"
#include "ui/pages/modplatform/technic/TechnicPage.h" #include "ui/pages/modplatform/technic/TechnicPage.h"
#include "ui/widgets/PageContainer.h" #include "ui/widgets/PageContainer.h"
NewInstanceDialog::NewInstanceDialog(const QString& initialGroup, NewInstanceDialog::NewInstanceDialog(const QString& initialGroup,
const QString& url, const QString& url,
const QMap<QString, QString>& extra_info, const QMap<QString, QString>& extra_info,
@ -127,7 +129,17 @@ NewInstanceDialog::NewInstanceDialog(const QString& initialGroup,
updateDialogState(); updateDialogState();
restoreGeometry(QByteArray::fromBase64(APPLICATION->settings()->get("NewInstanceGeometry").toByteArray())); if (APPLICATION->settings()->get("NewInstanceGeometry").isValid()) {
restoreGeometry(QByteArray::fromBase64(APPLICATION->settings()->get("NewInstanceGeometry").toByteArray()));
} else {
#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
auto screen = parent->screen();
#else
auto screen = QGuiApplication::primaryScreen();
#endif
auto geometry = screen->availableSize();
resize(width(), qMin(geometry.height() - 50, 710));
}
} }
void NewInstanceDialog::reject() void NewInstanceDialog::reject()

View File

@ -25,6 +25,7 @@
#include "Application.h" #include "Application.h"
#include "BuildConfig.h" #include "BuildConfig.h"
#include "Markdown.h" #include "Markdown.h"
#include "StringUtils.h"
#include "ui_UpdateAvailableDialog.h" #include "ui_UpdateAvailableDialog.h"
UpdateAvailableDialog::UpdateAvailableDialog(const QString& currentVersion, UpdateAvailableDialog::UpdateAvailableDialog(const QString& currentVersion,
@ -43,7 +44,7 @@ UpdateAvailableDialog::UpdateAvailableDialog(const QString& currentVersion,
ui->icon->setPixmap(APPLICATION->getThemedIcon("checkupdate").pixmap(64)); ui->icon->setPixmap(APPLICATION->getThemedIcon("checkupdate").pixmap(64));
auto releaseNotesHtml = markdownToHTML(releaseNotes); auto releaseNotesHtml = markdownToHTML(releaseNotes);
ui->releaseNotes->setHtml(releaseNotesHtml); ui->releaseNotes->setHtml(StringUtils::htmlListPatch(releaseNotesHtml));
ui->releaseNotes->setOpenExternalLinks(true); ui->releaseNotes->setOpenExternalLinks(true);
connect(ui->skipButton, &QPushButton::clicked, this, [this]() { connect(ui->skipButton, &QPushButton::clicked, this, [this]() {

View File

@ -66,6 +66,9 @@ void VisualGroup::update()
rows[currentRow].height = maxRowHeight; rows[currentRow].height = maxRowHeight;
rows[currentRow].top = offsetFromTop; rows[currentRow].top = offsetFromTop;
currentRow++; currentRow++;
if (currentRow >= rows.size()) {
currentRow = rows.size() - 1;
}
offsetFromTop += maxRowHeight + 5; offsetFromTop += maxRowHeight + 5;
positionInRow = 0; positionInRow = 0;
maxRowHeight = 0; maxRowHeight = 0;

View File

@ -70,15 +70,15 @@
</layout> </layout>
</widget> </widget>
<widget class="WideBar" name="actionsToolbar"> <widget class="WideBar" name="actionsToolbar">
<property name="useDefaultAction" stdset="0">
<bool>true</bool>
</property>
<property name="windowTitle"> <property name="windowTitle">
<string>Actions</string> <string>Actions</string>
</property> </property>
<property name="toolButtonStyle"> <property name="toolButtonStyle">
<enum>Qt::ToolButtonTextOnly</enum> <enum>Qt::ToolButtonTextOnly</enum>
</property> </property>
<property name="useDefaultAction" stdset="0">
<bool>true</bool>
</property>
<attribute name="toolBarArea"> <attribute name="toolBarArea">
<enum>RightToolBarArea</enum> <enum>RightToolBarArea</enum>
</attribute> </attribute>
@ -171,6 +171,17 @@
<string>Try to check or update all selected resources (all resources if none are selected)</string> <string>Try to check or update all selected resources (all resources if none are selected)</string>
</property> </property>
</action> </action>
<action name="actionExportMetadata">
<property name="enabled">
<bool>true</bool>
</property>
<property name="text">
<string>Export modlist</string>
</property>
<property name="toolTip">
<string>Export mod's metadata to text</string>
</property>
</action>
</widget> </widget>
<customwidgets> <customwidgets>
<customwidget> <customwidget>

View File

@ -20,6 +20,7 @@
#include "InstanceTask.h" #include "InstanceTask.h"
#include "Json.h" #include "Json.h"
#include "Markdown.h" #include "Markdown.h"
#include "StringUtils.h"
#include "modplatform/modrinth/ModrinthPackManifest.h" #include "modplatform/modrinth/ModrinthPackManifest.h"
@ -332,7 +333,7 @@ void ModrinthManagedPackPage::suggestVersion()
} }
auto version = m_pack.versions.at(index); auto version = m_pack.versions.at(index);
ui->changelogTextBrowser->setHtml(markdownToHTML(version.changelog.toUtf8())); ui->changelogTextBrowser->setHtml(StringUtils::htmlListPatch(markdownToHTML(version.changelog.toUtf8())));
ManagedPackPage::suggestVersion(); ManagedPackPage::suggestVersion();
} }
@ -420,7 +421,7 @@ void FlameManagedPackPage::parseManagedPack()
"Don't worry though, it will ask you to update this instance instead, so you'll not lose this instance!" "Don't worry though, it will ask you to update this instance instead, so you'll not lose this instance!"
"</h4>"); "</h4>");
ui->changelogTextBrowser->setHtml(message); ui->changelogTextBrowser->setHtml(StringUtils::htmlListPatch(message));
return; return;
} }
@ -502,7 +503,8 @@ void FlameManagedPackPage::suggestVersion()
} }
auto version = m_pack.versions.at(index); auto version = m_pack.versions.at(index);
ui->changelogTextBrowser->setHtml(m_api.getModFileChangelog(m_inst->getManagedPackID().toInt(), version.fileId)); ui->changelogTextBrowser->setHtml(
StringUtils::htmlListPatch(m_api.getModFileChangelog(m_inst->getManagedPackID().toInt(), version.fileId)));
ManagedPackPage::suggestVersion(); ManagedPackPage::suggestVersion();
} }

View File

@ -37,6 +37,7 @@
*/ */
#include "ModFolderPage.h" #include "ModFolderPage.h"
#include "ui/dialogs/ExportToModListDialog.h"
#include "ui_ExternalResourcesPage.h" #include "ui_ExternalResourcesPage.h"
#include <QAbstractItemModel> #include <QAbstractItemModel>
@ -121,6 +122,9 @@ ModFolderPage::ModFolderPage(BaseInstance* inst, std::shared_ptr<ModFolderModel>
ui->actionsToolbar->addAction(ui->actionVisitItemPage); ui->actionsToolbar->addAction(ui->actionVisitItemPage);
connect(ui->actionVisitItemPage, &QAction::triggered, this, &ModFolderPage::visitModPages); connect(ui->actionVisitItemPage, &QAction::triggered, this, &ModFolderPage::visitModPages);
ui->actionsToolbar->insertActionAfter(ui->actionVisitItemPage, ui->actionExportMetadata);
connect(ui->actionExportMetadata, &QAction::triggered, this, &ModFolderPage::exportModMetadata);
auto check_allow_update = [this] { return ui->treeView->selectionModel()->hasSelection() || !m_model->empty(); }; auto check_allow_update = [this] { return ui->treeView->selectionModel()->hasSelection() || !m_model->empty(); };
connect(ui->treeView->selectionModel(), &QItemSelectionModel::selectionChanged, this, connect(ui->treeView->selectionModel(), &QItemSelectionModel::selectionChanged, this,
@ -372,3 +376,15 @@ void ModFolderPage::deleteModMetadata()
m_model->deleteModsMetadata(selection); m_model->deleteModsMetadata(selection);
} }
void ModFolderPage::exportModMetadata()
{
auto selection = m_filterModel->mapSelectionToSource(ui->treeView->selectionModel()->selection()).indexes();
auto selectedMods = m_model->selectedMods(selection);
if (selectedMods.length() == 0)
selectedMods = m_model->allMods();
std::sort(selectedMods.begin(), selectedMods.end(), [](const Mod* a, const Mod* b) { return a->name() < b->name(); });
ExportToModListDialog dlg(m_instance->name(), selectedMods, this);
dlg.exec();
}

View File

@ -62,6 +62,7 @@ class ModFolderPage : public ExternalResourcesPage {
private slots: private slots:
void removeItems(const QItemSelection& selection) override; void removeItems(const QItemSelection& selection) override;
void deleteModMetadata(); void deleteModMetadata();
void exportModMetadata();
void installMods(); void installMods();
void updateMods(bool includeDeps = false); void updateMods(bool includeDeps = false);

View File

@ -49,7 +49,6 @@
CustomPage::CustomPage(NewInstanceDialog* dialog, QWidget* parent) : QWidget(parent), dialog(dialog), ui(new Ui::CustomPage) CustomPage::CustomPage(NewInstanceDialog* dialog, QWidget* parent) : QWidget(parent), dialog(dialog), ui(new Ui::CustomPage)
{ {
ui->setupUi(this); ui->setupUi(this);
ui->tabWidget->tabBar()->hide();
connect(ui->versionList, &VersionSelectWidget::selectedVersionChanged, this, &CustomPage::setSelectedVersion); connect(ui->versionList, &VersionSelectWidget::selectedVersionChanged, this, &CustomPage::setSelectedVersion);
filterChanged(); filterChanged();
connect(ui->alphaFilter, &QCheckBox::stateChanged, this, &CustomPage::filterChanged); connect(ui->alphaFilter, &QCheckBox::stateChanged, this, &CustomPage::filterChanged);

View File

@ -24,29 +24,21 @@
<number>0</number> <number>0</number>
</property> </property>
<item> <item>
<widget class="QTabWidget" name="tabWidget"> <widget class="QScrollArea" name="scrollArea">
<property name="currentIndex"> <property name="widgetResizable">
<number>0</number> <bool>true</bool>
</property> </property>
<widget class="QWidget" name="tab"> <widget class="QWidget" name="content">
<attribute name="title"> <property name="geometry">
<string notr="true"/> <rect>
</attribute> <x>0</x>
<layout class="QGridLayout" name="gridLayout_2"> <y>0</y>
<item row="2" column="0"> <width>813</width>
<widget class="Line" name="line"> <height>605</height>
<property name="sizePolicy"> </rect>
<sizepolicy hsizetype="Minimum" vsizetype="Fixed"> </property>
<horstretch>0</horstretch> <layout class="QVBoxLayout" name="verticalLayout_3">
<verstretch>0</verstretch> <item>
</sizepolicy>
</property>
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
</widget>
</item>
<item row="0" column="0">
<layout class="QHBoxLayout" name="minecraftLayout"> <layout class="QHBoxLayout" name="minecraftLayout">
<item> <item>
<widget class="VersionSelectWidget" name="versionList" native="true"> <widget class="VersionSelectWidget" name="versionList" native="true">
@ -147,7 +139,20 @@
</item> </item>
</layout> </layout>
</item> </item>
<item row="4" column="0"> <item>
<widget class="Line" name="line">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="loaderLayout"> <layout class="QHBoxLayout" name="loaderLayout">
<item> <item>
<widget class="VersionSelectWidget" name="loaderVersionList" native="true"> <widget class="VersionSelectWidget" name="loaderVersionList" native="true">
@ -273,7 +278,6 @@
</customwidget> </customwidget>
</customwidgets> </customwidgets>
<tabstops> <tabstops>
<tabstop>tabWidget</tabstop>
<tabstop>releaseFilter</tabstop> <tabstop>releaseFilter</tabstop>
<tabstop>snapshotFilter</tabstop> <tabstop>snapshotFilter</tabstop>
<tabstop>betaFilter</tabstop> <tabstop>betaFilter</tabstop>

View File

@ -40,6 +40,7 @@
#include "ui_ImportPage.h" #include "ui_ImportPage.h"
#include <QFileDialog> #include <QFileDialog>
#include <QMimeDatabase>
#include <QValidator> #include <QValidator>
#include <utility> #include <utility>
@ -51,6 +52,7 @@
#include "Json.h" #include "Json.h"
#include "InstanceImportTask.h" #include "InstanceImportTask.h"
#include "net/NetJob.h"
class UrlValidator : public QValidator { class UrlValidator : public QValidator {
public: public:

View File

@ -45,6 +45,7 @@
#include "Markdown.h" #include "Markdown.h"
#include "StringUtils.h"
#include "ui/dialogs/ResourceDownloadDialog.h" #include "ui/dialogs/ResourceDownloadDialog.h"
#include "ui/pages/modplatform/ResourceModel.h" #include "ui/pages/modplatform/ResourceModel.h"
#include "ui/widgets/ProjectItem.h" #include "ui/widgets/ProjectItem.h"
@ -234,8 +235,8 @@ void ResourcePage::updateUi()
text += "<hr>"; text += "<hr>";
m_ui->packDescription->setHtml( m_ui->packDescription->setHtml(StringUtils::htmlListPatch(
text + (current_pack->extraData.body.isEmpty() ? current_pack->description : markdownToHTML(current_pack->extraData.body))); text + (current_pack->extraData.body.isEmpty() ? current_pack->description : markdownToHTML(current_pack->extraData.body))));
m_ui->packDescription->flush(); m_ui->packDescription->flush();
} }

View File

@ -39,6 +39,7 @@
#include "ui_AtlPage.h" #include "ui_AtlPage.h"
#include "BuildConfig.h" #include "BuildConfig.h"
#include "StringUtils.h"
#include "AtlUserInteractionSupportImpl.h" #include "AtlUserInteractionSupportImpl.h"
#include "modplatform/atlauncher/ATLPackInstallTask.h" #include "modplatform/atlauncher/ATLPackInstallTask.h"
@ -144,7 +145,7 @@ void AtlPage::onSelectionChanged(QModelIndex first, [[maybe_unused]] QModelIndex
selected = filterModel->data(first, Qt::UserRole).value<ATLauncher::IndexedPack>(); selected = filterModel->data(first, Qt::UserRole).value<ATLauncher::IndexedPack>();
ui->packDescription->setHtml(selected.description.replace("\n", "<br>")); ui->packDescription->setHtml(StringUtils::htmlListPatch(selected.description.replace("\n", "<br>")));
for (const auto& version : selected.versions) { for (const auto& version : selected.versions) {
ui->versionSelectionBox->addItem(version.version); ui->versionSelectionBox->addItem(version.version);

View File

@ -43,6 +43,7 @@
#include "FlameModel.h" #include "FlameModel.h"
#include "InstanceImportTask.h" #include "InstanceImportTask.h"
#include "Json.h" #include "Json.h"
#include "StringUtils.h"
#include "modplatform/flame/FlameAPI.h" #include "modplatform/flame/FlameAPI.h"
#include "ui/dialogs/NewInstanceDialog.h" #include "ui/dialogs/NewInstanceDialog.h"
#include "ui/widgets/ProjectItem.h" #include "ui/widgets/ProjectItem.h"
@ -178,7 +179,11 @@ void FlamePage::onSelectionChanged(QModelIndex curr, [[maybe_unused]] QModelInde
for (auto version : current.versions) { for (auto version : current.versions) {
auto release_type = version.version_type.isValid() ? QString(" [%1]").arg(version.version_type.toString()) : ""; auto release_type = version.version_type.isValid() ? QString(" [%1]").arg(version.version_type.toString()) : "";
ui->versionSelectionBox->addItem(QString("%1%2").arg(version.version, release_type), QVariant(version.downloadUrl)); auto mcVersion = !version.mcVersion.isEmpty() && !version.version.contains(version.mcVersion)
? QString(" for %1").arg(version.mcVersion)
: "";
ui->versionSelectionBox->addItem(QString("%1%2%3").arg(version.version, mcVersion, release_type),
QVariant(version.downloadUrl));
} }
QVariant current_updated; QVariant current_updated;
@ -292,6 +297,6 @@ void FlamePage::updateUi()
text += "<hr>"; text += "<hr>";
text += api.getModDescription(current.addonId).toUtf8(); text += api.getModDescription(current.addonId).toUtf8();
ui->packDescription->setHtml(text + current.description); ui->packDescription->setHtml(StringUtils::htmlListPatch(text + current.description));
ui->packDescription->flush(); ui->packDescription->flush();
} }

View File

@ -35,6 +35,7 @@
*/ */
#include "Page.h" #include "Page.h"
#include "StringUtils.h"
#include "ui/widgets/ProjectItem.h" #include "ui/widgets/ProjectItem.h"
#include "ui_Page.h" #include "ui_Page.h"
@ -260,8 +261,9 @@ void Page::onPackSelectionChanged(Modpack* pack)
{ {
ui->versionSelectionBox->clear(); ui->versionSelectionBox->clear();
if (pack) { if (pack) {
currentModpackInfo->setHtml("Pack by <b>" + pack->author + "</b>" + "<br>Minecraft " + pack->mcVersion + "<br>" + "<br>" + currentModpackInfo->setHtml(StringUtils::htmlListPatch("Pack by <b>" + pack->author + "</b>" + "<br>Minecraft " + pack->mcVersion +
pack->description + "<ul><li>" + pack->mods.replace(";", "</li><li>") + "</li></ul>"); "<br>" + "<br>" + pack->description + "<ul><li>" +
pack->mods.replace(";", "</li><li>") + "</li></ul>"));
bool currentAdded = false; bool currentAdded = false;
for (int i = 0; i < pack->oldVersions.size(); i++) { for (int i = 0; i < pack->oldVersions.size(); i++) {

View File

@ -44,6 +44,7 @@
#include "InstanceImportTask.h" #include "InstanceImportTask.h"
#include "Json.h" #include "Json.h"
#include "Markdown.h" #include "Markdown.h"
#include "StringUtils.h"
#include "ui/widgets/ProjectItem.h" #include "ui/widgets/ProjectItem.h"
@ -223,11 +224,12 @@ void ModrinthPage::onSelectionChanged(QModelIndex curr, [[maybe_unused]] QModelI
} }
for (auto version : current.versions) { for (auto version : current.versions) {
auto release_type = version.version_type.isValid() ? QString(" [%1]").arg(version.version_type.toString()) : ""; auto release_type = version.version_type.isValid() ? QString(" [%1]").arg(version.version_type.toString()) : "";
if (!version.name.contains(version.version)) auto mcVersion = !version.gameVersion.isEmpty() && !version.name.contains(version.gameVersion)
ui->versionSelectionBox->addItem(QString("%1 — %2%3").arg(version.name, version.version, release_type), ? QString(" for %1").arg(version.gameVersion)
QVariant(version.id)); : "";
else auto versionStr = !version.name.contains(version.version) ? version.version : "";
ui->versionSelectionBox->addItem(QString("%1%2").arg(version.name, release_type), QVariant(version.id)); ui->versionSelectionBox->addItem(QString("%1%2 — %3%4").arg(version.name, mcVersion, versionStr, release_type),
QVariant(version.id));
} }
QVariant current_updated; QVariant current_updated;
@ -304,7 +306,7 @@ void ModrinthPage::updateUI()
text += markdownToHTML(current.extra.body.toUtf8()); text += markdownToHTML(current.extra.body.toUtf8());
ui->packDescription->setHtml(text + current.description); ui->packDescription->setHtml(StringUtils::htmlListPatch(text + current.description));
ui->packDescription->flush(); ui->packDescription->flush();
} }

View File

@ -44,6 +44,7 @@
#include "BuildConfig.h" #include "BuildConfig.h"
#include "Json.h" #include "Json.h"
#include "StringUtils.h"
#include "TechnicModel.h" #include "TechnicModel.h"
#include "modplatform/technic/SingleZipPackInstallTask.h" #include "modplatform/technic/SingleZipPackInstallTask.h"
#include "modplatform/technic/SolderPackInstallTask.h" #include "modplatform/technic/SolderPackInstallTask.h"
@ -233,7 +234,7 @@ void TechnicPage::metadataLoaded()
text += "<br><br>"; text += "<br><br>";
ui->packDescription->setHtml(text + current.description); ui->packDescription->setHtml(StringUtils::htmlListPatch(text + current.description));
// Strip trailing forward-slashes from Solder URL's // Strip trailing forward-slashes from Solder URL's
if (current.isSolder) { if (current.isSolder) {

View File

@ -313,3 +313,13 @@ void ThemeManager::initializeCatPacks()
} }
} }
} }
void ThemeManager::refresh()
{
m_themes.clear();
m_icons.clear();
m_cat_packs.clear();
initializeThemes();
initializeCatPacks();
};

View File

@ -55,6 +55,8 @@ class ThemeManager {
QString getCatPack(QString catName = ""); QString getCatPack(QString catName = "");
QList<CatPack*> getValidCatPacks(); QList<CatPack*> getValidCatPacks();
void refresh();
private: private:
std::map<QString, std::unique_ptr<ITheme>> m_themes; std::map<QString, std::unique_ptr<ITheme>> m_themes;
std::map<QString, IconTheme> m_icons; std::map<QString, IconTheme> m_icons;

View File

@ -36,6 +36,8 @@
#include <QLabel> #include <QLabel>
#include <QMessageBox> #include <QMessageBox>
#include <QTextCursor>
#include <QTextDocument>
#include <QToolTip> #include <QToolTip>
#include "InfoFrame.h" #include "InfoFrame.h"
@ -274,12 +276,27 @@ void InfoFrame::setDescription(QString text)
} }
QString labeltext; QString labeltext;
labeltext.reserve(300); labeltext.reserve(300);
if (finaltext.length() > 290) {
// elide rich text by getting characters without formatting
const int maxCharacterElide = 290;
QTextDocument doc;
doc.setHtml(text);
if (doc.characterCount() > maxCharacterElide) {
ui->descriptionLabel->setOpenExternalLinks(false); ui->descriptionLabel->setOpenExternalLinks(false);
ui->descriptionLabel->setTextFormat(Qt::TextFormat::RichText); ui->descriptionLabel->setTextFormat(Qt::TextFormat::RichText); // This allows injecting HTML here.
m_description = text; m_description = text;
// This allows injecting HTML here.
labeltext.append("<html><body>" + finaltext.left(287) + "<a href=\"#mod_desc\">...</a></body></html>"); // move the cursor to the character elide, doesn't see html
QTextCursor cursor(&doc);
cursor.movePosition(QTextCursor::End);
cursor.setPosition(maxCharacterElide, QTextCursor::KeepAnchor);
cursor.removeSelectedText();
// insert the post fix at the cursor
cursor.insertHtml("<a href=\"#mod_desc\">...</a>");
labeltext.append(doc.toHtml());
QObject::connect(ui->descriptionLabel, &QLabel::linkActivated, this, &InfoFrame::descriptionEllipsisHandler); QObject::connect(ui->descriptionLabel, &QLabel::linkActivated, this, &InfoFrame::descriptionEllipsisHandler);
} else { } else {
ui->descriptionLabel->setTextFormat(Qt::TextFormat::AutoText); ui->descriptionLabel->setTextFormat(Qt::TextFormat::AutoText);
@ -316,7 +333,7 @@ void InfoFrame::setLicense(QString text)
if (finaltext.length() > 290) { if (finaltext.length() > 290) {
ui->licenseLabel->setOpenExternalLinks(false); ui->licenseLabel->setOpenExternalLinks(false);
ui->licenseLabel->setTextFormat(Qt::TextFormat::RichText); ui->licenseLabel->setTextFormat(Qt::TextFormat::RichText);
m_description = text; m_license = text;
// This allows injecting HTML here. // This allows injecting HTML here.
labeltext.append("<html><body>" + finaltext.left(287) + "<a href=\"#mod_desc\">...</a></body></html>"); labeltext.append("<html><body>" + finaltext.left(287) + "<a href=\"#mod_desc\">...</a></body></html>");
QObject::connect(ui->licenseLabel, &QLabel::linkActivated, this, &InfoFrame::licenseEllipsisHandler); QObject::connect(ui->licenseLabel, &QLabel::linkActivated, this, &InfoFrame::licenseEllipsisHandler);

View File

@ -27,6 +27,7 @@ ThemeCustomizationWidget::ThemeCustomizationWidget(QWidget* parent) : QWidget(pa
{ {
ui->setupUi(this); ui->setupUi(this);
loadSettings(); loadSettings();
ThemeCustomizationWidget::refresh();
connect(ui->iconsComboBox, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &ThemeCustomizationWidget::applyIconTheme); connect(ui->iconsComboBox, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &ThemeCustomizationWidget::applyIconTheme);
connect(ui->widgetStyleComboBox, QOverload<int>::of(&QComboBox::currentIndexChanged), this, connect(ui->widgetStyleComboBox, QOverload<int>::of(&QComboBox::currentIndexChanged), this,
@ -39,6 +40,8 @@ ThemeCustomizationWidget::ThemeCustomizationWidget(QWidget* parent) : QWidget(pa
[] { DesktopServices::openPath(APPLICATION->themeManager()->getApplicationThemesFolder().path()); }); [] { DesktopServices::openPath(APPLICATION->themeManager()->getApplicationThemesFolder().path()); });
connect(ui->catPackFolder, &QPushButton::clicked, this, connect(ui->catPackFolder, &QPushButton::clicked, this,
[] { DesktopServices::openPath(APPLICATION->themeManager()->getCatPacksFolder().path()); }); [] { DesktopServices::openPath(APPLICATION->themeManager()->getCatPacksFolder().path()); });
connect(ui->refreshButton, &QPushButton::clicked, this, &ThemeCustomizationWidget::refresh);
} }
ThemeCustomizationWidget::~ThemeCustomizationWidget() ThemeCustomizationWidget::~ThemeCustomizationWidget()
@ -169,3 +172,22 @@ void ThemeCustomizationWidget::retranslate()
{ {
ui->retranslateUi(this); ui->retranslateUi(this);
} }
void ThemeCustomizationWidget::refresh()
{
applySettings();
disconnect(ui->iconsComboBox, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &ThemeCustomizationWidget::applyIconTheme);
disconnect(ui->widgetStyleComboBox, QOverload<int>::of(&QComboBox::currentIndexChanged), this,
&ThemeCustomizationWidget::applyWidgetTheme);
disconnect(ui->backgroundCatComboBox, QOverload<int>::of(&QComboBox::currentIndexChanged), this,
&ThemeCustomizationWidget::applyCatTheme);
APPLICATION->themeManager()->refresh();
ui->iconsComboBox->clear();
ui->widgetStyleComboBox->clear();
ui->backgroundCatComboBox->clear();
loadSettings();
connect(ui->iconsComboBox, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &ThemeCustomizationWidget::applyIconTheme);
connect(ui->widgetStyleComboBox, QOverload<int>::of(&QComboBox::currentIndexChanged), this,
&ThemeCustomizationWidget::applyWidgetTheme);
connect(ui->backgroundCatComboBox, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &ThemeCustomizationWidget::applyCatTheme);
};

View File

@ -44,6 +44,7 @@ class ThemeCustomizationWidget : public QWidget {
void applyIconTheme(int index); void applyIconTheme(int index);
void applyWidgetTheme(int index); void applyWidgetTheme(int index);
void applyCatTheme(int index); void applyCatTheme(int index);
void refresh();
signals: signals:
int currentIconThemeChanged(int index); int currentIconThemeChanged(int index);

View File

@ -13,7 +13,7 @@
<property name="windowTitle"> <property name="windowTitle">
<string notr="true">Form</string> <string notr="true">Form</string>
</property> </property>
<layout class="QFormLayout" name="formLayout"> <layout class="QVBoxLayout" name="verticalLayout">
<property name="sizeConstraint"> <property name="sizeConstraint">
<enum>QLayout::SetMinimumSize</enum> <enum>QLayout::SetMinimumSize</enum>
</property> </property>
@ -29,141 +29,179 @@
<property name="bottomMargin"> <property name="bottomMargin">
<number>0</number> <number>0</number>
</property> </property>
<item row="0" column="0"> <item>
<widget class="QLabel" name="iconsLabel"> <layout class="QFormLayout" name="formLayout">
<property name="text"> <item row="0" column="0">
<string>&amp;Icons</string> <widget class="QLabel" name="iconsLabel">
</property>
<property name="buddy">
<cstring>iconsComboBox</cstring>
</property>
</widget>
</item>
<item row="0" column="1">
<layout class="QHBoxLayout" name="iconsLayout">
<item>
<widget class="QComboBox" name="iconsComboBox">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="focusPolicy">
<enum>Qt::StrongFocus</enum>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="iconsFolder">
<property name="toolTip">
<string>View icon themes folder.</string>
</property>
<property name="text"> <property name="text">
<string/> <string>&amp;Icons</string>
</property> </property>
<property name="icon"> <property name="buddy">
<iconset theme="viewfolder"> <cstring>iconsComboBox</cstring>
<normaloff>.</normaloff>.</iconset>
</property>
<property name="flat">
<bool>true</bool>
</property> </property>
</widget> </widget>
</item> </item>
</layout> <item row="0" column="1">
</item> <layout class="QHBoxLayout" name="iconsLayout">
<item row="1" column="0"> <item>
<widget class="QLabel" name="widgetStyleLabel"> <widget class="QComboBox" name="iconsComboBox">
<property name="text"> <property name="sizePolicy">
<string>&amp;Widgets</string> <sizepolicy hsizetype="Expanding" vsizetype="Fixed">
</property> <horstretch>0</horstretch>
<property name="buddy"> <verstretch>0</verstretch>
<cstring>widgetStyleComboBox</cstring> </sizepolicy>
</property> </property>
</widget> <property name="focusPolicy">
</item> <enum>Qt::StrongFocus</enum>
<item row="1" column="1"> </property>
<layout class="QHBoxLayout" name="widgetStyleLayout"> </widget>
<item> </item>
<widget class="QComboBox" name="widgetStyleComboBox"> <item>
<property name="sizePolicy"> <widget class="QPushButton" name="iconsFolder">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed"> <property name="toolTip">
<horstretch>0</horstretch> <string>View icon themes folder.</string>
<verstretch>0</verstretch> </property>
</sizepolicy> <property name="text">
</property> <string/>
<property name="focusPolicy"> </property>
<enum>Qt::StrongFocus</enum> <property name="icon">
</property> <iconset theme="viewfolder"/>
</widget> </property>
<property name="flat">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</item> </item>
<item> <item row="1" column="0">
<widget class="QPushButton" name="widgetStyleFolder"> <widget class="QLabel" name="widgetStyleLabel">
<property name="toolTip">
<string>View widget themes folder.</string>
</property>
<property name="text"> <property name="text">
<string/> <string>&amp;Widgets</string>
</property> </property>
<property name="icon"> <property name="buddy">
<iconset theme="viewfolder"> <cstring>widgetStyleComboBox</cstring>
<normaloff>.</normaloff>.</iconset>
</property>
<property name="flat">
<bool>true</bool>
</property> </property>
</widget> </widget>
</item> </item>
</layout> <item row="1" column="1">
</item> <layout class="QHBoxLayout" name="widgetStyleLayout">
<item row="2" column="0"> <item>
<widget class="QLabel" name="backgroundCatLabel"> <widget class="QComboBox" name="widgetStyleComboBox">
<property name="toolTip"> <property name="sizePolicy">
<string>The cat appears in the background and is not shown by default. It is only made visible when pressing the Cat button in the Toolbar.</string> <sizepolicy hsizetype="Expanding" vsizetype="Fixed">
</property> <horstretch>0</horstretch>
<property name="text"> <verstretch>0</verstretch>
<string>C&amp;at</string> </sizepolicy>
</property> </property>
<property name="buddy"> <property name="focusPolicy">
<cstring>backgroundCatComboBox</cstring> <enum>Qt::StrongFocus</enum>
</property> </property>
</widget> </widget>
</item> </item>
<item row="2" column="1"> <item>
<layout class="QHBoxLayout" name="catLayout"> <widget class="QPushButton" name="widgetStyleFolder">
<item> <property name="toolTip">
<widget class="QComboBox" name="backgroundCatComboBox"> <string>View widget themes folder.</string>
<property name="sizePolicy"> </property>
<sizepolicy hsizetype="Expanding" vsizetype="Fixed"> <property name="text">
<horstretch>0</horstretch> <string/>
<verstretch>0</verstretch> </property>
</sizepolicy> <property name="icon">
</property> <iconset theme="viewfolder"/>
<property name="focusPolicy"> </property>
<enum>Qt::StrongFocus</enum> <property name="flat">
</property> <bool>true</bool>
</property>
</widget>
</item>
</layout>
</item>
<item row="2" column="0">
<widget class="QLabel" name="backgroundCatLabel">
<property name="toolTip"> <property name="toolTip">
<string>The cat appears in the background and is not shown by default. It is only made visible when pressing the Cat button in the Toolbar.</string> <string>The cat appears in the background and is not shown by default. It is only made visible when pressing the Cat button in the Toolbar.</string>
</property> </property>
<property name="text">
<string>C&amp;at</string>
</property>
<property name="buddy">
<cstring>backgroundCatComboBox</cstring>
</property>
</widget>
</item>
<item row="2" column="1">
<layout class="QHBoxLayout" name="catLayout">
<item>
<widget class="QComboBox" name="backgroundCatComboBox">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="focusPolicy">
<enum>Qt::StrongFocus</enum>
</property>
<property name="toolTip">
<string>The cat appears in the background and is not shown by default. It is only made visible when pressing the Cat button in the Toolbar.</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="catPackFolder">
<property name="toolTip">
<string>View cat packs folder.</string>
</property>
<property name="text">
<string/>
</property>
<property name="icon">
<iconset theme="viewfolder"/>
</property>
<property name="flat">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="refreshLayout">
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="refreshButton">
<property name="text">
<string>Refresh all</string>
</property>
</widget> </widget>
</item> </item>
<item> <item>
<widget class="QPushButton" name="catPackFolder"> <spacer name="horizontalSpacer_2">
<property name="toolTip"> <property name="orientation">
<string>View cat packs folder.</string> <enum>Qt::Horizontal</enum>
</property> </property>
<property name="text"> <property name="sizeHint" stdset="0">
<string/> <size>
<width>40</width>
<height>20</height>
</size>
</property> </property>
<property name="icon"> </spacer>
<iconset theme="viewfolder">
<normaloff>.</normaloff>.</iconset>
</property>
<property name="flat">
<bool>true</bool>
</property>
</widget>
</item> </item>
</layout> </layout>
</item> </item>

View File

@ -352,15 +352,10 @@ PrismUpdaterApp::PrismUpdaterApp(int& argc, char** argv) : QApplication(argc, ar
FS::ensureFolderPathExists(FS::PathCombine(m_dataPath, "logs")); FS::ensureFolderPathExists(FS::PathCombine(m_dataPath, "logs"));
static const QString baseLogFile = BuildConfig.LAUNCHER_NAME + "Updater" + (m_checkOnly ? "-CheckOnly" : "") + "-%0.log"; static const QString baseLogFile = BuildConfig.LAUNCHER_NAME + "Updater" + (m_checkOnly ? "-CheckOnly" : "") + "-%0.log";
static const QString logBase = FS::PathCombine(m_dataPath, "logs", baseLogFile); static const QString logBase = FS::PathCombine(m_dataPath, "logs", baseLogFile);
auto moveFile = [](const QString& oldName, const QString& newName) {
QFile::remove(newName);
QFile::copy(oldName, newName);
QFile::remove(oldName);
};
if (FS::ensureFolderPathExists("logs")) { // enough history to track both launches of the updater during a portable install if (FS::ensureFolderPathExists("logs")) { // enough history to track both launches of the updater during a portable install
moveFile(logBase.arg(1), logBase.arg(2)); FS::move(logBase.arg(1), logBase.arg(2));
moveFile(logBase.arg(0), logBase.arg(1)); FS::move(logBase.arg(0), logBase.arg(1));
} }
logFile = std::unique_ptr<QFile>(new QFile(logBase.arg(0))); logFile = std::unique_ptr<QFile>(new QFile(logBase.arg(0)));
@ -924,7 +919,7 @@ bool PrismUpdaterApp::callAppImageUpdate()
void PrismUpdaterApp::clearUpdateLog() void PrismUpdaterApp::clearUpdateLog()
{ {
QFile::remove(m_updateLogPath); FS::deletePath(m_updateLogPath);
} }
void PrismUpdaterApp::logUpdate(const QString& msg) void PrismUpdaterApp::logUpdate(const QString& msg)

View File

@ -26,6 +26,7 @@
#include <QTextBrowser> #include <QTextBrowser>
#include "Markdown.h" #include "Markdown.h"
#include "StringUtils.h"
SelectReleaseDialog::SelectReleaseDialog(const Version& current_version, const QList<GitHubRelease>& releases, QWidget* parent) SelectReleaseDialog::SelectReleaseDialog(const Version& current_version, const QList<GitHubRelease>& releases, QWidget* parent)
: QDialog(parent), m_releases(releases), m_currentVersion(current_version), ui(new Ui::SelectReleaseDialog) : QDialog(parent), m_releases(releases), m_currentVersion(current_version), ui(new Ui::SelectReleaseDialog)
@ -96,7 +97,7 @@ void SelectReleaseDialog::selectionChanged(QTreeWidgetItem* current, QTreeWidget
QString body = markdownToHTML(release.body.toUtf8()); QString body = markdownToHTML(release.body.toUtf8());
m_selectedRelease = release; m_selectedRelease = release;
ui->changelogTextBrowser->setHtml(body); ui->changelogTextBrowser->setHtml(StringUtils::htmlListPatch(body));
} }
SelectReleaseAssetDialog::SelectReleaseAssetDialog(const QList<GitHubReleaseAsset>& assets, QWidget* parent) SelectReleaseAssetDialog::SelectReleaseAssetDialog(const QList<GitHubReleaseAsset>& assets, QWidget* parent)

View File

@ -57,5 +57,8 @@ ecm_add_test(Index_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}:
ecm_add_test(Version_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test ecm_add_test(Version_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test
TEST_NAME Version) TEST_NAME Version)
ecm_add_test(MetaComponentParse_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test
TEST_NAME MetaComponentParse)
ecm_add_test(CatPack_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test ecm_add_test(CatPack_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test
TEST_NAME CatPack) TEST_NAME CatPack)

View File

@ -0,0 +1,87 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* This file incorporates work covered by the following copyright and
* permission notice:
*
* Copyright 2013-2021 MultiMC Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonValue>
#include <QTest>
#include <QTimer>
#include <FileSystem.h>
#include <minecraft/mod/tasks/LocalResourcePackParseTask.h>
class MetaComponentParseTest : public QObject {
Q_OBJECT
void doTest(QString name)
{
QString source = QFINDTESTDATA("testdata/MetaComponentParse");
QString comp_rp = FS::PathCombine(source, name);
QFile file;
file.setFileName(comp_rp);
QVERIFY(file.open(QIODevice::ReadOnly | QIODevice::Text));
QString data = file.readAll();
file.close();
QJsonDocument doc = QJsonDocument::fromJson(data.toUtf8());
QJsonObject obj = doc.object();
QJsonValue description_json = obj.value("description");
QJsonValue expected_json = obj.value("expected_output");
QVERIFY(!description_json.isUndefined());
QVERIFY(expected_json.isString());
QString expected = expected_json.toString();
QString processed = ResourcePackUtils::processComponent(description_json);
QCOMPARE(processed, expected);
}
private slots:
void test_parseComponentBasic() { doTest("component_basic.json"); }
void test_parseComponentWithFormat() { doTest("component_with_format.json"); }
void test_parseComponentWithExtra() { doTest("component_with_extra.json"); }
void test_parseComponentWithLink() { doTest("component_with_link.json"); }
void test_parseComponentWithMixed() { doTest("component_with_mixed.json"); }
};
QTEST_GUILESS_MAIN(MetaComponentParseTest)
#include "MetaComponentParse_test.moc"

View File

@ -0,0 +1,8 @@
{
"description": [
{
"text": "Hello, Component!"
}
],
"expected_output": "Hello, Component!"
}

View File

@ -0,0 +1,21 @@
{
"description": [
{
"text": "Hello, ",
"color": "red",
"bold": true,
"italic": true,
"extra": [
{
"extra": [
"Component!"
],
"bold": false,
"italic": false
}
]
}
],
"expected_output":
"<span style=\"color: red; font-weight: bold; font-style: italic;\">Hello, <span style=\"font-weight: normal; font-style: normal;\">Component!</span></span>"
}

View File

@ -0,0 +1,13 @@
{
"description": [
{
"text": "Hello, Component!",
"color": "blue",
"bold": true,
"italic": true,
"underlined": true,
"strikethrough": true
}
],
"expected_output": "<span style=\"color: blue; font-weight: bold; font-style: italic;\"><s><u>Hello, Component!</u></s></span>"
}

View File

@ -0,0 +1,12 @@
{
"description": [
{
"text": "Hello, Component!",
"clickEvent": {
"action": "open_url",
"value": "https://google.com"
}
}
],
"expected_output": "<a href=\"https://google.com\">Hello, Component!</a>"
}

View File

@ -0,0 +1,45 @@
{
"description": [
{
"text": "The quick ",
"color": "blue",
"italic": true
},
{
"text": "brown fox ",
"color": "#873600",
"bold": true,
"underlined": true,
"extra": [
{
"text": "jumped over ",
"color": "blue",
"bold": false,
"underlined": false,
"italic": true,
"strikethrough": true
}
]
},
{
"text": "the lazy dog's back. ",
"color": "green",
"bold": true,
"italic": true,
"underlined": true,
"strikethrough": true,
"extra": [
{
"text": "1234567890 ",
"color": "black",
"strikethrough": false,
"extra": [
"How vexingly quick daft zebras jump!"
]
}
]
}
],
"expected_output":
"<span style=\"color: blue; font-style: italic;\">The quick </span><span style=\"color: #873600; font-weight: bold;\"><u>brown fox </u><span style=\"color: blue; font-weight: normal; font-style: italic;\"><s>jumped over </s></span></span><span style=\"color: green; font-weight: bold; font-style: italic;\"><s><u>the lazy dog's back. </u></s><span style=\"color: black;\"><u>1234567890 </u>How vexingly quick daft zebras jump!</span></span>"
}