diff --git a/CMakeLists.txt b/CMakeLists.txt
index ca5ca62c2..33d281772 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -179,7 +179,7 @@ set(Launcher_HELP_URL "https://prismlauncher.org/wiki/help-pages/%1" CACHE STRIN
######## Set version numbers ########
set(Launcher_VERSION_MAJOR 8)
-set(Launcher_VERSION_MINOR 3)
+set(Launcher_VERSION_MINOR 4)
set(Launcher_VERSION_NAME "${Launcher_VERSION_MAJOR}.${Launcher_VERSION_MINOR}")
set(Launcher_VERSION_NAME4 "${Launcher_VERSION_MAJOR}.${Launcher_VERSION_MINOR}.0.0")
diff --git a/buildconfig/BuildConfig.h b/buildconfig/BuildConfig.h
index 278da0dfd..71f597857 100644
--- a/buildconfig/BuildConfig.h
+++ b/buildconfig/BuildConfig.h
@@ -167,15 +167,14 @@ class Config {
QString DISCORD_URL;
QString SUBREDDIT_URL;
- QString RESOURCE_BASE = "https://resources.download.minecraft.net/";
- QString LIBRARY_BASE = "https://libraries.minecraft.net/";
-
// Minecraft expects these without trailing slashes, best to keep that format everywhere
QString MOJANG_AUTH_BASE = "https://authserver.mojang.com";
QString MOJANG_ACCOUNT_BASE = "https://api.mojang.com";
QString MOJANG_SESSION_BASE = "https://sessionserver.mojang.com";
QString MOJANG_SERVICES_BASE = "https://api.minecraftservices.com";
+ QString RESOURCE_BASE = "https://resources.download.minecraft.net/";
+ QString LIBRARY_BASE = "https://libraries.minecraft.net/";
QString IMGUR_BASE_URL = "https://api.imgur.com/3/";
QString FMLLIBS_BASE_URL = "https://files.prismlauncher.org/fmllibs/"; // FIXME: move into CMakeLists
QString TRANSLATIONS_BASE_URL = "https://i18n.prismlauncher.org/"; // FIXME: move into CMakeLists
diff --git a/cmake/MacOSXBundleInfo.plist.in b/cmake/MacOSXBundleInfo.plist.in
index d36ac3e8f..c439efe25 100644
--- a/cmake/MacOSXBundleInfo.plist.in
+++ b/cmake/MacOSXBundleInfo.plist.in
@@ -6,6 +6,8 @@
A Minecraft mod wants to access your camera.
NSMicrophoneUsageDescription
A Minecraft mod wants to access your microphone.
+ NSDownloadsFolderUsageDescription
+ Prism uses access to your Downloads folder to help you more quickly add mods that can't be automatically downloaded to your instance. You can change where Prism scans for downloaded mods in Settings or the prompt that appears.
NSPrincipalClass
NSApplication
NSHighResolutionCapable
diff --git a/flatpak/org.unmojang.FjordLauncher.yml b/flatpak/org.unmojang.FjordLauncher.yml
index bbadf2991..e2a7b31a0 100644
--- a/flatpak/org.unmojang.FjordLauncher.yml
+++ b/flatpak/org.unmojang.FjordLauncher.yml
@@ -67,7 +67,8 @@ modules:
config-opts:
- -DCMAKE_BUILD_TYPE=Release
- -DBUILD_SHARED_LIBS:BOOL=ON
- - -DGLFW_USE_WAYLAND=ON
+ - -DGLFW_USE_WAYLAND:BOOL=ON
+ - -DGLFW_BUILD_DOCS:BOOL=OFF
sources:
- type: git
url: https://github.com/glfw/glfw.git
diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt
index f88309347..b5ce4dabb 100644
--- a/launcher/CMakeLists.txt
+++ b/launcher/CMakeLists.txt
@@ -855,6 +855,8 @@ SET(LAUNCHER_SOURCES
ui/themes/DarkTheme.h
ui/themes/ITheme.cpp
ui/themes/ITheme.h
+ ui/themes/HintOverrideProxyStyle.cpp
+ ui/themes/HintOverrideProxyStyle.h
ui/themes/SystemTheme.cpp
ui/themes/SystemTheme.h
ui/themes/IconTheme.cpp
diff --git a/launcher/FileSystem.cpp b/launcher/FileSystem.cpp
index 70704e1d3..ff9c90cf7 100644
--- a/launcher/FileSystem.cpp
+++ b/launcher/FileSystem.cpp
@@ -801,25 +801,68 @@ QString NormalizePath(QString path)
}
}
-static const QString BAD_PATH_CHARS = "\"?<>:;*|!+\r\n";
-static const QString BAD_FILENAME_CHARS = BAD_PATH_CHARS + "\\/";
+static const QString BAD_WIN_CHARS = "<>:\"|?*\r\n";
+static const QString BAD_NTFS_CHARS = "<>:\"|?*";
+static const QString BAD_HFS_CHARS = ":";
+
+static const QString BAD_FILENAME_CHARS = BAD_WIN_CHARS + "\\/";
QString RemoveInvalidFilenameChars(QString string, QChar replaceWith)
{
for (int i = 0; i < string.length(); i++)
if (string.at(i) < ' ' || BAD_FILENAME_CHARS.contains(string.at(i)))
string[i] = replaceWith;
-
return string;
}
-QString RemoveInvalidPathChars(QString string, QChar replaceWith)
+QString RemoveInvalidPathChars(QString path, QChar replaceWith)
{
- for (int i = 0; i < string.length(); i++)
- if (string.at(i) < ' ' || BAD_PATH_CHARS.contains(string.at(i)))
- string[i] = replaceWith;
+ QString invalidChars;
+#ifdef Q_OS_WIN
+ invalidChars = BAD_WIN_CHARS;
+#endif
- return string;
+ // the null character is ignored in this check as it was not a problem until now
+ switch (statFS(path).fsType) {
+ case FilesystemType::FAT: // similar to NTFS
+ /* fallthrough */
+ case FilesystemType::NTFS:
+ /* fallthrough */
+ case FilesystemType::REFS: // similar to NTFS(should be available only on windows)
+ invalidChars += BAD_NTFS_CHARS;
+ break;
+ // case FilesystemType::EXT:
+ // case FilesystemType::EXT_2_OLD:
+ // case FilesystemType::EXT_2_3_4:
+ // case FilesystemType::XFS:
+ // case FilesystemType::BTRFS:
+ // case FilesystemType::NFS:
+ // case FilesystemType::ZFS:
+ case FilesystemType::APFS:
+ /* fallthrough */
+ case FilesystemType::HFS:
+ /* fallthrough */
+ case FilesystemType::HFSPLUS:
+ /* fallthrough */
+ case FilesystemType::HFSX:
+ invalidChars += BAD_HFS_CHARS;
+ break;
+ // case FilesystemType::FUSEBLK:
+ // case FilesystemType::F2FS:
+ // case FilesystemType::UNKNOWN:
+ default:
+ break;
+ }
+
+ if (invalidChars.size() != 0) {
+ for (int i = 0; i < path.length(); i++) {
+ if (path.at(i) < ' ' || invalidChars.contains(path.at(i))) {
+ path[i] = replaceWith;
+ }
+ }
+ }
+
+ return path;
}
QString DirNameFromString(QString string, QString inDir)
diff --git a/launcher/FileSystem.h b/launcher/FileSystem.h
index 5496c3795..ea4bb9674 100644
--- a/launcher/FileSystem.h
+++ b/launcher/FileSystem.h
@@ -378,6 +378,7 @@ enum class FilesystemType {
HFSX,
FUSEBLK,
F2FS,
+ BCACHEFS,
UNKNOWN
};
@@ -406,6 +407,7 @@ static const QMap s_filesystem_type_names = { { Fil
{ FilesystemType::HFSX, { "HFSX" } },
{ FilesystemType::FUSEBLK, { "FUSEBLK" } },
{ FilesystemType::F2FS, { "F2FS" } },
+ { FilesystemType::BCACHEFS, { "BCACHEFS" } },
{ FilesystemType::UNKNOWN, { "UNKNOWN" } } };
/**
@@ -458,7 +460,7 @@ QString nearestExistentAncestor(const QString& path);
FilesystemInfo statFS(const QString& path);
static const QList s_clone_filesystems = { FilesystemType::BTRFS, FilesystemType::APFS, FilesystemType::ZFS,
- FilesystemType::XFS, FilesystemType::REFS };
+ FilesystemType::XFS, FilesystemType::REFS, FilesystemType::BCACHEFS };
/**
* @brief if the Filesystem is reflink/clone capable
diff --git a/launcher/LaunchController.cpp b/launcher/LaunchController.cpp
index 9f3c36bf9..61dacb4c6 100644
--- a/launcher/LaunchController.cpp
+++ b/launcher/LaunchController.cpp
@@ -405,7 +405,7 @@ void LaunchController::launchInstance()
online_mode = "online";
// 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 = "";
QHostInfo host_info;
diff --git a/launcher/MMCZip.cpp b/launcher/MMCZip.cpp
index 145dd77ad..5acdbce9c 100644
--- a/launcher/MMCZip.cpp
+++ b/launcher/MMCZip.cpp
@@ -288,9 +288,7 @@ std::optional extractSubDir(QuaZip* zip, const QString& subdir, con
do {
QString file_name = zip->getCurrentFileName();
-#ifdef Q_OS_WIN
file_name = FS::RemoveInvalidPathChars(file_name);
-#endif
if (!file_name.startsWith(subdir))
continue;
diff --git a/launcher/MMCZip.h b/launcher/MMCZip.h
index 43b4ab933..b28eb195c 100644
--- a/launcher/MMCZip.h
+++ b/launcher/MMCZip.h
@@ -154,7 +154,12 @@ bool collectFileListRecursively(const QString& rootDir, const QString& subDir, Q
#if defined(LAUNCHER_APPLICATION)
class ExportToZipTask : public Task {
public:
- ExportToZipTask(QString outputPath, QDir dir, QFileInfoList files, QString destinationPrefix = "", bool followSymlinks = false)
+ ExportToZipTask(QString outputPath,
+ QDir dir,
+ QFileInfoList files,
+ QString destinationPrefix = "",
+ bool followSymlinks = false,
+ bool utf8Enabled = false)
: m_output_path(outputPath)
, m_output(outputPath)
, m_dir(dir)
@@ -163,10 +168,15 @@ class ExportToZipTask : public Task {
, m_follow_symlinks(followSymlinks)
{
setAbortable(true);
- m_output.setUtf8Enabled(true);
+ m_output.setUtf8Enabled(utf8Enabled);
};
- ExportToZipTask(QString outputPath, QString dir, QFileInfoList files, QString destinationPrefix = "", bool followSymlinks = false)
- : ExportToZipTask(outputPath, QDir(dir), files, destinationPrefix, followSymlinks){};
+ ExportToZipTask(QString outputPath,
+ QString dir,
+ QFileInfoList files,
+ QString destinationPrefix = "",
+ bool followSymlinks = false,
+ bool utf8Enabled = false)
+ : ExportToZipTask(outputPath, QDir(dir), files, destinationPrefix, followSymlinks, utf8Enabled){};
virtual ~ExportToZipTask() = default;
diff --git a/launcher/StringUtils.cpp b/launcher/StringUtils.cpp
index 72ccdfbff..edda9f247 100644
--- a/launcher/StringUtils.cpp
+++ b/launcher/StringUtils.cpp
@@ -212,3 +212,25 @@ QPair StringUtils::splitFirst(const QString& s, const QRegular
right = s.mid(end);
return qMakePair(left, right);
}
+
+static const QRegularExpression ulMatcher("<\\s*/\\s*ul\\s*>");
+
+QString StringUtils::htmlListPatch(QString htmlStr)
+{
+ int pos = htmlStr.indexOf(ulMatcher);
+ int imgPos;
+ while (pos != -1) {
+ pos = htmlStr.indexOf(">", pos) + 1; // Get the size of the tag. Add one for zeroeth index
+ imgPos = htmlStr.indexOf("
");
+
+ pos = htmlStr.indexOf(ulMatcher, pos);
+ }
+ return htmlStr;
+}
\ No newline at end of file
diff --git a/launcher/StringUtils.h b/launcher/StringUtils.h
index 9d2bdd85e..624ee41a3 100644
--- a/launcher/StringUtils.h
+++ b/launcher/StringUtils.h
@@ -85,4 +85,6 @@ QPair splitFirst(const QString& s, const QString& sep, Qt::Cas
QPair splitFirst(const QString& s, QChar sep, Qt::CaseSensitivity cs = Qt::CaseSensitive);
QPair splitFirst(const QString& s, const QRegularExpression& re);
+QString htmlListPatch(QString htmlStr);
+
} // namespace StringUtils
diff --git a/launcher/java/JavaUtils.cpp b/launcher/java/JavaUtils.cpp
index 3c39120c1..8ea578010 100644
--- a/launcher/java/JavaUtils.cpp
+++ b/launcher/java/JavaUtils.cpp
@@ -207,7 +207,7 @@ QList JavaUtils::FindJavaFromRegistryKey(DWORD keyType, QString
QString newKeyName = keyName + "\\" + newSubkeyName + subkeySuffix;
HKEY newKey;
- if (RegOpenKeyExW(HKEY_LOCAL_MACHINE, newKeyName.toStdWString().c_str(), 0, KEY_READ | KEY_WOW64_64KEY, &newKey) ==
+ if (RegOpenKeyExW(HKEY_LOCAL_MACHINE, newKeyName.toStdWString().c_str(), 0, KEY_READ | keyType, &newKey) ==
ERROR_SUCCESS) {
// Read the JavaHome value to find where Java is installed.
DWORD valueSz = 0;
@@ -283,6 +283,12 @@ QList JavaUtils::FindJavaPaths()
QList ADOPTIUMJDK64s =
this->FindJavaFromRegistryKey(KEY_WOW64_64KEY, "SOFTWARE\\Eclipse Adoptium\\JDK", "Path", "\\hotspot\\MSI");
+ // IBM Semeru
+ QList SEMERUJRE32s = this->FindJavaFromRegistryKey(KEY_WOW64_32KEY, "SOFTWARE\\Semeru\\JRE", "Path", "\\openj9\\MSI");
+ QList SEMERUJRE64s = this->FindJavaFromRegistryKey(KEY_WOW64_64KEY, "SOFTWARE\\Semeru\\JRE", "Path", "\\openj9\\MSI");
+ QList SEMERUJDK32s = this->FindJavaFromRegistryKey(KEY_WOW64_32KEY, "SOFTWARE\\Semeru\\JDK", "Path", "\\openj9\\MSI");
+ QList SEMERUJDK64s = this->FindJavaFromRegistryKey(KEY_WOW64_64KEY, "SOFTWARE\\Semeru\\JDK", "Path", "\\openj9\\MSI");
+
// Microsoft
QList MICROSOFTJDK64s =
this->FindJavaFromRegistryKey(KEY_WOW64_64KEY, "SOFTWARE\\Microsoft\\JDK", "Path", "\\hotspot\\MSI");
@@ -300,6 +306,7 @@ QList JavaUtils::FindJavaPaths()
java_candidates.append(NEWJRE64s);
java_candidates.append(ADOPTOPENJRE64s);
java_candidates.append(ADOPTIUMJRE64s);
+ java_candidates.append(SEMERUJRE64s);
java_candidates.append(MakeJavaPtr("C:/Program Files/Java/jre8/bin/javaw.exe"));
java_candidates.append(MakeJavaPtr("C:/Program Files/Java/jre7/bin/javaw.exe"));
java_candidates.append(MakeJavaPtr("C:/Program Files/Java/jre6/bin/javaw.exe"));
@@ -308,6 +315,7 @@ QList JavaUtils::FindJavaPaths()
java_candidates.append(ADOPTOPENJDK64s);
java_candidates.append(FOUNDATIONJDK64s);
java_candidates.append(ADOPTIUMJDK64s);
+ java_candidates.append(SEMERUJDK64s);
java_candidates.append(MICROSOFTJDK64s);
java_candidates.append(ZULU64s);
java_candidates.append(LIBERICA64s);
@@ -316,6 +324,7 @@ QList JavaUtils::FindJavaPaths()
java_candidates.append(NEWJRE32s);
java_candidates.append(ADOPTOPENJRE32s);
java_candidates.append(ADOPTIUMJRE32s);
+ java_candidates.append(SEMERUJRE32s);
java_candidates.append(MakeJavaPtr("C:/Program Files (x86)/Java/jre8/bin/javaw.exe"));
java_candidates.append(MakeJavaPtr("C:/Program Files (x86)/Java/jre7/bin/javaw.exe"));
java_candidates.append(MakeJavaPtr("C:/Program Files (x86)/Java/jre6/bin/javaw.exe"));
@@ -324,6 +333,7 @@ QList JavaUtils::FindJavaPaths()
java_candidates.append(ADOPTOPENJDK32s);
java_candidates.append(FOUNDATIONJDK32s);
java_candidates.append(ADOPTIUMJDK32s);
+ java_candidates.append(SEMERUJDK32s);
java_candidates.append(ZULU32s);
java_candidates.append(LIBERICA32s);
@@ -362,6 +372,12 @@ QList JavaUtils::FindJavaPaths()
javas.append(systemLibraryJVMDir.absolutePath() + "/" + java + "/Contents/Home/bin/java");
javas.append(systemLibraryJVMDir.absolutePath() + "/" + java + "/Contents/Commands/java");
}
+
+ auto home = qEnvironmentVariable("HOME");
+
+ // javas downloaded by sdkman
+ javas.append(FS::PathCombine(home, ".sdkman/candidates/java"));
+
javas.append(getMinecraftJavaBundle());
javas = addJavasFromEnv(javas);
javas.removeDuplicates();
@@ -404,6 +420,7 @@ QList JavaUtils::FindJavaPaths()
// manually installed JDKs in /opt
scanJavaDirs("/opt/jdk");
scanJavaDirs("/opt/jdks");
+ scanJavaDirs("/opt/ibm"); // IBM Semeru Certified Edition
// flatpak
scanJavaDirs("/app/jdk");
@@ -449,13 +466,13 @@ QStringList getMinecraftJavaBundle()
executable += "w.exe";
auto appDataPath = QProcessEnvironment::systemEnvironment().value("APPDATA", "");
- processpaths << FS::PathCombine(QFileInfo(appDataPath).absolutePath(), ".minecraft", "runtime");
+ processpaths << FS::PathCombine(QFileInfo(appDataPath).absoluteFilePath(), ".minecraft", "runtime");
// add the microsoft store version of the launcher to the search. the current path is:
// C:\Users\USERNAME\AppData\Local\Packages\Microsoft.4297127D64EC6_8wekyb3d8bbwe\LocalCache\Local\runtime
auto localAppDataPath = QProcessEnvironment::systemEnvironment().value("LOCALAPPDATA", "");
auto minecraftMSStorePath =
- FS::PathCombine(QFileInfo(localAppDataPath).absolutePath(), "Packages", "Microsoft.4297127D64EC6_8wekyb3d8bbwe");
+ FS::PathCombine(QFileInfo(localAppDataPath).absoluteFilePath(), "Packages", "Microsoft.4297127D64EC6_8wekyb3d8bbwe");
processpaths << FS::PathCombine(minecraftMSStorePath, "LocalCache", "Local", "runtime");
#else
processpaths << FS::PathCombine(QDir::homePath(), ".minecraft", "runtime");
diff --git a/launcher/minecraft/Library.cpp b/launcher/minecraft/Library.cpp
index 0e8ddf03d..b4e5a6095 100644
--- a/launcher/minecraft/Library.cpp
+++ b/launcher/minecraft/Library.cpp
@@ -50,6 +50,7 @@ void Library::getApplicableFiles(const RuntimeContext& runtimeContext,
{
bool local = isLocal();
auto actualPath = [&](QString relPath) {
+ relPath = FS::RemoveInvalidPathChars(relPath);
QFileInfo out(FS::PathCombine(storagePrefix(), relPath));
if (local && !overridePath.isEmpty()) {
QString fileName = out.fileName();
diff --git a/launcher/minecraft/MinecraftInstance.cpp b/launcher/minecraft/MinecraftInstance.cpp
index 4e9f78945..4809cdc06 100644
--- a/launcher/minecraft/MinecraftInstance.cpp
+++ b/launcher/minecraft/MinecraftInstance.cpp
@@ -1040,7 +1040,7 @@ QString MinecraftInstance::getStatusbarDescription()
QString description;
description.append(tr("Minecraft %1").arg(mcVersion));
if (m_settings->get("ShowGameTime").toBool()) {
- if (lastTimePlayed() > 0) {
+ if (lastTimePlayed() > 0 && lastLaunch() > 0) {
QDateTime lastLaunchTime = QDateTime::fromMSecsSinceEpoch(lastLaunch());
description.append(
tr(", last played on %1 for %2")
diff --git a/launcher/minecraft/mod/DataPack.cpp b/launcher/minecraft/mod/DataPack.cpp
index fc2d3f68b..64c687c03 100644
--- a/launcher/minecraft/mod/DataPack.cpp
+++ b/launcher/minecraft/mod/DataPack.cpp
@@ -28,15 +28,52 @@
#include "Version.h"
// Values taken from:
-// https://minecraft.wiki/w/Tutorials/Creating_a_data_pack#%22pack_format%22
-static const QMap> s_pack_format_versions = {
- { 4, { Version("1.13"), Version("1.14.4") } }, { 5, { Version("1.15"), Version("1.16.1") } },
- { 6, { Version("1.16.2"), Version("1.16.5") } }, { 7, { Version("1.17"), Version("1.17.1") } },
- { 8, { Version("1.18"), Version("1.18.1") } }, { 9, { Version("1.18.2"), Version("1.18.2") } },
- { 10, { Version("1.19"), Version("1.19.3") } }, { 11, { Version("23w03a"), Version("23w05a") } },
- { 12, { Version("1.19.4"), Version("1.19.4") } }, { 13, { Version("23w12a"), Version("23w14a") } },
- { 14, { Version("23w16a"), Version("23w17a") } }, { 15, { Version("1.20"), Version("1.20") } },
-};
+// https://minecraft.wiki/w/Pack_format#List_of_data_pack_formats
+static const QMap> s_pack_format_versions = { { 4, { Version("1.13"), Version("1.14.4") } },
+ { 5, { Version("1.15"), Version("1.16.1") } },
+ { 6, { Version("1.16.2"), Version("1.16.5") } },
+ { 7, { Version("1.17"), Version("1.17.1") } },
+ { 8, { Version("1.18"), Version("1.18.1") } },
+ { 9, { Version("1.18.2"), Version("1.18.2") } },
+ { 10, { Version("1.19"), Version("1.19.3") } },
+ { 11, { Version("23w03a"), Version("23w05a") } },
+ { 12, { Version("1.19.4"), Version("1.19.4") } },
+ { 13, { Version("23w12a"), Version("23w14a") } },
+ { 14, { Version("23w16a"), Version("23w17a") } },
+ { 15, { Version("1.20"), Version("1.20.1") } },
+ { 16, { Version("23w31a"), Version("23w31a") } },
+ { 17, { Version("23w32a"), Version("23w35a") } },
+ { 18, { Version("1.20.2"), Version("1.20.2") } },
+ { 19, { Version("23w40a"), Version("23w40a") } },
+ { 20, { Version("23w41a"), Version("23w41a") } },
+ { 21, { Version("23w42a"), Version("23w42a") } },
+ { 22, { Version("23w43a"), Version("23w43b") } },
+ { 23, { Version("23w44a"), Version("23w44a") } },
+ { 24, { Version("23w45a"), Version("23w45a") } },
+ { 25, { Version("23w46a"), Version("23w46a") } },
+ { 26, { Version("1.20.3"), Version("1.20.4") } },
+ { 27, { Version("23w51a"), Version("23w51b") } },
+ { 28, { Version("24w05a"), Version("24w05b") } },
+ { 29, { Version("24w04a"), Version("24w04a") } },
+ { 30, { Version("24w05a"), Version("24w05b") } },
+ { 31, { Version("24w06a"), Version("24w06a") } },
+ { 32, { Version("24w07a"), Version("24w07a") } },
+ { 33, { Version("24w09a"), Version("24w09a") } },
+ { 34, { Version("24w10a"), Version("24w10a") } },
+ { 35, { Version("24w11a"), Version("24w11a") } },
+ { 36, { Version("24w12a"), Version("24w12a") } },
+ { 37, { Version("24w13a"), Version("24w13a") } },
+ { 38, { Version("24w14a"), Version("24w14a") } },
+ { 39, { Version("1.20.5-pre1"), Version("1.20.5-pre1") } },
+ { 40, { Version("1.20.5-pre2"), Version("1.20.5-pre2") } },
+ { 41, { Version("1.20.5"), Version("1.20.6") } },
+ { 42, { Version("24w18a"), Version("24w18a") } },
+ { 43, { Version("24w19a"), Version("24w19b") } },
+ { 44, { Version("24w20a"), Version("24w20a") } },
+ { 45, { Version("21w21a"), Version("21w21b") } },
+ { 46, { Version("1.21-pre1"), Version("1.21-pre1") } },
+ { 47, { Version("1.21-pre2"), Version("1.21-pre2") } },
+ { 48, { Version("1.21"), Version("1.21") } } };
void DataPack::setPackFormat(int new_format_id)
{
diff --git a/launcher/minecraft/mod/ResourcePack.cpp b/launcher/minecraft/mod/ResourcePack.cpp
index 074534405..e885a3117 100644
--- a/launcher/minecraft/mod/ResourcePack.cpp
+++ b/launcher/minecraft/mod/ResourcePack.cpp
@@ -11,15 +11,24 @@
#include "minecraft/mod/tasks/LocalResourcePackParseTask.h"
// Values taken from:
-// https://minecraft.wiki/w/Tutorials/Creating_a_resource_pack#Formatting_pack.mcmeta
+// https://minecraft.wiki/w/Pack_format#List_of_resource_pack_formats
static const QMap> s_pack_format_versions = {
- { 1, { Version("1.6.1"), Version("1.8.9") } }, { 2, { Version("1.9"), Version("1.10.2") } },
- { 3, { Version("1.11"), Version("1.12.2") } }, { 4, { Version("1.13"), Version("1.14.4") } },
- { 5, { Version("1.15"), Version("1.16.1") } }, { 6, { Version("1.16.2"), Version("1.16.5") } },
- { 7, { Version("1.17"), Version("1.17.1") } }, { 8, { Version("1.18"), Version("1.18.2") } },
- { 9, { Version("1.19"), Version("1.19.2") } }, { 11, { Version("22w42a"), Version("22w44a") } },
- { 12, { Version("1.19.3"), Version("1.19.3") } }, { 13, { Version("1.19.4"), Version("1.19.4") } },
- { 14, { Version("1.20"), Version("1.20") } }
+ { 1, { Version("1.6.1"), Version("1.8.9") } }, { 2, { Version("1.9"), Version("1.10.2") } },
+ { 3, { Version("1.11"), Version("1.12.2") } }, { 4, { Version("1.13"), Version("1.14.4") } },
+ { 5, { Version("1.15"), Version("1.16.1") } }, { 6, { Version("1.16.2"), Version("1.16.5") } },
+ { 7, { Version("1.17"), Version("1.17.1") } }, { 8, { Version("1.18"), Version("1.18.2") } },
+ { 9, { Version("1.19"), Version("1.19.2") } }, { 11, { Version("22w42a"), Version("22w44a") } },
+ { 12, { Version("1.19.3"), Version("1.19.3") } }, { 13, { Version("1.19.4"), Version("1.19.4") } },
+ { 14, { Version("23w14a"), Version("23w16a") } }, { 15, { Version("1.20"), Version("1.20.1") } },
+ { 16, { Version("23w31a"), Version("23w31a") } }, { 17, { Version("23w32a"), Version("23w35a") } },
+ { 18, { Version("1.20.2"), Version("23w16a") } }, { 19, { Version("23w42a"), Version("23w42a") } },
+ { 20, { Version("23w43a"), Version("23w44a") } }, { 21, { Version("23w45a"), Version("23w46a") } },
+ { 22, { Version("1.20.3-pre1"), Version("23w51b") } }, { 24, { Version("24w03a"), Version("24w04a") } },
+ { 25, { Version("24w05a"), Version("24w05b") } }, { 26, { Version("24w06a"), Version("24w07a") } },
+ { 28, { Version("24w09a"), Version("24w10a") } }, { 29, { Version("24w11a"), Version("24w11a") } },
+ { 30, { Version("24w12a"), Version("23w12a") } }, { 31, { Version("24w13a"), Version("1.20.5-pre3") } },
+ { 32, { Version("1.20.5-pre4"), Version("1.20.6") } }, { 33, { Version("24w18a"), Version("24w20a") } },
+ { 34, { Version("24w21a"), Version("1.21") } }
};
void ResourcePack::setPackFormat(int new_format_id)
diff --git a/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp b/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp
index 8ae8145de..01de88b04 100644
--- a/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp
+++ b/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp
@@ -1031,6 +1031,12 @@ void PackInstallTask::install()
return;
components->setComponentVersion("net.minecraftforge", version);
+ } else if (m_version.loader.type == QString("neoforge")) {
+ auto version = getVersionForLoader("net.neoforged");
+ if (version == Q_NULLPTR)
+ return;
+
+ components->setComponentVersion("net.neoforged", version);
} else if (m_version.loader.type == QString("fabric")) {
auto version = getVersionForLoader("net.fabricmc.fabric-loader");
if (version == Q_NULLPTR)
diff --git a/launcher/modplatform/flame/FlameInstanceCreationTask.cpp b/launcher/modplatform/flame/FlameInstanceCreationTask.cpp
index ef552c3c2..003203879 100644
--- a/launcher/modplatform/flame/FlameInstanceCreationTask.cpp
+++ b/launcher/modplatform/flame/FlameInstanceCreationTask.cpp
@@ -537,7 +537,10 @@ void FlameCreationTask::setupDownloadJob(QEventLoop& loop)
selectedOptionalMods = optionalModDialog.getResult();
}
for (const auto& result : results) {
- auto relpath = FS::PathCombine(result.targetFolder, result.fileName);
+ auto fileName = result.fileName;
+ fileName = FS::RemoveInvalidPathChars(fileName);
+ auto relpath = FS::PathCombine(result.targetFolder, fileName);
+
if (!result.required && !selectedOptionalMods.contains(relpath)) {
relpath += ".disabled";
}
diff --git a/launcher/modplatform/flame/FlameModIndex.cpp b/launcher/modplatform/flame/FlameModIndex.cpp
index 345883c17..1ca9237f4 100644
--- a/launcher/modplatform/flame/FlameModIndex.cpp
+++ b/launcher/modplatform/flame/FlameModIndex.cpp
@@ -1,5 +1,6 @@
#include "FlameModIndex.h"
+#include "FileSystem.h"
#include "Json.h"
#include "minecraft/MinecraftInstance.h"
#include "minecraft/PackProfile.h"
@@ -138,6 +139,7 @@ auto FlameMod::loadIndexedPackVersion(QJsonObject& obj, bool load_changelog) ->
file.version = Json::requireString(obj, "displayName");
file.downloadUrl = Json::ensureString(obj, "downloadUrl");
file.fileName = Json::requireString(obj, "fileName");
+ file.fileName = FS::RemoveInvalidPathChars(file.fileName);
ModPlatform::IndexedVersionType::VersionType ver_type;
switch (Json::requireInteger(obj, "releaseType")) {
diff --git a/launcher/modplatform/flame/FlamePackExportTask.cpp b/launcher/modplatform/flame/FlamePackExportTask.cpp
index 3a2028fd1..569181732 100644
--- a/launcher/modplatform/flame/FlamePackExportTask.cpp
+++ b/launcher/modplatform/flame/FlamePackExportTask.cpp
@@ -201,7 +201,7 @@ void FlamePackExportTask::makeApiRequest()
<< " reason: " << parseError.errorString();
qWarning() << *response;
- failed(parseError.errorString());
+ emitFailed(parseError.errorString());
return;
}
@@ -213,6 +213,7 @@ void FlamePackExportTask::makeApiRequest()
if (dataArr.isEmpty()) {
qWarning() << "No matches found for fingerprint search!";
+ getProjectsInfo();
return;
}
for (auto match : dataArr) {
@@ -243,9 +244,9 @@ void FlamePackExportTask::makeApiRequest()
qDebug() << doc;
}
pendingHashes.clear();
+ getProjectsInfo();
});
- connect(task.get(), &Task::finished, this, &FlamePackExportTask::getProjectsInfo);
- connect(task.get(), &NetJob::failed, this, &FlamePackExportTask::emitFailed);
+ connect(task.get(), &NetJob::failed, this, &FlamePackExportTask::getProjectsInfo);
task->start();
}
@@ -279,7 +280,7 @@ void FlamePackExportTask::getProjectsInfo()
qWarning() << "Error while parsing JSON response from CurseForge projects task at " << parseError.offset
<< " reason: " << parseError.errorString();
qWarning() << *response;
- failed(parseError.errorString());
+ emitFailed(parseError.errorString());
return;
}
@@ -333,7 +334,7 @@ void FlamePackExportTask::buildZip()
setStatus(tr("Adding files..."));
setProgress(4, 5);
- auto zipTask = makeShared(output, gameRoot, files, "overrides/", true);
+ auto zipTask = makeShared(output, gameRoot, files, "overrides/", true, false);
zipTask->addExtraFile("manifest.json", generateIndex());
zipTask->addExtraFile("modlist.html", generateHTML());
diff --git a/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.cpp b/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.cpp
index 96d9c84d2..c55581b79 100644
--- a/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.cpp
+++ b/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.cpp
@@ -238,11 +238,13 @@ bool ModrinthCreationTask::createInstance()
auto root_modpack_url = QUrl::fromLocalFile(root_modpack_path);
for (auto file : m_files) {
- auto file_path = FS::PathCombine(root_modpack_path, file.path);
+ auto fileName = file.path;
+ fileName = FS::RemoveInvalidPathChars(fileName);
+ auto file_path = FS::PathCombine(root_modpack_path, fileName);
if (!root_modpack_url.isParentOf(QUrl::fromLocalFile(file_path))) {
// This means we somehow got out of the root folder, so abort here to prevent exploits
setError(tr("One of the files has a path that leads to an arbitrary location (%1). This is a security risk and isn't allowed.")
- .arg(file.path));
+ .arg(fileName));
return false;
}
diff --git a/launcher/modplatform/modrinth/ModrinthPackExportTask.cpp b/launcher/modplatform/modrinth/ModrinthPackExportTask.cpp
index c704708ad..7e52153b9 100644
--- a/launcher/modplatform/modrinth/ModrinthPackExportTask.cpp
+++ b/launcher/modplatform/modrinth/ModrinthPackExportTask.cpp
@@ -200,7 +200,7 @@ void ModrinthPackExportTask::buildZip()
{
setStatus(tr("Adding files..."));
- auto zipTask = makeShared(output, gameRoot, files, "overrides/", true);
+ auto zipTask = makeShared(output, gameRoot, files, "overrides/", true, true);
zipTask->addExtraFile("modrinth.index.json", generateIndex());
zipTask->setExcludeFiles(resolvedFiles.keys());
diff --git a/launcher/modplatform/modrinth/ModrinthPackIndex.cpp b/launcher/modplatform/modrinth/ModrinthPackIndex.cpp
index c1c30ab5f..6c3df0902 100644
--- a/launcher/modplatform/modrinth/ModrinthPackIndex.cpp
+++ b/launcher/modplatform/modrinth/ModrinthPackIndex.cpp
@@ -17,6 +17,7 @@
*/
#include "ModrinthPackIndex.h"
+#include "FileSystem.h"
#include "ModrinthAPI.h"
#include "Json.h"
@@ -226,6 +227,7 @@ auto Modrinth::loadIndexedPackVersion(QJsonObject& obj, QString preferred_hash_t
if (parent.contains("url")) {
file.downloadUrl = Json::requireString(parent, "url");
file.fileName = Json::requireString(parent, "filename");
+ file.fileName = FS::RemoveInvalidPathChars(file.fileName);
file.is_preferred = Json::requireBoolean(parent, "primary") || (files.count() == 1);
auto hash_list = Json::requireObject(parent, "hashes");
diff --git a/launcher/modplatform/technic/TechnicPackProcessor.cpp b/launcher/modplatform/technic/TechnicPackProcessor.cpp
index 3b9424bf8..a548820e8 100644
--- a/launcher/modplatform/technic/TechnicPackProcessor.cpp
+++ b/launcher/modplatform/technic/TechnicPackProcessor.cpp
@@ -83,8 +83,10 @@ void Technic::TechnicPackProcessor::run(SettingsObjectPtr globalSettings,
data = file.readAll();
file.close();
} else {
- if (minecraftVersion.isEmpty())
+ if (minecraftVersion.isEmpty()) {
emit failed(tr("Could not find \"version.json\" inside \"bin/modpack.jar\", but Minecraft version is unknown"));
+ return;
+ }
components->setComponentVersion("net.minecraft", minecraftVersion, true);
components->installJarMods({ modpackJar });
@@ -131,7 +133,9 @@ void Technic::TechnicPackProcessor::run(SettingsObjectPtr globalSettings,
file.close();
} else {
// This is the "Vanilla" modpack, excluded by the search code
- emit failed(tr("Unable to find a \"version.json\"!"));
+ components->setComponentVersion("net.minecraft", minecraftVersion, true);
+ components->saveNow();
+ emit succeeded();
return;
}
@@ -155,8 +159,26 @@ void Technic::TechnicPackProcessor::run(SettingsObjectPtr globalSettings,
auto libraryObject = Json::ensureObject(library, {}, "");
auto libraryName = Json::ensureString(libraryObject, "name", "", "");
- if ((libraryName.startsWith("net.minecraftforge:forge:") || libraryName.startsWith("net.minecraftforge:fmlloader:")) &&
- libraryName.contains('-')) {
+ if (libraryName.startsWith("net.neoforged.fancymodloader:")) { // it is neoforge
+ // no easy way to get the version from the libs so use the arguments
+ auto arguments = Json::ensureObject(root, "arguments", {});
+ bool isVersionArg = false;
+ QString neoforgeVersion;
+ for (auto arg : Json::ensureArray(arguments, "game", {})) {
+ auto argument = Json::ensureString(arg, "");
+ if (isVersionArg) {
+ neoforgeVersion = argument;
+ break;
+ } else {
+ isVersionArg = "--fml.neoForgeVersion" == argument || "--fml.forgeVersion" == argument;
+ }
+ }
+ if (!neoforgeVersion.isEmpty()) {
+ components->setComponentVersion("net.neoforged", neoforgeVersion);
+ }
+ break;
+ } else if ((libraryName.startsWith("net.minecraftforge:forge:") || libraryName.startsWith("net.minecraftforge:fmlloader:")) &&
+ libraryName.contains('-')) {
QString libraryVersion = libraryName.section(':', 2);
if (!libraryVersion.startsWith("1.7.10-")) {
components->setComponentVersion("net.minecraftforge", libraryName.section('-', 1));
@@ -164,6 +186,7 @@ void Technic::TechnicPackProcessor::run(SettingsObjectPtr globalSettings,
// 1.7.10 versions sometimes look like 1.7.10-10.13.4.1614-1.7.10, this filters out the 10.13.4.1614 part
components->setComponentVersion("net.minecraftforge", libraryName.section('-', 1, 1));
}
+ break;
} else {
// ->
static QMap loaderMap{ { "net.minecraftforge:minecraftforge:", "net.minecraftforge" },
diff --git a/launcher/net/HttpMetaCache.cpp b/launcher/net/HttpMetaCache.cpp
index f37bc0bf8..d8b1c7636 100644
--- a/launcher/net/HttpMetaCache.cpp
+++ b/launcher/net/HttpMetaCache.cpp
@@ -84,6 +84,7 @@ auto HttpMetaCache::getEntry(QString base, QString resource_path) -> MetaEntryPt
auto HttpMetaCache::resolveEntry(QString base, QString resource_path, QString expected_etag) -> MetaEntryPtr
{
+ resource_path = FS::RemoveInvalidPathChars(resource_path);
auto entry = getEntry(base, resource_path);
// it's not present? generate a default stale entry
if (!entry) {
diff --git a/launcher/net/NetRequest.cpp b/launcher/net/NetRequest.cpp
index 728c0e077..526fe77a5 100644
--- a/launcher/net/NetRequest.cpp
+++ b/launcher/net/NetRequest.cpp
@@ -68,7 +68,8 @@ void NetRequest::executeTask()
if (getState() == Task::State::AbortedByUser) {
qCWarning(logCat) << getUid().toString() << "Attempt to start an aborted Request:" << m_url.toString();
- emitAborted();
+ emit aborted();
+ emit finished();
return;
}
@@ -85,10 +86,12 @@ void NetRequest::executeTask()
break;
case State::Inactive:
case State::Failed:
- emitFailed();
+ emit failed("Failed to initilize sink");
+ emit finished();
return;
case State::AbortedByUser:
- emitAborted();
+ emit aborted();
+ emit finished();
return;
}
diff --git a/launcher/screenshots/ImgurAlbumCreation.cpp b/launcher/screenshots/ImgurAlbumCreation.cpp
index 7e42ff40c..c63c8b39b 100644
--- a/launcher/screenshots/ImgurAlbumCreation.cpp
+++ b/launcher/screenshots/ImgurAlbumCreation.cpp
@@ -51,7 +51,7 @@
Net::NetRequest::Ptr ImgurAlbumCreation::make(std::shared_ptr output, QList screenshots)
{
auto up = makeShared();
- up->m_url = BuildConfig.IMGUR_BASE_URL + "album.json";
+ up->m_url = BuildConfig.IMGUR_BASE_URL + "album";
up->m_sink.reset(new Sink(output));
up->m_screenshots = screenshots;
return up;
@@ -72,7 +72,7 @@ void ImgurAlbumCreation::init()
qDebug() << "Setting up imgur upload";
auto api_headers = new Net::StaticHeaderProxy(
QList{ { "Content-Type", "application/x-www-form-urlencoded" },
- { "Authorization", QString("Client-ID %1").arg(BuildConfig.IMGUR_CLIENT_ID).toStdString().c_str() },
+ { "Authorization", QString("Client-ID %1").arg(BuildConfig.IMGUR_CLIENT_ID).toUtf8() },
{ "Accept", "application/json" } });
addHeaderProxy(api_headers);
}
diff --git a/launcher/screenshots/ImgurUpload.cpp b/launcher/screenshots/ImgurUpload.cpp
index 15fb043e4..941b92ce6 100644
--- a/launcher/screenshots/ImgurUpload.cpp
+++ b/launcher/screenshots/ImgurUpload.cpp
@@ -50,9 +50,8 @@
void ImgurUpload::init()
{
qDebug() << "Setting up imgur upload";
- auto api_headers = new Net::StaticHeaderProxy(
- QList{ { "Authorization", QString("Client-ID %1").arg(BuildConfig.IMGUR_CLIENT_ID).toStdString().c_str() },
- { "Accept", "application/json" } });
+ auto api_headers = new Net::StaticHeaderProxy(QList{
+ { "Authorization", QString("Client-ID %1").arg(BuildConfig.IMGUR_CLIENT_ID).toUtf8() }, { "Accept", "application/json" } });
addHeaderProxy(api_headers);
}
@@ -70,14 +69,14 @@ QNetworkReply* ImgurUpload::getReply(QNetworkRequest& request)
QHttpPart filePart;
filePart.setBodyDevice(file);
filePart.setHeader(QNetworkRequest::ContentTypeHeader, "image/png");
- filePart.setHeader(QNetworkRequest::ContentDispositionHeader, "form-data; name=\"image\"");
+ filePart.setHeader(QNetworkRequest::ContentDispositionHeader, "form-data; name=\"image\"; filename=\"" + file->fileName() + "\"");
multipart->append(filePart);
QHttpPart typePart;
typePart.setHeader(QNetworkRequest::ContentDispositionHeader, "form-data; name=\"type\"");
typePart.setBody("file");
multipart->append(typePart);
QHttpPart namePart;
- namePart.setHeader(QNetworkRequest::ContentDispositionHeader, "form-data; name=\"name\"");
+ namePart.setHeader(QNetworkRequest::ContentDispositionHeader, "form-data; name=\"title\"");
namePart.setBody(m_fileInfo.baseName().toUtf8());
multipart->append(namePart);
@@ -124,7 +123,7 @@ auto ImgurUpload::Sink::finalize(QNetworkReply&) -> Task::State
Net::NetRequest::Ptr ImgurUpload::make(ScreenShot::Ptr m_shot)
{
auto up = makeShared(m_shot->m_file);
- up->m_url = std::move(BuildConfig.IMGUR_BASE_URL + "upload.json");
+ up->m_url = std::move(BuildConfig.IMGUR_BASE_URL + "image");
up->m_sink.reset(new Sink(m_shot));
return up;
}
diff --git a/launcher/ui/dialogs/AboutDialog.cpp b/launcher/ui/dialogs/AboutDialog.cpp
index 5b40fbd32..d4225f06c 100644
--- a/launcher/ui/dialogs/AboutDialog.cpp
+++ b/launcher/ui/dialogs/AboutDialog.cpp
@@ -38,6 +38,7 @@
#include "Application.h"
#include "BuildConfig.h"
#include "Markdown.h"
+#include "StringUtils.h"
#include "ui_AboutDialog.h"
#include
@@ -152,10 +153,10 @@ AboutDialog::AboutDialog(QWidget* parent) : QDialog(parent), ui(new Ui::AboutDia
setWindowTitle(tr("About %1").arg(launcherName));
QString chtml = getCreditsHtml();
- ui->creditsText->setHtml(chtml);
+ ui->creditsText->setHtml(StringUtils::htmlListPatch(chtml));
QString lhtml = getLicenseHtml();
- ui->licenseText->setHtml(lhtml);
+ ui->licenseText->setHtml(StringUtils::htmlListPatch(lhtml));
ui->urlLabel->setOpenExternalLinks(true);
diff --git a/launcher/ui/dialogs/BlockedModsDialog.cpp b/launcher/ui/dialogs/BlockedModsDialog.cpp
index 7a5a16818..2b415c2d9 100644
--- a/launcher/ui/dialogs/BlockedModsDialog.cpp
+++ b/launcher/ui/dialogs/BlockedModsDialog.cpp
@@ -40,6 +40,7 @@
#include
#include
#include
+#include
BlockedModsDialog::BlockedModsDialog(QWidget* parent, const QString& title, const QString& text, QList& mods, QString hash_type)
: QDialog(parent), ui(new Ui::BlockedModsDialog), m_mods(mods), m_hash_type(hash_type)
@@ -60,8 +61,13 @@ BlockedModsDialog::BlockedModsDialog(QWidget* parent, const QString& title, cons
qDebug() << "[Blocked Mods Dialog] Mods List: " << mods;
- setupWatch();
- scanPaths();
+ // defer setup of file system watchers until after the dialog is shown
+ // this allows OS (namely macOS) permission prompts to show after the relevant dialog appears
+ QTimer::singleShot(0, this, [this] {
+ setupWatch();
+ scanPaths();
+ update();
+ });
this->setWindowTitle(title);
ui->labelDescription->setText(text);
@@ -158,7 +164,8 @@ void BlockedModsDialog::update()
QString watching;
for (auto& dir : m_watcher.directories()) {
- watching += QString("%1
").arg(dir);
+ QUrl fileURL = QUrl::fromLocalFile(dir);
+ watching += QString("%2
").arg(fileURL.toString(), dir);
}
ui->textBrowserWatched->setText(watching);
@@ -194,6 +201,10 @@ void BlockedModsDialog::setupWatch()
void BlockedModsDialog::watchPath(QString path, bool watch_recursive)
{
auto to_watch = QFileInfo(path);
+ if (!to_watch.isReadable()) {
+ qWarning() << "[Blocked Mods Dialog] Failed to add Watch Path (unable to read):" << path;
+ return;
+ }
auto to_watch_path = to_watch.canonicalFilePath();
if (m_watcher.directories().contains(to_watch_path))
return; // don't watch the same path twice (no loops!)
diff --git a/launcher/ui/dialogs/ExportInstanceDialog.cpp b/launcher/ui/dialogs/ExportInstanceDialog.cpp
index 703736d68..9f2b3ac42 100644
--- a/launcher/ui/dialogs/ExportInstanceDialog.cpp
+++ b/launcher/ui/dialogs/ExportInstanceDialog.cpp
@@ -146,7 +146,7 @@ void ExportInstanceDialog::doExport()
return;
}
- auto task = makeShared(output, m_instance->instanceRoot(), files, "", true);
+ auto task = makeShared(output, m_instance->instanceRoot(), files, "", true, true);
connect(task.get(), &Task::failed, this,
[this, output](QString reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->show(); });
diff --git a/launcher/ui/dialogs/ExportToModListDialog.cpp b/launcher/ui/dialogs/ExportToModListDialog.cpp
index a343f555a..7debf0cd8 100644
--- a/launcher/ui/dialogs/ExportToModListDialog.cpp
+++ b/launcher/ui/dialogs/ExportToModListDialog.cpp
@@ -22,6 +22,7 @@
#include
#include "FileSystem.h"
#include "Markdown.h"
+#include "StringUtils.h"
#include "minecraft/MinecraftInstance.h"
#include "minecraft/mod/ModFolderModel.h"
#include "modplatform/helpers/ExportToModList.h"
@@ -143,10 +144,10 @@ void ExportToModListDialog::triggerImp()
case ExportToModList::CUSTOM:
return;
case ExportToModList::HTML:
- ui->resultText->setHtml(txt);
+ ui->resultText->setHtml(StringUtils::htmlListPatch(txt));
break;
case ExportToModList::MARKDOWN:
- ui->resultText->setHtml(markdownToHTML(txt));
+ ui->resultText->setHtml(StringUtils::htmlListPatch(markdownToHTML(txt)));
break;
case ExportToModList::PLAINTXT:
break;
diff --git a/launcher/ui/dialogs/ModUpdateDialog.cpp b/launcher/ui/dialogs/ModUpdateDialog.cpp
index 190638487..c71b9f37c 100644
--- a/launcher/ui/dialogs/ModUpdateDialog.cpp
+++ b/launcher/ui/dialogs/ModUpdateDialog.cpp
@@ -3,6 +3,7 @@
#include "CustomMessageBox.h"
#include "ProgressDialog.h"
#include "ScrollMessageBox.h"
+#include "StringUtils.h"
#include "minecraft/mod/tasks/GetModDependenciesTask.h"
#include "modplatform/ModIndex.h"
#include "modplatform/flame/FlameAPI.h"
@@ -464,7 +465,7 @@ void ModUpdateDialog::appendMod(CheckUpdateTask::UpdatableMod const& info, QStri
break;
}
- changelog_area->setHtml(text);
+ changelog_area->setHtml(StringUtils::htmlListPatch(text));
changelog_area->setOpenExternalLinks(true);
changelog_area->setLineWrapMode(QTextBrowser::LineWrapMode::WidgetWidth);
changelog_area->setVerticalScrollBarPolicy(Qt::ScrollBarPolicy::ScrollBarAsNeeded);
diff --git a/launcher/ui/dialogs/NewInstanceDialog.cpp b/launcher/ui/dialogs/NewInstanceDialog.cpp
index 9992ff20c..6d3199731 100644
--- a/launcher/ui/dialogs/NewInstanceDialog.cpp
+++ b/launcher/ui/dialogs/NewInstanceDialog.cpp
@@ -52,6 +52,7 @@
#include
#include
#include
+#include
#include
#include
@@ -64,6 +65,7 @@
#include "ui/pages/modplatform/modrinth/ModrinthPage.h"
#include "ui/pages/modplatform/technic/TechnicPage.h"
#include "ui/widgets/PageContainer.h"
+
NewInstanceDialog::NewInstanceDialog(const QString& initialGroup,
const QString& url,
const QMap& extra_info,
@@ -125,7 +127,17 @@ NewInstanceDialog::NewInstanceDialog(const QString& initialGroup,
updateDialogState();
- restoreGeometry(QByteArray::fromBase64(APPLICATION->settings()->get("NewInstanceGeometry").toByteArray()));
+ if (APPLICATION->settings()->get("NewInstanceGeometry").isValid()) {
+ restoreGeometry(QByteArray::fromBase64(APPLICATION->settings()->get("NewInstanceGeometry").toByteArray()));
+ } else {
+#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
+ auto screen = parent->screen();
+#else
+ auto screen = QGuiApplication::primaryScreen();
+#endif
+ auto geometry = screen->availableSize();
+ resize(width(), qMin(geometry.height() - 50, 710));
+ }
}
void NewInstanceDialog::reject()
diff --git a/launcher/ui/dialogs/UpdateAvailableDialog.cpp b/launcher/ui/dialogs/UpdateAvailableDialog.cpp
index 5eebe87a3..810a1f089 100644
--- a/launcher/ui/dialogs/UpdateAvailableDialog.cpp
+++ b/launcher/ui/dialogs/UpdateAvailableDialog.cpp
@@ -25,6 +25,7 @@
#include "Application.h"
#include "BuildConfig.h"
#include "Markdown.h"
+#include "StringUtils.h"
#include "ui_UpdateAvailableDialog.h"
UpdateAvailableDialog::UpdateAvailableDialog(const QString& currentVersion,
@@ -43,7 +44,7 @@ UpdateAvailableDialog::UpdateAvailableDialog(const QString& currentVersion,
ui->icon->setPixmap(APPLICATION->getThemedIcon("checkupdate").pixmap(64));
auto releaseNotesHtml = markdownToHTML(releaseNotes);
- ui->releaseNotes->setHtml(releaseNotesHtml);
+ ui->releaseNotes->setHtml(StringUtils::htmlListPatch(releaseNotesHtml));
ui->releaseNotes->setOpenExternalLinks(true);
connect(ui->skipButton, &QPushButton::clicked, this, [this]() {
diff --git a/launcher/ui/instanceview/VisualGroup.cpp b/launcher/ui/instanceview/VisualGroup.cpp
index 7bff727fe..83103c502 100644
--- a/launcher/ui/instanceview/VisualGroup.cpp
+++ b/launcher/ui/instanceview/VisualGroup.cpp
@@ -66,6 +66,9 @@ void VisualGroup::update()
rows[currentRow].height = maxRowHeight;
rows[currentRow].top = offsetFromTop;
currentRow++;
+ if (currentRow >= rows.size()) {
+ currentRow = rows.size() - 1;
+ }
offsetFromTop += maxRowHeight + 5;
positionInRow = 0;
maxRowHeight = 0;
diff --git a/launcher/ui/pages/instance/ManagedPackPage.cpp b/launcher/ui/pages/instance/ManagedPackPage.cpp
index 2210d0263..a47403926 100644
--- a/launcher/ui/pages/instance/ManagedPackPage.cpp
+++ b/launcher/ui/pages/instance/ManagedPackPage.cpp
@@ -20,6 +20,7 @@
#include "InstanceTask.h"
#include "Json.h"
#include "Markdown.h"
+#include "StringUtils.h"
#include "modplatform/modrinth/ModrinthPackManifest.h"
@@ -332,7 +333,7 @@ void ModrinthManagedPackPage::suggestVersion()
}
auto version = m_pack.versions.at(index);
- ui->changelogTextBrowser->setHtml(markdownToHTML(version.changelog.toUtf8()));
+ ui->changelogTextBrowser->setHtml(StringUtils::htmlListPatch(markdownToHTML(version.changelog.toUtf8())));
ManagedPackPage::suggestVersion();
}
@@ -420,7 +421,7 @@ void FlameManagedPackPage::parseManagedPack()
"Don't worry though, it will ask you to update this instance instead, so you'll not lose this instance!"
"");
- ui->changelogTextBrowser->setHtml(message);
+ ui->changelogTextBrowser->setHtml(StringUtils::htmlListPatch(message));
return;
}
@@ -502,7 +503,8 @@ void FlameManagedPackPage::suggestVersion()
}
auto version = m_pack.versions.at(index);
- ui->changelogTextBrowser->setHtml(m_api.getModFileChangelog(m_inst->getManagedPackID().toInt(), version.fileId));
+ ui->changelogTextBrowser->setHtml(
+ StringUtils::htmlListPatch(m_api.getModFileChangelog(m_inst->getManagedPackID().toInt(), version.fileId)));
ManagedPackPage::suggestVersion();
}
diff --git a/launcher/ui/pages/modplatform/CustomPage.cpp b/launcher/ui/pages/modplatform/CustomPage.cpp
index 068fb3a36..ba22bd2e6 100644
--- a/launcher/ui/pages/modplatform/CustomPage.cpp
+++ b/launcher/ui/pages/modplatform/CustomPage.cpp
@@ -49,13 +49,11 @@
CustomPage::CustomPage(NewInstanceDialog* dialog, QWidget* parent) : QWidget(parent), dialog(dialog), ui(new Ui::CustomPage)
{
ui->setupUi(this);
- ui->tabWidget->tabBar()->hide();
connect(ui->versionList, &VersionSelectWidget::selectedVersionChanged, this, &CustomPage::setSelectedVersion);
filterChanged();
connect(ui->alphaFilter, &QCheckBox::stateChanged, this, &CustomPage::filterChanged);
connect(ui->betaFilter, &QCheckBox::stateChanged, this, &CustomPage::filterChanged);
connect(ui->snapshotFilter, &QCheckBox::stateChanged, this, &CustomPage::filterChanged);
- connect(ui->oldSnapshotFilter, &QCheckBox::stateChanged, this, &CustomPage::filterChanged);
connect(ui->releaseFilter, &QCheckBox::stateChanged, this, &CustomPage::filterChanged);
connect(ui->experimentsFilter, &QCheckBox::stateChanged, this, &CustomPage::filterChanged);
connect(ui->refreshBtn, &QPushButton::clicked, this, &CustomPage::refresh);
@@ -96,13 +94,11 @@ void CustomPage::filterChanged()
{
QStringList out;
if (ui->alphaFilter->isChecked())
- out << "(old_alpha)";
+ out << "(alpha)";
if (ui->betaFilter->isChecked())
- out << "(old_beta)";
+ out << "(beta)";
if (ui->snapshotFilter->isChecked())
out << "(snapshot)";
- if (ui->oldSnapshotFilter->isChecked())
- out << "(old_snapshot)";
if (ui->releaseFilter->isChecked())
out << "(release)";
if (ui->experimentsFilter->isChecked())
diff --git a/launcher/ui/pages/modplatform/CustomPage.ui b/launcher/ui/pages/modplatform/CustomPage.ui
index 23351ccd4..39d9aa6dc 100644
--- a/launcher/ui/pages/modplatform/CustomPage.ui
+++ b/launcher/ui/pages/modplatform/CustomPage.ui
@@ -24,29 +24,21 @@
0
-
-
-
- 0
+
+
+ true
-
-
-
-
-
-
-
-
-
-
- 0
- 0
-
-
-
- Qt::Horizontal
-
-
-
- -
+
+
+
+ 0
+ 0
+ 813
+ 605
+
+
+
+
-
-
@@ -93,16 +85,6 @@
- -
-
-
- Old Snapshots
-
-
- true
-
-
-
-
@@ -157,7 +139,20 @@
- -
+
-
+
+
+
+ 0
+ 0
+
+
+
+ Qt::Horizontal
+
+
+
+ -
-
@@ -283,10 +278,8 @@
- tabWidget
releaseFilter
snapshotFilter
- oldSnapshotFilter
betaFilter
alphaFilter
experimentsFilter
diff --git a/launcher/ui/pages/modplatform/ResourcePage.cpp b/launcher/ui/pages/modplatform/ResourcePage.cpp
index ae48e5523..b9c706c6c 100644
--- a/launcher/ui/pages/modplatform/ResourcePage.cpp
+++ b/launcher/ui/pages/modplatform/ResourcePage.cpp
@@ -45,6 +45,7 @@
#include "Markdown.h"
+#include "StringUtils.h"
#include "ui/dialogs/ResourceDownloadDialog.h"
#include "ui/pages/modplatform/ResourceModel.h"
#include "ui/widgets/ProjectItem.h"
@@ -234,8 +235,8 @@ void ResourcePage::updateUi()
text += "
";
- m_ui->packDescription->setHtml(
- text + (current_pack->extraData.body.isEmpty() ? current_pack->description : markdownToHTML(current_pack->extraData.body)));
+ m_ui->packDescription->setHtml(StringUtils::htmlListPatch(
+ text + (current_pack->extraData.body.isEmpty() ? current_pack->description : markdownToHTML(current_pack->extraData.body))));
m_ui->packDescription->flush();
}
diff --git a/launcher/ui/pages/modplatform/atlauncher/AtlPage.cpp b/launcher/ui/pages/modplatform/atlauncher/AtlPage.cpp
index e492830c6..d79b7621a 100644
--- a/launcher/ui/pages/modplatform/atlauncher/AtlPage.cpp
+++ b/launcher/ui/pages/modplatform/atlauncher/AtlPage.cpp
@@ -39,6 +39,7 @@
#include "ui_AtlPage.h"
#include "BuildConfig.h"
+#include "StringUtils.h"
#include "AtlUserInteractionSupportImpl.h"
#include "modplatform/atlauncher/ATLPackInstallTask.h"
@@ -144,7 +145,7 @@ void AtlPage::onSelectionChanged(QModelIndex first, [[maybe_unused]] QModelIndex
selected = filterModel->data(first, Qt::UserRole).value();
- ui->packDescription->setHtml(selected.description.replace("\n", "
"));
+ ui->packDescription->setHtml(StringUtils::htmlListPatch(selected.description.replace("\n", "
")));
for (const auto& version : selected.versions) {
ui->versionSelectionBox->addItem(version.version);
diff --git a/launcher/ui/pages/modplatform/flame/FlamePage.cpp b/launcher/ui/pages/modplatform/flame/FlamePage.cpp
index f1fd9b5d8..d3473412a 100644
--- a/launcher/ui/pages/modplatform/flame/FlamePage.cpp
+++ b/launcher/ui/pages/modplatform/flame/FlamePage.cpp
@@ -43,6 +43,7 @@
#include "FlameModel.h"
#include "InstanceImportTask.h"
#include "Json.h"
+#include "StringUtils.h"
#include "modplatform/flame/FlameAPI.h"
#include "ui/dialogs/NewInstanceDialog.h"
#include "ui/widgets/ProjectItem.h"
@@ -292,6 +293,6 @@ void FlamePage::updateUi()
text += "
";
text += api.getModDescription(current.addonId).toUtf8();
- ui->packDescription->setHtml(text + current.description);
+ ui->packDescription->setHtml(StringUtils::htmlListPatch(text + current.description));
ui->packDescription->flush();
}
diff --git a/launcher/ui/pages/modplatform/import_ftb/ImportFTBPage.cpp b/launcher/ui/pages/modplatform/import_ftb/ImportFTBPage.cpp
index ac06f4cdd..db59fe10a 100644
--- a/launcher/ui/pages/modplatform/import_ftb/ImportFTBPage.cpp
+++ b/launcher/ui/pages/modplatform/import_ftb/ImportFTBPage.cpp
@@ -21,6 +21,7 @@
#include "ui_ImportFTBPage.h"
#include
+#include
#include
#include "FileSystem.h"
#include "ListModel.h"
@@ -58,8 +59,8 @@ ImportFTBPage::ImportFTBPage(NewInstanceDialog* dialog, QWidget* parent) : QWidg
connect(ui->searchEdit, &QLineEdit::textChanged, this, &ImportFTBPage::triggerSearch);
connect(ui->browseButton, &QPushButton::clicked, this, [this] {
- auto path = listModel->getPath();
- QString dir = QFileDialog::getExistingDirectory(this, tr("Select FTBApp instances directory"), path, QFileDialog::ShowDirsOnly);
+ QString dir = QFileDialog::getExistingDirectory(this, tr("Select FTBApp instances directory"), listModel->getUserPath(),
+ QFileDialog::ShowDirsOnly);
if (!dir.isEmpty())
listModel->setPath(dir);
});
diff --git a/launcher/ui/pages/modplatform/import_ftb/ListModel.cpp b/launcher/ui/pages/modplatform/import_ftb/ListModel.cpp
index e058937a6..f3c737977 100644
--- a/launcher/ui/pages/modplatform/import_ftb/ListModel.cpp
+++ b/launcher/ui/pages/modplatform/import_ftb/ListModel.cpp
@@ -24,45 +24,76 @@
#include
#include
#include "Application.h"
+#include "Exception.h"
#include "FileSystem.h"
+#include "Json.h"
#include "StringUtils.h"
#include "modplatform/import_ftb/PackHelpers.h"
#include "ui/widgets/ProjectItem.h"
namespace FTBImportAPP {
-QString getStaticPath()
+QString getFTBRoot()
{
- QString partialPath;
+ QString partialPath = QDir::homePath();
#if defined(Q_OS_OSX)
- partialPath = FS::PathCombine(QDir::homePath(), "Library/Application Support");
-#elif defined(Q_OS_WIN32)
- partialPath = QProcessEnvironment::systemEnvironment().value("LOCALAPPDATA", "");
-#else
- partialPath = QDir::homePath();
+ partialPath = FS::PathCombine(partialPath, "Library/Application Support");
#endif
return FS::PathCombine(partialPath, ".ftba");
}
-static const QString FTB_APP_PATH = FS::PathCombine(getStaticPath(), "instances");
+QString getDynamicPath()
+{
+ auto settingsPath = FS::PathCombine(getFTBRoot(), "storage", "settings.json");
+ if (!QFileInfo::exists(settingsPath))
+ settingsPath = FS::PathCombine(getFTBRoot(), "bin", "settings.json");
+ if (!QFileInfo::exists(settingsPath)) {
+ qWarning() << "The ftb app setings doesn't exist.";
+ return {};
+ }
+ try {
+ auto doc = Json::requireDocument(FS::read(settingsPath));
+ return Json::requireString(Json::requireObject(doc), "instanceLocation");
+ } catch (const Exception& e) {
+ qCritical() << "Could not read ftb settings file: " << e.cause();
+ }
+ return {};
+}
+
+ListModel::ListModel(QObject* parent) : QAbstractListModel(parent), m_instances_path(getDynamicPath()) {}
void ListModel::update()
{
beginResetModel();
- modpacks.clear();
+ m_modpacks.clear();
- QString instancesPath = getPath();
- if (auto instancesInfo = QFileInfo(instancesPath); instancesInfo.exists() && instancesInfo.isDir()) {
- QDirIterator directoryIterator(instancesPath, QDir::Dirs | QDir::NoDotAndDotDot | QDir::Readable | QDir::Hidden,
+ auto wasPathAdded = [this](QString path) {
+ for (auto pack : m_modpacks) {
+ if (pack.path == path)
+ return true;
+ }
+ return false;
+ };
+
+ auto scanPath = [this, wasPathAdded](QString path) {
+ if (path.isEmpty())
+ return;
+ if (auto instancesInfo = QFileInfo(path); !instancesInfo.exists() || !instancesInfo.isDir())
+ return;
+ QDirIterator directoryIterator(path, QDir::Dirs | QDir::NoDotAndDotDot | QDir::Readable | QDir::Hidden,
QDirIterator::FollowSymlinks);
while (directoryIterator.hasNext()) {
- auto modpack = parseDirectory(directoryIterator.next());
- if (!modpack.path.isEmpty())
- modpacks.append(modpack);
+ auto currentPath = directoryIterator.next();
+ if (!wasPathAdded(currentPath)) {
+ auto modpack = parseDirectory(currentPath);
+ if (!modpack.path.isEmpty())
+ m_modpacks.append(modpack);
+ }
}
- } else {
- qDebug() << "Couldn't find ftb instances folder: " << instancesPath;
- }
+ };
+
+ scanPath(APPLICATION->settings()->get("FTBAppInstancesPath").toString());
+ scanPath(m_instances_path);
endResetModel();
}
@@ -70,11 +101,11 @@ void ListModel::update()
QVariant ListModel::data(const QModelIndex& index, int role) const
{
int pos = index.row();
- if (pos >= modpacks.size() || pos < 0 || !index.isValid()) {
+ if (pos >= m_modpacks.size() || pos < 0 || !index.isValid()) {
return QVariant();
}
- auto pack = modpacks.at(pos);
+ auto pack = m_modpacks.at(pos);
if (role == Qt::ToolTipRole) {
}
@@ -110,9 +141,9 @@ QVariant ListModel::data(const QModelIndex& index, int role) const
FilterModel::FilterModel(QObject* parent) : QSortFilterProxyModel(parent)
{
- currentSorting = Sorting::ByGameVersion;
- sortings.insert(tr("Sort by Name"), Sorting::ByName);
- sortings.insert(tr("Sort by Game Version"), Sorting::ByGameVersion);
+ m_currentSorting = Sorting::ByGameVersion;
+ m_sortings.insert(tr("Sort by Name"), Sorting::ByName);
+ m_sortings.insert(tr("Sort by Game Version"), Sorting::ByGameVersion);
}
bool FilterModel::lessThan(const QModelIndex& left, const QModelIndex& right) const
@@ -120,12 +151,12 @@ bool FilterModel::lessThan(const QModelIndex& left, const QModelIndex& right) co
Modpack leftPack = sourceModel()->data(left, Qt::UserRole).value();
Modpack rightPack = sourceModel()->data(right, Qt::UserRole).value();
- if (currentSorting == Sorting::ByGameVersion) {
+ if (m_currentSorting == Sorting::ByGameVersion) {
Version lv(leftPack.mcVersion);
Version rv(rightPack.mcVersion);
return lv < rv;
- } else if (currentSorting == Sorting::ByName) {
+ } else if (m_currentSorting == Sorting::ByName) {
return StringUtils::naturalCompare(leftPack.name, rightPack.name, Qt::CaseSensitive) >= 0;
}
@@ -136,39 +167,39 @@ bool FilterModel::lessThan(const QModelIndex& left, const QModelIndex& right) co
bool FilterModel::filterAcceptsRow([[maybe_unused]] int sourceRow, [[maybe_unused]] const QModelIndex& sourceParent) const
{
- if (searchTerm.isEmpty()) {
+ if (m_searchTerm.isEmpty()) {
return true;
}
QModelIndex index = sourceModel()->index(sourceRow, 0, sourceParent);
Modpack pack = sourceModel()->data(index, Qt::UserRole).value();
- return pack.name.contains(searchTerm, Qt::CaseInsensitive);
+ return pack.name.contains(m_searchTerm, Qt::CaseInsensitive);
}
void FilterModel::setSearchTerm(const QString term)
{
- searchTerm = term.trimmed();
+ m_searchTerm = term.trimmed();
invalidate();
}
const QMap FilterModel::getAvailableSortings()
{
- return sortings;
+ return m_sortings;
}
QString FilterModel::translateCurrentSorting()
{
- return sortings.key(currentSorting);
+ return m_sortings.key(m_currentSorting);
}
void FilterModel::setSorting(Sorting s)
{
- currentSorting = s;
+ m_currentSorting = s;
invalidate();
}
FilterModel::Sorting FilterModel::getCurrentSorting()
{
- return currentSorting;
+ return m_currentSorting;
}
void ListModel::setPath(QString path)
{
@@ -176,11 +207,11 @@ void ListModel::setPath(QString path)
update();
}
-QString ListModel::getPath()
+QString ListModel::getUserPath()
{
auto path = APPLICATION->settings()->get("FTBAppInstancesPath").toString();
- if (path.isEmpty() || !QFileInfo(path).exists())
- path = FTB_APP_PATH;
+ if (path.isEmpty())
+ path = m_instances_path;
return path;
}
} // namespace FTBImportAPP
\ No newline at end of file
diff --git a/launcher/ui/pages/modplatform/import_ftb/ListModel.h b/launcher/ui/pages/modplatform/import_ftb/ListModel.h
index ed33a88f3..a842ac8ff 100644
--- a/launcher/ui/pages/modplatform/import_ftb/ListModel.h
+++ b/launcher/ui/pages/modplatform/import_ftb/ListModel.h
@@ -42,28 +42,29 @@ class FilterModel : public QSortFilterProxyModel {
bool lessThan(const QModelIndex& left, const QModelIndex& right) const override;
private:
- QMap sortings;
- Sorting currentSorting;
- QString searchTerm;
+ QMap m_sortings;
+ Sorting m_currentSorting;
+ QString m_searchTerm;
};
class ListModel : public QAbstractListModel {
Q_OBJECT
public:
- ListModel(QObject* parent) : QAbstractListModel(parent) {}
+ ListModel(QObject* parent);
virtual ~ListModel() = default;
- int rowCount(const QModelIndex& parent) const { return modpacks.size(); }
+ int rowCount(const QModelIndex& parent) const { return m_modpacks.size(); }
int columnCount(const QModelIndex& parent) const { return 1; }
QVariant data(const QModelIndex& index, int role) const;
void update();
- QString getPath();
+ QString getUserPath();
void setPath(QString path);
private:
- ModpackList modpacks;
+ ModpackList m_modpacks;
+ const QString m_instances_path;
};
} // namespace FTBImportAPP
\ No newline at end of file
diff --git a/launcher/ui/pages/modplatform/legacy_ftb/Page.cpp b/launcher/ui/pages/modplatform/legacy_ftb/Page.cpp
index 0ecaf4625..a587b5baf 100644
--- a/launcher/ui/pages/modplatform/legacy_ftb/Page.cpp
+++ b/launcher/ui/pages/modplatform/legacy_ftb/Page.cpp
@@ -35,6 +35,7 @@
*/
#include "Page.h"
+#include "StringUtils.h"
#include "ui/widgets/ProjectItem.h"
#include "ui_Page.h"
@@ -260,8 +261,9 @@ void Page::onPackSelectionChanged(Modpack* pack)
{
ui->versionSelectionBox->clear();
if (pack) {
- currentModpackInfo->setHtml("Pack by " + pack->author + "" + "
Minecraft " + pack->mcVersion + "
" + "
" +
- pack->description + "- " + pack->mods.replace(";", "
- ") + "
");
+ currentModpackInfo->setHtml(StringUtils::htmlListPatch("Pack by " + pack->author + "" + "
Minecraft " + pack->mcVersion +
+ "
" + "
" + pack->description + "- " +
+ pack->mods.replace(";", "
- ") + "
"));
bool currentAdded = false;
for (int i = 0; i < pack->oldVersions.size(); i++) {
diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp
index fffa21940..df446da0d 100644
--- a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp
+++ b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp
@@ -44,6 +44,7 @@
#include "InstanceImportTask.h"
#include "Json.h"
#include "Markdown.h"
+#include "StringUtils.h"
#include "ui/widgets/ProjectItem.h"
@@ -303,7 +304,7 @@ void ModrinthPage::updateUI()
text += markdownToHTML(current.extra.body.toUtf8());
- ui->packDescription->setHtml(text + current.description);
+ ui->packDescription->setHtml(StringUtils::htmlListPatch(text + current.description));
ui->packDescription->flush();
}
diff --git a/launcher/ui/pages/modplatform/technic/TechnicPage.cpp b/launcher/ui/pages/modplatform/technic/TechnicPage.cpp
index 6b1ec8cb5..391c10122 100644
--- a/launcher/ui/pages/modplatform/technic/TechnicPage.cpp
+++ b/launcher/ui/pages/modplatform/technic/TechnicPage.cpp
@@ -44,6 +44,7 @@
#include "BuildConfig.h"
#include "Json.h"
+#include "StringUtils.h"
#include "TechnicModel.h"
#include "modplatform/technic/SingleZipPackInstallTask.h"
#include "modplatform/technic/SolderPackInstallTask.h"
@@ -233,7 +234,7 @@ void TechnicPage::metadataLoaded()
text += "
";
- ui->packDescription->setHtml(text + current.description);
+ ui->packDescription->setHtml(StringUtils::htmlListPatch(text + current.description));
// Strip trailing forward-slashes from Solder URL's
if (current.isSolder) {
diff --git a/launcher/ui/themes/HintOverrideProxyStyle.cpp b/launcher/ui/themes/HintOverrideProxyStyle.cpp
new file mode 100644
index 000000000..80e821349
--- /dev/null
+++ b/launcher/ui/themes/HintOverrideProxyStyle.cpp
@@ -0,0 +1,30 @@
+// SPDX-License-Identifier: GPL-3.0-only
+/*
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (C) 2024 TheKodeToad
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+#include "HintOverrideProxyStyle.h"
+
+int HintOverrideProxyStyle::styleHint(QStyle::StyleHint hint,
+ const QStyleOption* option,
+ const QWidget* widget,
+ QStyleHintReturn* returnData) const
+{
+ if (hint == QStyle::SH_ItemView_ActivateItemOnSingleClick)
+ return 0;
+
+ return QProxyStyle::styleHint(hint, option, widget, returnData);
+}
diff --git a/launcher/ui/themes/HintOverrideProxyStyle.h b/launcher/ui/themes/HintOverrideProxyStyle.h
new file mode 100644
index 000000000..09b296018
--- /dev/null
+++ b/launcher/ui/themes/HintOverrideProxyStyle.h
@@ -0,0 +1,34 @@
+// SPDX-License-Identifier: GPL-3.0-only
+/*
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (C) 2024 TheKodeToad
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+#pragma once
+
+#include
+#include
+
+/// Used to override platform-specific behaviours which the launcher does work well with.
+class HintOverrideProxyStyle : public QProxyStyle {
+ Q_OBJECT
+ public:
+ HintOverrideProxyStyle(QStyle* style) : QProxyStyle(style) {}
+
+ int styleHint(QStyle::StyleHint hint,
+ const QStyleOption* option = nullptr,
+ const QWidget* widget = nullptr,
+ QStyleHintReturn* returnData = nullptr) const override;
+};
diff --git a/launcher/ui/themes/ITheme.cpp b/launcher/ui/themes/ITheme.cpp
index 316b0f2ed..046ae16b4 100644
--- a/launcher/ui/themes/ITheme.cpp
+++ b/launcher/ui/themes/ITheme.cpp
@@ -2,6 +2,7 @@
/*
* Prism Launcher - Minecraft Launcher
* Copyright (C) 2022 Tayou
+ * Copyright (C) 2024 TheKodeToad
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -36,12 +37,13 @@
#include
#include
#include "Application.h"
+#include "HintOverrideProxyStyle.h"
#include "rainbow.h"
void ITheme::apply(bool)
{
APPLICATION->setStyleSheet(QString());
- QApplication::setStyle(QStyleFactory::create(qtTheme()));
+ QApplication::setStyle(new HintOverrideProxyStyle(QStyleFactory::create(qtTheme())));
if (hasColorScheme()) {
QApplication::setPalette(colorScheme());
}
diff --git a/launcher/ui/themes/SystemTheme.cpp b/launcher/ui/themes/SystemTheme.cpp
index 7ad144c7a..cefe664db 100644
--- a/launcher/ui/themes/SystemTheme.cpp
+++ b/launcher/ui/themes/SystemTheme.cpp
@@ -2,6 +2,7 @@
/*
* Prism Launcher - Minecraft Launcher
* Copyright (C) 2022 Tayou
+ * Copyright (C) 2024 TheKodeToad
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -37,6 +38,7 @@
#include
#include
#include
+#include "HintOverrideProxyStyle.h"
#include "ThemeManager.h"
SystemTheme::SystemTheme()
@@ -64,8 +66,11 @@ void SystemTheme::apply(bool initial)
{
// See https://github.com/MultiMC/Launcher/issues/1790
// or https://github.com/PrismLauncher/PrismLauncher/issues/490
- if (initial)
+ if (initial) {
+ QApplication::setStyle(new HintOverrideProxyStyle(QStyleFactory::create(qtTheme())));
return;
+ }
+
ITheme::apply(initial);
}
diff --git a/launcher/ui/widgets/VariableSizedImageObject.cpp b/launcher/ui/widgets/VariableSizedImageObject.cpp
index cebf2a5f1..3dd9d5634 100644
--- a/launcher/ui/widgets/VariableSizedImageObject.cpp
+++ b/launcher/ui/widgets/VariableSizedImageObject.cpp
@@ -22,6 +22,7 @@
#include
#include
#include
+#include
#include "Application.h"
@@ -36,6 +37,30 @@ QSizeF VariableSizedImageObject::intrinsicSize(QTextDocument* doc, int posInDocu
auto image = qvariant_cast(format.property(ImageData));
auto size = image.size();
+ if (size.isEmpty()) // can't resize an empty image
+ return { size };
+
+ // calculate the new image size based on the properties
+ int width = 0;
+ int height = 0;
+ auto widthVar = format.property(QTextFormat::ImageWidth);
+ if (widthVar.isValid()) {
+ width = widthVar.toInt();
+ }
+ auto heigthVar = format.property(QTextFormat::ImageHeight);
+ if (heigthVar.isValid()) {
+ height = heigthVar.toInt();
+ }
+ if (width != 0 && height != 0) {
+ size.setWidth(width);
+ size.setHeight(height);
+ } else if (width != 0) {
+ size.setHeight((width * size.height()) / size.width());
+ size.setWidth(width);
+ } else if (height != 0) {
+ size.setWidth((height * size.width()) / size.height());
+ size.setHeight(height);
+ }
// Get the width of the text content to make the image similar sized.
// doc->textWidth() includes the margin, so we need to remove it.
@@ -46,6 +71,7 @@ QSizeF VariableSizedImageObject::intrinsicSize(QTextDocument* doc, int posInDocu
return { size };
}
+
void VariableSizedImageObject::drawObject(QPainter* painter,
const QRectF& rect,
QTextDocument* doc,
@@ -57,7 +83,20 @@ void VariableSizedImageObject::drawObject(QPainter* painter,
if (m_fetching_images.contains(image_url))
return;
- loadImage(doc, image_url, posInDocument);
+ auto meta = std::make_shared();
+ meta->posInDocument = posInDocument;
+ meta->url = image_url;
+
+ auto widthVar = format.property(QTextFormat::ImageWidth);
+ if (widthVar.isValid()) {
+ meta->width = widthVar.toInt();
+ }
+ auto heigthVar = format.property(QTextFormat::ImageHeight);
+ if (heigthVar.isValid()) {
+ meta->height = heigthVar.toInt();
+ }
+
+ loadImage(doc, meta);
return;
}
@@ -72,16 +111,19 @@ void VariableSizedImageObject::flush()
m_fetching_images.clear();
}
-void VariableSizedImageObject::parseImage(QTextDocument* doc, QImage image, int posInDocument)
+void VariableSizedImageObject::parseImage(QTextDocument* doc, std::shared_ptr meta)
{
QTextCursor cursor(doc);
- cursor.setPosition(posInDocument);
+ cursor.setPosition(meta->posInDocument);
cursor.setKeepPositionOnInsert(true);
auto image_char_format = cursor.charFormat();
image_char_format.setObjectType(QTextFormat::ImageObject);
- image_char_format.setProperty(ImageData, image);
+ image_char_format.setProperty(ImageData, meta->image);
+ image_char_format.setProperty(QTextFormat::ImageName, meta->url.toDisplayString());
+ image_char_format.setProperty(QTextFormat::ImageWidth, meta->width);
+ image_char_format.setProperty(QTextFormat::ImageHeight, meta->height);
// Qt doesn't allow us to modify the properties of an existing object in the document.
// So we remove the old one and add the new one with the ImageData property set.
@@ -89,23 +131,24 @@ void VariableSizedImageObject::parseImage(QTextDocument* doc, QImage image, int
cursor.insertText(QString(QChar::ObjectReplacementCharacter), image_char_format);
}
-void VariableSizedImageObject::loadImage(QTextDocument* doc, const QUrl& source, int posInDocument)
+void VariableSizedImageObject::loadImage(QTextDocument* doc, std::shared_ptr meta)
{
- m_fetching_images.insert(source);
+ m_fetching_images.insert(meta->url);
MetaEntryPtr entry = APPLICATION->metacache()->resolveEntry(
m_meta_entry,
- QString("images/%1").arg(QString(QCryptographicHash::hash(source.toEncoded(), QCryptographicHash::Algorithm::Sha1).toHex())));
+ QString("images/%1").arg(QString(QCryptographicHash::hash(meta->url.toEncoded(), QCryptographicHash::Algorithm::Sha1).toHex())));
- auto job = new NetJob(QString("Load Image: %1").arg(source.fileName()), APPLICATION->network());
- job->addNetAction(Net::ApiDownload::makeCached(source, entry));
+ auto job = new NetJob(QString("Load Image: %1").arg(meta->url.fileName()), APPLICATION->network());
+ job->addNetAction(Net::ApiDownload::makeCached(meta->url, entry));
auto full_entry_path = entry->getFullPath();
- auto source_url = source;
- auto loadImage = [this, doc, full_entry_path, source_url, posInDocument](const QImage& image) {
+ auto source_url = meta->url;
+ auto loadImage = [this, doc, full_entry_path, source_url, meta](const QImage& image) {
doc->addResource(QTextDocument::ImageResource, source_url, image);
- parseImage(doc, image, posInDocument);
+ meta->image = image;
+ parseImage(doc, meta);
// This size hack is needed to prevent the content from being laid out in an area smaller
// than the total width available (weird).
diff --git a/launcher/ui/widgets/VariableSizedImageObject.h b/launcher/ui/widgets/VariableSizedImageObject.h
index ca67af0c9..df3ab4f77 100644
--- a/launcher/ui/widgets/VariableSizedImageObject.h
+++ b/launcher/ui/widgets/VariableSizedImageObject.h
@@ -22,6 +22,7 @@
#include
#include
#include
+#include
/** Custom image text object to be used instead of the normal one in ProjectDescriptionPage.
*
@@ -32,6 +33,14 @@ class VariableSizedImageObject final : public QObject, public QTextObjectInterfa
Q_OBJECT
Q_INTERFACES(QTextObjectInterface)
+ struct ImageMetadata {
+ int posInDocument;
+ QUrl url;
+ QImage image;
+ int width;
+ int height;
+ };
+
public:
QSizeF intrinsicSize(QTextDocument* doc, int posInDocument, const QTextFormat& format) override;
void drawObject(QPainter* painter, const QRectF& rect, QTextDocument* doc, int posInDocument, const QTextFormat& format) override;
@@ -49,13 +58,13 @@ class VariableSizedImageObject final : public QObject, public QTextObjectInterfa
private:
/** Adds the image to the document, in the given position.
*/
- void parseImage(QTextDocument* doc, QImage image, int posInDocument);
+ void parseImage(QTextDocument* doc, std::shared_ptr meta);
/** Loads an image from an external source, and adds it to the document.
*
* This uses m_meta_entry to cache the image.
*/
- void loadImage(QTextDocument* doc, const QUrl& source, int posInDocument);
+ void loadImage(QTextDocument* doc, std::shared_ptr meta);
private:
QString m_meta_entry;
diff --git a/launcher/updater/prismupdater/PrismUpdater.cpp b/launcher/updater/prismupdater/PrismUpdater.cpp
index b369fc1c2..6b472c376 100644
--- a/launcher/updater/prismupdater/PrismUpdater.cpp
+++ b/launcher/updater/prismupdater/PrismUpdater.cpp
@@ -1118,7 +1118,6 @@ void PrismUpdaterApp::backupAppDir()
"Qt*.dll",
});
}
- file_list.append("portable.txt");
logUpdate("manifest.txt empty or missing. making best guess at files to back up.");
}
logUpdate(tr("Backing up:\n %1").arg(file_list.join(",\n ")));
diff --git a/launcher/updater/prismupdater/UpdaterDialogs.cpp b/launcher/updater/prismupdater/UpdaterDialogs.cpp
index 395b658db..06dc161b1 100644
--- a/launcher/updater/prismupdater/UpdaterDialogs.cpp
+++ b/launcher/updater/prismupdater/UpdaterDialogs.cpp
@@ -26,6 +26,7 @@
#include
#include "Markdown.h"
+#include "StringUtils.h"
SelectReleaseDialog::SelectReleaseDialog(const Version& current_version, const QList& releases, QWidget* parent)
: QDialog(parent), m_releases(releases), m_currentVersion(current_version), ui(new Ui::SelectReleaseDialog)
@@ -96,7 +97,7 @@ void SelectReleaseDialog::selectionChanged(QTreeWidgetItem* current, QTreeWidget
QString body = markdownToHTML(release.body.toUtf8());
m_selectedRelease = release;
- ui->changelogTextBrowser->setHtml(body);
+ ui->changelogTextBrowser->setHtml(StringUtils::htmlListPatch(body));
}
SelectReleaseAssetDialog::SelectReleaseAssetDialog(const QList& assets, QWidget* parent)
diff --git a/program_info/org.unmojang.FjordLauncher.desktop.in b/program_info/org.unmojang.FjordLauncher.desktop.in
index c06ebb96e..09c509aa1 100755
--- a/program_info/org.unmojang.FjordLauncher.desktop.in
+++ b/program_info/org.unmojang.FjordLauncher.desktop.in
@@ -1,7 +1,7 @@
[Desktop Entry]
Version=1.0
Name=Fjord Launcher
-Comment=A custom launcher for Minecraft that allows you to easily manage multiple installations of Minecraft at once.
+Comment=Discover, manage, and play Minecraft instances
Type=Application
Terminal=false
Exec=@Launcher_APP_BINARY_NAME@ %U