Merge remote-tracking branch 'prismlauncher/release-8.x' into develop

This commit is contained in:
Evan Goode 2024-06-23 11:34:11 -04:00
commit 51da756e19
58 changed files with 563 additions and 192 deletions

View File

@ -179,7 +179,7 @@ set(Launcher_HELP_URL "https://prismlauncher.org/wiki/help-pages/%1" CACHE STRIN
######## Set version numbers ######## ######## Set version numbers ########
set(Launcher_VERSION_MAJOR 8) 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_NAME "${Launcher_VERSION_MAJOR}.${Launcher_VERSION_MINOR}")
set(Launcher_VERSION_NAME4 "${Launcher_VERSION_MAJOR}.${Launcher_VERSION_MINOR}.0.0") set(Launcher_VERSION_NAME4 "${Launcher_VERSION_MAJOR}.${Launcher_VERSION_MINOR}.0.0")

View File

@ -167,15 +167,14 @@ class Config {
QString DISCORD_URL; QString DISCORD_URL;
QString SUBREDDIT_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 // Minecraft expects these without trailing slashes, best to keep that format everywhere
QString MOJANG_AUTH_BASE = "https://authserver.mojang.com"; QString MOJANG_AUTH_BASE = "https://authserver.mojang.com";
QString MOJANG_ACCOUNT_BASE = "https://api.mojang.com"; QString MOJANG_ACCOUNT_BASE = "https://api.mojang.com";
QString MOJANG_SESSION_BASE = "https://sessionserver.mojang.com"; QString MOJANG_SESSION_BASE = "https://sessionserver.mojang.com";
QString MOJANG_SERVICES_BASE = "https://api.minecraftservices.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 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

View File

@ -6,6 +6,8 @@
<string>A Minecraft mod wants to access your camera.</string> <string>A Minecraft mod wants to access your camera.</string>
<key>NSMicrophoneUsageDescription</key> <key>NSMicrophoneUsageDescription</key>
<string>A Minecraft mod wants to access your microphone.</string> <string>A Minecraft mod wants to access your microphone.</string>
<key>NSDownloadsFolderUsageDescription</key>
<string>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.</string>
<key>NSPrincipalClass</key> <key>NSPrincipalClass</key>
<string>NSApplication</string> <string>NSApplication</string>
<key>NSHighResolutionCapable</key> <key>NSHighResolutionCapable</key>

View File

@ -67,7 +67,8 @@ modules:
config-opts: config-opts:
- -DCMAKE_BUILD_TYPE=Release - -DCMAKE_BUILD_TYPE=Release
- -DBUILD_SHARED_LIBS:BOOL=ON - -DBUILD_SHARED_LIBS:BOOL=ON
- -DGLFW_USE_WAYLAND=ON - -DGLFW_USE_WAYLAND:BOOL=ON
- -DGLFW_BUILD_DOCS:BOOL=OFF
sources: sources:
- type: git - type: git
url: https://github.com/glfw/glfw.git url: https://github.com/glfw/glfw.git

View File

@ -855,6 +855,8 @@ SET(LAUNCHER_SOURCES
ui/themes/DarkTheme.h ui/themes/DarkTheme.h
ui/themes/ITheme.cpp ui/themes/ITheme.cpp
ui/themes/ITheme.h ui/themes/ITheme.h
ui/themes/HintOverrideProxyStyle.cpp
ui/themes/HintOverrideProxyStyle.h
ui/themes/SystemTheme.cpp ui/themes/SystemTheme.cpp
ui/themes/SystemTheme.h ui/themes/SystemTheme.h
ui/themes/IconTheme.cpp ui/themes/IconTheme.cpp

View File

@ -801,25 +801,68 @@ QString NormalizePath(QString path)
} }
} }
static const QString BAD_PATH_CHARS = "\"?<>:;*|!+\r\n"; static const QString BAD_WIN_CHARS = "<>:\"|?*\r\n";
static const QString BAD_FILENAME_CHARS = BAD_PATH_CHARS + "\\/"; 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) QString RemoveInvalidFilenameChars(QString string, QChar replaceWith)
{ {
for (int i = 0; i < string.length(); i++) for (int i = 0; i < string.length(); i++)
if (string.at(i) < ' ' || BAD_FILENAME_CHARS.contains(string.at(i))) if (string.at(i) < ' ' || BAD_FILENAME_CHARS.contains(string.at(i)))
string[i] = replaceWith; string[i] = replaceWith;
return string; return string;
} }
QString RemoveInvalidPathChars(QString string, QChar replaceWith) QString RemoveInvalidPathChars(QString path, QChar replaceWith)
{ {
for (int i = 0; i < string.length(); i++) QString invalidChars;
if (string.at(i) < ' ' || BAD_PATH_CHARS.contains(string.at(i))) #ifdef Q_OS_WIN
string[i] = replaceWith; 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) QString DirNameFromString(QString string, QString inDir)

View File

@ -378,6 +378,7 @@ enum class FilesystemType {
HFSX, HFSX,
FUSEBLK, FUSEBLK,
F2FS, F2FS,
BCACHEFS,
UNKNOWN UNKNOWN
}; };
@ -406,6 +407,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 +460,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

@ -405,7 +405,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

@ -288,9 +288,7 @@ std::optional<QStringList> extractSubDir(QuaZip* zip, const QString& subdir, con
do { do {
QString file_name = zip->getCurrentFileName(); QString file_name = zip->getCurrentFileName();
#ifdef Q_OS_WIN
file_name = FS::RemoveInvalidPathChars(file_name); file_name = FS::RemoveInvalidPathChars(file_name);
#endif
if (!file_name.startsWith(subdir)) if (!file_name.startsWith(subdir))
continue; continue;

View File

@ -154,7 +154,12 @@ bool collectFileListRecursively(const QString& rootDir, const QString& subDir, Q
#if defined(LAUNCHER_APPLICATION) #if defined(LAUNCHER_APPLICATION)
class ExportToZipTask : public Task { class ExportToZipTask : public Task {
public: 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_path(outputPath)
, m_output(outputPath) , m_output(outputPath)
, m_dir(dir) , m_dir(dir)
@ -163,10 +168,15 @@ class ExportToZipTask : public Task {
, m_follow_symlinks(followSymlinks) , m_follow_symlinks(followSymlinks)
{ {
setAbortable(true); setAbortable(true);
m_output.setUtf8Enabled(true); m_output.setUtf8Enabled(utf8Enabled);
}; };
ExportToZipTask(QString outputPath, QString dir, QFileInfoList files, QString destinationPrefix = "", bool followSymlinks = false) ExportToZipTask(QString outputPath,
: ExportToZipTask(outputPath, QDir(dir), files, destinationPrefix, followSymlinks){}; QString dir,
QFileInfoList files,
QString destinationPrefix = "",
bool followSymlinks = false,
bool utf8Enabled = false)
: ExportToZipTask(outputPath, QDir(dir), files, destinationPrefix, followSymlinks, utf8Enabled){};
virtual ~ExportToZipTask() = default; virtual ~ExportToZipTask() = default;

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

@ -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);
@ -362,6 +372,12 @@ QList<QString> JavaUtils::FindJavaPaths()
javas.append(systemLibraryJVMDir.absolutePath() + "/" + java + "/Contents/Home/bin/java"); javas.append(systemLibraryJVMDir.absolutePath() + "/" + java + "/Contents/Home/bin/java");
javas.append(systemLibraryJVMDir.absolutePath() + "/" + java + "/Contents/Commands/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.append(getMinecraftJavaBundle());
javas = addJavasFromEnv(javas); javas = addJavasFromEnv(javas);
javas.removeDuplicates(); javas.removeDuplicates();
@ -404,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");
@ -449,13 +466,13 @@ QStringList getMinecraftJavaBundle()
executable += "w.exe"; executable += "w.exe";
auto appDataPath = QProcessEnvironment::systemEnvironment().value("APPDATA", ""); 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: // 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 // C:\Users\USERNAME\AppData\Local\Packages\Microsoft.4297127D64EC6_8wekyb3d8bbwe\LocalCache\Local\runtime
auto localAppDataPath = QProcessEnvironment::systemEnvironment().value("LOCALAPPDATA", ""); auto localAppDataPath = QProcessEnvironment::systemEnvironment().value("LOCALAPPDATA", "");
auto minecraftMSStorePath = 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"); processpaths << FS::PathCombine(minecraftMSStorePath, "LocalCache", "Local", "runtime");
#else #else
processpaths << FS::PathCombine(QDir::homePath(), ".minecraft", "runtime"); processpaths << FS::PathCombine(QDir::homePath(), ".minecraft", "runtime");

View File

@ -50,6 +50,7 @@ void Library::getApplicableFiles(const RuntimeContext& runtimeContext,
{ {
bool local = isLocal(); bool local = isLocal();
auto actualPath = [&](QString relPath) { auto actualPath = [&](QString relPath) {
relPath = FS::RemoveInvalidPathChars(relPath);
QFileInfo out(FS::PathCombine(storagePrefix(), relPath)); QFileInfo out(FS::PathCombine(storagePrefix(), relPath));
if (local && !overridePath.isEmpty()) { if (local && !overridePath.isEmpty()) {
QString fileName = out.fileName(); QString fileName = out.fileName();

View File

@ -1040,7 +1040,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

@ -28,15 +28,52 @@
#include "Version.h" #include "Version.h"
// Values taken from: // Values taken from:
// https://minecraft.wiki/w/Tutorials/Creating_a_data_pack#%22pack_format%22 // https://minecraft.wiki/w/Pack_format#List_of_data_pack_formats
static const QMap<int, std::pair<Version, Version>> s_pack_format_versions = { static const QMap<int, std::pair<Version, Version>> s_pack_format_versions = { { 4, { Version("1.13"), Version("1.14.4") } },
{ 4, { Version("1.13"), Version("1.14.4") } }, { 5, { Version("1.15"), Version("1.16.1") } }, { 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") } }, { 6, { Version("1.16.2"), Version("1.16.5") } },
{ 8, { Version("1.18"), Version("1.18.1") } }, { 9, { Version("1.18.2"), Version("1.18.2") } }, { 7, { Version("1.17"), Version("1.17.1") } },
{ 10, { Version("1.19"), Version("1.19.3") } }, { 11, { Version("23w03a"), Version("23w05a") } }, { 8, { Version("1.18"), Version("1.18.1") } },
{ 12, { Version("1.19.4"), Version("1.19.4") } }, { 13, { Version("23w12a"), Version("23w14a") } }, { 9, { Version("1.18.2"), Version("1.18.2") } },
{ 14, { Version("23w16a"), Version("23w17a") } }, { 15, { Version("1.20"), Version("1.20") } }, { 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) void DataPack::setPackFormat(int new_format_id)
{ {

View File

@ -11,7 +11,7 @@
#include "minecraft/mod/tasks/LocalResourcePackParseTask.h" #include "minecraft/mod/tasks/LocalResourcePackParseTask.h"
// Values taken from: // 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<int, std::pair<Version, Version>> s_pack_format_versions = { static const QMap<int, std::pair<Version, Version>> s_pack_format_versions = {
{ 1, { Version("1.6.1"), Version("1.8.9") } }, { 2, { Version("1.9"), Version("1.10.2") } }, { 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") } }, { 3, { Version("1.11"), Version("1.12.2") } }, { 4, { Version("1.13"), Version("1.14.4") } },
@ -19,7 +19,16 @@ static const QMap<int, std::pair<Version, Version>> s_pack_format_versions = {
{ 7, { Version("1.17"), Version("1.17.1") } }, { 8, { Version("1.18"), Version("1.18.2") } }, { 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") } }, { 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") } }, { 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") } } { 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) void ResourcePack::setPackFormat(int new_format_id)

View File

@ -1031,6 +1031,12 @@ void PackInstallTask::install()
return; return;
components->setComponentVersion("net.minecraftforge", version); 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")) { } else if (m_version.loader.type == QString("fabric")) {
auto version = getVersionForLoader("net.fabricmc.fabric-loader"); auto version = getVersionForLoader("net.fabricmc.fabric-loader");
if (version == Q_NULLPTR) if (version == Q_NULLPTR)

View File

@ -537,7 +537,10 @@ void FlameCreationTask::setupDownloadJob(QEventLoop& loop)
selectedOptionalMods = optionalModDialog.getResult(); selectedOptionalMods = optionalModDialog.getResult();
} }
for (const auto& result : results) { 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)) { if (!result.required && !selectedOptionalMods.contains(relpath)) {
relpath += ".disabled"; relpath += ".disabled";
} }

View File

@ -1,5 +1,6 @@
#include "FlameModIndex.h" #include "FlameModIndex.h"
#include "FileSystem.h"
#include "Json.h" #include "Json.h"
#include "minecraft/MinecraftInstance.h" #include "minecraft/MinecraftInstance.h"
#include "minecraft/PackProfile.h" #include "minecraft/PackProfile.h"
@ -138,6 +139,7 @@ auto FlameMod::loadIndexedPackVersion(QJsonObject& obj, bool load_changelog) ->
file.version = Json::requireString(obj, "displayName"); file.version = Json::requireString(obj, "displayName");
file.downloadUrl = Json::ensureString(obj, "downloadUrl"); file.downloadUrl = Json::ensureString(obj, "downloadUrl");
file.fileName = Json::requireString(obj, "fileName"); file.fileName = Json::requireString(obj, "fileName");
file.fileName = FS::RemoveInvalidPathChars(file.fileName);
ModPlatform::IndexedVersionType::VersionType ver_type; ModPlatform::IndexedVersionType::VersionType ver_type;
switch (Json::requireInteger(obj, "releaseType")) { switch (Json::requireInteger(obj, "releaseType")) {

View File

@ -201,7 +201,7 @@ void FlamePackExportTask::makeApiRequest()
<< " reason: " << parseError.errorString(); << " reason: " << parseError.errorString();
qWarning() << *response; qWarning() << *response;
failed(parseError.errorString()); emitFailed(parseError.errorString());
return; return;
} }
@ -213,6 +213,7 @@ void FlamePackExportTask::makeApiRequest()
if (dataArr.isEmpty()) { if (dataArr.isEmpty()) {
qWarning() << "No matches found for fingerprint search!"; qWarning() << "No matches found for fingerprint search!";
getProjectsInfo();
return; return;
} }
for (auto match : dataArr) { for (auto match : dataArr) {
@ -243,9 +244,9 @@ void FlamePackExportTask::makeApiRequest()
qDebug() << doc; qDebug() << doc;
} }
pendingHashes.clear(); pendingHashes.clear();
getProjectsInfo();
}); });
connect(task.get(), &Task::finished, this, &FlamePackExportTask::getProjectsInfo); connect(task.get(), &NetJob::failed, this, &FlamePackExportTask::getProjectsInfo);
connect(task.get(), &NetJob::failed, this, &FlamePackExportTask::emitFailed);
task->start(); task->start();
} }
@ -279,7 +280,7 @@ void FlamePackExportTask::getProjectsInfo()
qWarning() << "Error while parsing JSON response from CurseForge projects task at " << parseError.offset qWarning() << "Error while parsing JSON response from CurseForge projects task at " << parseError.offset
<< " reason: " << parseError.errorString(); << " reason: " << parseError.errorString();
qWarning() << *response; qWarning() << *response;
failed(parseError.errorString()); emitFailed(parseError.errorString());
return; return;
} }
@ -333,7 +334,7 @@ void FlamePackExportTask::buildZip()
setStatus(tr("Adding files...")); setStatus(tr("Adding files..."));
setProgress(4, 5); setProgress(4, 5);
auto zipTask = makeShared<MMCZip::ExportToZipTask>(output, gameRoot, files, "overrides/", true); auto zipTask = makeShared<MMCZip::ExportToZipTask>(output, gameRoot, files, "overrides/", true, false);
zipTask->addExtraFile("manifest.json", generateIndex()); zipTask->addExtraFile("manifest.json", generateIndex());
zipTask->addExtraFile("modlist.html", generateHTML()); zipTask->addExtraFile("modlist.html", generateHTML());

View File

@ -238,11 +238,13 @@ bool ModrinthCreationTask::createInstance()
auto root_modpack_url = QUrl::fromLocalFile(root_modpack_path); auto root_modpack_url = QUrl::fromLocalFile(root_modpack_path);
for (auto file : m_files) { 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))) { 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 // 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.") 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; return false;
} }

View File

@ -200,7 +200,7 @@ void ModrinthPackExportTask::buildZip()
{ {
setStatus(tr("Adding files...")); setStatus(tr("Adding files..."));
auto zipTask = makeShared<MMCZip::ExportToZipTask>(output, gameRoot, files, "overrides/", true); auto zipTask = makeShared<MMCZip::ExportToZipTask>(output, gameRoot, files, "overrides/", true, true);
zipTask->addExtraFile("modrinth.index.json", generateIndex()); zipTask->addExtraFile("modrinth.index.json", generateIndex());
zipTask->setExcludeFiles(resolvedFiles.keys()); zipTask->setExcludeFiles(resolvedFiles.keys());

View File

@ -17,6 +17,7 @@
*/ */
#include "ModrinthPackIndex.h" #include "ModrinthPackIndex.h"
#include "FileSystem.h"
#include "ModrinthAPI.h" #include "ModrinthAPI.h"
#include "Json.h" #include "Json.h"
@ -226,6 +227,7 @@ auto Modrinth::loadIndexedPackVersion(QJsonObject& obj, QString preferred_hash_t
if (parent.contains("url")) { if (parent.contains("url")) {
file.downloadUrl = Json::requireString(parent, "url"); file.downloadUrl = Json::requireString(parent, "url");
file.fileName = Json::requireString(parent, "filename"); file.fileName = Json::requireString(parent, "filename");
file.fileName = FS::RemoveInvalidPathChars(file.fileName);
file.is_preferred = Json::requireBoolean(parent, "primary") || (files.count() == 1); file.is_preferred = Json::requireBoolean(parent, "primary") || (files.count() == 1);
auto hash_list = Json::requireObject(parent, "hashes"); auto hash_list = Json::requireObject(parent, "hashes");

View File

@ -83,8 +83,10 @@ void Technic::TechnicPackProcessor::run(SettingsObjectPtr globalSettings,
data = file.readAll(); data = file.readAll();
file.close(); file.close();
} else { } else {
if (minecraftVersion.isEmpty()) if (minecraftVersion.isEmpty()) {
emit failed(tr("Could not find \"version.json\" inside \"bin/modpack.jar\", but Minecraft version is unknown")); 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->setComponentVersion("net.minecraft", minecraftVersion, true);
components->installJarMods({ modpackJar }); components->installJarMods({ modpackJar });
@ -131,7 +133,9 @@ void Technic::TechnicPackProcessor::run(SettingsObjectPtr globalSettings,
file.close(); file.close();
} else { } else {
// This is the "Vanilla" modpack, excluded by the search code // 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; return;
} }
@ -155,7 +159,25 @@ void Technic::TechnicPackProcessor::run(SettingsObjectPtr globalSettings,
auto libraryObject = Json::ensureObject(library, {}, ""); auto libraryObject = Json::ensureObject(library, {}, "");
auto libraryName = Json::ensureString(libraryObject, "name", "", ""); auto libraryName = Json::ensureString(libraryObject, "name", "", "");
if ((libraryName.startsWith("net.minecraftforge:forge:") || libraryName.startsWith("net.minecraftforge:fmlloader:")) && 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('-')) { libraryName.contains('-')) {
QString libraryVersion = libraryName.section(':', 2); QString libraryVersion = libraryName.section(':', 2);
if (!libraryVersion.startsWith("1.7.10-")) { if (!libraryVersion.startsWith("1.7.10-")) {
@ -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 // 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)); components->setComponentVersion("net.minecraftforge", libraryName.section('-', 1, 1));
} }
break;
} else { } else {
// <Technic library name prefix> -> <our component name> // <Technic library name prefix> -> <our component name>
static QMap<QString, QString> loaderMap{ { "net.minecraftforge:minecraftforge:", "net.minecraftforge" }, static QMap<QString, QString> loaderMap{ { "net.minecraftforge:minecraftforge:", "net.minecraftforge" },

View File

@ -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 auto HttpMetaCache::resolveEntry(QString base, QString resource_path, QString expected_etag) -> MetaEntryPtr
{ {
resource_path = FS::RemoveInvalidPathChars(resource_path);
auto entry = getEntry(base, resource_path); auto entry = getEntry(base, resource_path);
// it's not present? generate a default stale entry // it's not present? generate a default stale entry
if (!entry) { if (!entry) {

View File

@ -68,7 +68,8 @@ void NetRequest::executeTask()
if (getState() == Task::State::AbortedByUser) { if (getState() == Task::State::AbortedByUser) {
qCWarning(logCat) << getUid().toString() << "Attempt to start an aborted Request:" << m_url.toString(); qCWarning(logCat) << getUid().toString() << "Attempt to start an aborted Request:" << m_url.toString();
emitAborted(); emit aborted();
emit finished();
return; return;
} }
@ -85,10 +86,12 @@ void NetRequest::executeTask()
break; break;
case State::Inactive: case State::Inactive:
case State::Failed: case State::Failed:
emitFailed(); emit failed("Failed to initilize sink");
emit finished();
return; return;
case State::AbortedByUser: case State::AbortedByUser:
emitAborted(); emit aborted();
emit finished();
return; return;
} }

View File

@ -51,7 +51,7 @@
Net::NetRequest::Ptr ImgurAlbumCreation::make(std::shared_ptr<ImgurAlbumCreation::Result> output, QList<ScreenShot::Ptr> screenshots) Net::NetRequest::Ptr ImgurAlbumCreation::make(std::shared_ptr<ImgurAlbumCreation::Result> output, QList<ScreenShot::Ptr> screenshots)
{ {
auto up = makeShared<ImgurAlbumCreation>(); auto up = makeShared<ImgurAlbumCreation>();
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_sink.reset(new Sink(output));
up->m_screenshots = screenshots; up->m_screenshots = screenshots;
return up; return up;
@ -72,7 +72,7 @@ void ImgurAlbumCreation::init()
qDebug() << "Setting up imgur upload"; qDebug() << "Setting up imgur upload";
auto api_headers = new Net::StaticHeaderProxy( auto api_headers = new Net::StaticHeaderProxy(
QList<Net::HeaderPair>{ { "Content-Type", "application/x-www-form-urlencoded" }, QList<Net::HeaderPair>{ { "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" } }); { "Accept", "application/json" } });
addHeaderProxy(api_headers); addHeaderProxy(api_headers);
} }

View File

@ -50,9 +50,8 @@
void ImgurUpload::init() void ImgurUpload::init()
{ {
qDebug() << "Setting up imgur upload"; qDebug() << "Setting up imgur upload";
auto api_headers = new Net::StaticHeaderProxy( auto api_headers = new Net::StaticHeaderProxy(QList<Net::HeaderPair>{
QList<Net::HeaderPair>{ { "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" } });
{ "Accept", "application/json" } });
addHeaderProxy(api_headers); addHeaderProxy(api_headers);
} }
@ -70,14 +69,14 @@ QNetworkReply* ImgurUpload::getReply(QNetworkRequest& request)
QHttpPart filePart; QHttpPart filePart;
filePart.setBodyDevice(file); filePart.setBodyDevice(file);
filePart.setHeader(QNetworkRequest::ContentTypeHeader, "image/png"); 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); multipart->append(filePart);
QHttpPart typePart; QHttpPart typePart;
typePart.setHeader(QNetworkRequest::ContentDispositionHeader, "form-data; name=\"type\""); typePart.setHeader(QNetworkRequest::ContentDispositionHeader, "form-data; name=\"type\"");
typePart.setBody("file"); typePart.setBody("file");
multipart->append(typePart); multipart->append(typePart);
QHttpPart namePart; QHttpPart namePart;
namePart.setHeader(QNetworkRequest::ContentDispositionHeader, "form-data; name=\"name\""); namePart.setHeader(QNetworkRequest::ContentDispositionHeader, "form-data; name=\"title\"");
namePart.setBody(m_fileInfo.baseName().toUtf8()); namePart.setBody(m_fileInfo.baseName().toUtf8());
multipart->append(namePart); multipart->append(namePart);
@ -124,7 +123,7 @@ auto ImgurUpload::Sink::finalize(QNetworkReply&) -> Task::State
Net::NetRequest::Ptr ImgurUpload::make(ScreenShot::Ptr m_shot) Net::NetRequest::Ptr ImgurUpload::make(ScreenShot::Ptr m_shot)
{ {
auto up = makeShared<ImgurUpload>(m_shot->m_file); auto up = makeShared<ImgurUpload>(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)); up->m_sink.reset(new Sink(m_shot));
return up; return up;
} }

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>
@ -152,10 +153,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

@ -40,6 +40,7 @@
#include <QMimeData> #include <QMimeData>
#include <QPushButton> #include <QPushButton>
#include <QStandardPaths> #include <QStandardPaths>
#include <QTimer>
BlockedModsDialog::BlockedModsDialog(QWidget* parent, const QString& title, const QString& text, QList<BlockedMod>& mods, QString hash_type) BlockedModsDialog::BlockedModsDialog(QWidget* parent, const QString& title, const QString& text, QList<BlockedMod>& mods, QString hash_type)
: QDialog(parent), ui(new Ui::BlockedModsDialog), m_mods(mods), m_hash_type(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; qDebug() << "[Blocked Mods Dialog] Mods List: " << mods;
// 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(); setupWatch();
scanPaths(); scanPaths();
update();
});
this->setWindowTitle(title); this->setWindowTitle(title);
ui->labelDescription->setText(text); ui->labelDescription->setText(text);
@ -158,7 +164,8 @@ void BlockedModsDialog::update()
QString watching; QString watching;
for (auto& dir : m_watcher.directories()) { for (auto& dir : m_watcher.directories()) {
watching += QString("<a href=\"%1\">%1</a><br/>").arg(dir); QUrl fileURL = QUrl::fromLocalFile(dir);
watching += QString("<a href=\"%1\">%2</a><br/>").arg(fileURL.toString(), dir);
} }
ui->textBrowserWatched->setText(watching); ui->textBrowserWatched->setText(watching);
@ -194,6 +201,10 @@ void BlockedModsDialog::setupWatch()
void BlockedModsDialog::watchPath(QString path, bool watch_recursive) void BlockedModsDialog::watchPath(QString path, bool watch_recursive)
{ {
auto to_watch = QFileInfo(path); 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(); auto to_watch_path = to_watch.canonicalFilePath();
if (m_watcher.directories().contains(to_watch_path)) if (m_watcher.directories().contains(to_watch_path))
return; // don't watch the same path twice (no loops!) return; // don't watch the same path twice (no loops!)

View File

@ -146,7 +146,7 @@ void ExportInstanceDialog::doExport()
return; return;
} }
auto task = makeShared<MMCZip::ExportToZipTask>(output, m_instance->instanceRoot(), files, "", true); auto task = makeShared<MMCZip::ExportToZipTask>(output, m_instance->instanceRoot(), files, "", true, true);
connect(task.get(), &Task::failed, this, connect(task.get(), &Task::failed, this,
[this, output](QString reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->show(); }); [this, output](QString reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->show(); });

View File

@ -22,6 +22,7 @@
#include <QTextEdit> #include <QTextEdit>
#include "FileSystem.h" #include "FileSystem.h"
#include "Markdown.h" #include "Markdown.h"
#include "StringUtils.h"
#include "minecraft/MinecraftInstance.h" #include "minecraft/MinecraftInstance.h"
#include "minecraft/mod/ModFolderModel.h" #include "minecraft/mod/ModFolderModel.h"
#include "modplatform/helpers/ExportToModList.h" #include "modplatform/helpers/ExportToModList.h"
@ -143,10 +144,10 @@ void ExportToModListDialog::triggerImp()
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;

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"
@ -464,7 +465,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>
@ -64,6 +65,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,
@ -125,7 +127,17 @@ NewInstanceDialog::NewInstanceDialog(const QString& initialGroup,
updateDialogState(); updateDialogState();
if (APPLICATION->settings()->get("NewInstanceGeometry").isValid()) {
restoreGeometry(QByteArray::fromBase64(APPLICATION->settings()->get("NewInstanceGeometry").toByteArray())); 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

@ -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

@ -49,13 +49,11 @@
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);
connect(ui->betaFilter, &QCheckBox::stateChanged, this, &CustomPage::filterChanged); connect(ui->betaFilter, &QCheckBox::stateChanged, this, &CustomPage::filterChanged);
connect(ui->snapshotFilter, &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->releaseFilter, &QCheckBox::stateChanged, this, &CustomPage::filterChanged);
connect(ui->experimentsFilter, &QCheckBox::stateChanged, this, &CustomPage::filterChanged); connect(ui->experimentsFilter, &QCheckBox::stateChanged, this, &CustomPage::filterChanged);
connect(ui->refreshBtn, &QPushButton::clicked, this, &CustomPage::refresh); connect(ui->refreshBtn, &QPushButton::clicked, this, &CustomPage::refresh);
@ -96,13 +94,11 @@ void CustomPage::filterChanged()
{ {
QStringList out; QStringList out;
if (ui->alphaFilter->isChecked()) if (ui->alphaFilter->isChecked())
out << "(old_alpha)"; out << "(alpha)";
if (ui->betaFilter->isChecked()) if (ui->betaFilter->isChecked())
out << "(old_beta)"; out << "(beta)";
if (ui->snapshotFilter->isChecked()) if (ui->snapshotFilter->isChecked())
out << "(snapshot)"; out << "(snapshot)";
if (ui->oldSnapshotFilter->isChecked())
out << "(old_snapshot)";
if (ui->releaseFilter->isChecked()) if (ui->releaseFilter->isChecked())
out << "(release)"; out << "(release)";
if (ui->experimentsFilter->isChecked()) if (ui->experimentsFilter->isChecked())

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">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property> </property>
<property name="orientation"> <layout class="QVBoxLayout" name="verticalLayout_3">
<enum>Qt::Horizontal</enum> <item>
</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">
@ -93,16 +85,6 @@
</property> </property>
</widget> </widget>
</item> </item>
<item>
<widget class="QCheckBox" name="oldSnapshotFilter">
<property name="text">
<string>Old Snapshots</string>
</property>
<property name="checkable">
<bool>true</bool>
</property>
</widget>
</item>
<item> <item>
<widget class="QCheckBox" name="betaFilter"> <widget class="QCheckBox" name="betaFilter">
<property name="text"> <property name="text">
@ -157,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">
@ -283,10 +278,8 @@
</customwidget> </customwidget>
</customwidgets> </customwidgets>
<tabstops> <tabstops>
<tabstop>tabWidget</tabstop>
<tabstop>releaseFilter</tabstop> <tabstop>releaseFilter</tabstop>
<tabstop>snapshotFilter</tabstop> <tabstop>snapshotFilter</tabstop>
<tabstop>oldSnapshotFilter</tabstop>
<tabstop>betaFilter</tabstop> <tabstop>betaFilter</tabstop>
<tabstop>alphaFilter</tabstop> <tabstop>alphaFilter</tabstop>
<tabstop>experimentsFilter</tabstop> <tabstop>experimentsFilter</tabstop>

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"
@ -292,6 +293,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

@ -21,6 +21,7 @@
#include "ui_ImportFTBPage.h" #include "ui_ImportFTBPage.h"
#include <QFileDialog> #include <QFileDialog>
#include <QFileInfo>
#include <QWidget> #include <QWidget>
#include "FileSystem.h" #include "FileSystem.h"
#include "ListModel.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->searchEdit, &QLineEdit::textChanged, this, &ImportFTBPage::triggerSearch);
connect(ui->browseButton, &QPushButton::clicked, this, [this] { connect(ui->browseButton, &QPushButton::clicked, this, [this] {
auto path = listModel->getPath(); QString dir = QFileDialog::getExistingDirectory(this, tr("Select FTBApp instances directory"), listModel->getUserPath(),
QString dir = QFileDialog::getExistingDirectory(this, tr("Select FTBApp instances directory"), path, QFileDialog::ShowDirsOnly); QFileDialog::ShowDirsOnly);
if (!dir.isEmpty()) if (!dir.isEmpty())
listModel->setPath(dir); listModel->setPath(dir);
}); });

View File

@ -24,45 +24,76 @@
#include <QIcon> #include <QIcon>
#include <QProcessEnvironment> #include <QProcessEnvironment>
#include "Application.h" #include "Application.h"
#include "Exception.h"
#include "FileSystem.h" #include "FileSystem.h"
#include "Json.h"
#include "StringUtils.h" #include "StringUtils.h"
#include "modplatform/import_ftb/PackHelpers.h" #include "modplatform/import_ftb/PackHelpers.h"
#include "ui/widgets/ProjectItem.h" #include "ui/widgets/ProjectItem.h"
namespace FTBImportAPP { namespace FTBImportAPP {
QString getStaticPath() QString getFTBRoot()
{ {
QString partialPath; QString partialPath = QDir::homePath();
#if defined(Q_OS_OSX) #if defined(Q_OS_OSX)
partialPath = FS::PathCombine(QDir::homePath(), "Library/Application Support"); partialPath = FS::PathCombine(partialPath, "Library/Application Support");
#elif defined(Q_OS_WIN32)
partialPath = QProcessEnvironment::systemEnvironment().value("LOCALAPPDATA", "");
#else
partialPath = QDir::homePath();
#endif #endif
return FS::PathCombine(partialPath, ".ftba"); 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() void ListModel::update()
{ {
beginResetModel(); beginResetModel();
modpacks.clear(); m_modpacks.clear();
QString instancesPath = getPath(); auto wasPathAdded = [this](QString path) {
if (auto instancesInfo = QFileInfo(instancesPath); instancesInfo.exists() && instancesInfo.isDir()) { for (auto pack : m_modpacks) {
QDirIterator directoryIterator(instancesPath, QDir::Dirs | QDir::NoDotAndDotDot | QDir::Readable | QDir::Hidden, 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); QDirIterator::FollowSymlinks);
while (directoryIterator.hasNext()) { while (directoryIterator.hasNext()) {
auto modpack = parseDirectory(directoryIterator.next()); auto currentPath = directoryIterator.next();
if (!wasPathAdded(currentPath)) {
auto modpack = parseDirectory(currentPath);
if (!modpack.path.isEmpty()) if (!modpack.path.isEmpty())
modpacks.append(modpack); m_modpacks.append(modpack);
} }
} else {
qDebug() << "Couldn't find ftb instances folder: " << instancesPath;
} }
};
scanPath(APPLICATION->settings()->get("FTBAppInstancesPath").toString());
scanPath(m_instances_path);
endResetModel(); endResetModel();
} }
@ -70,11 +101,11 @@ void ListModel::update()
QVariant ListModel::data(const QModelIndex& index, int role) const QVariant ListModel::data(const QModelIndex& index, int role) const
{ {
int pos = index.row(); int pos = index.row();
if (pos >= modpacks.size() || pos < 0 || !index.isValid()) { if (pos >= m_modpacks.size() || pos < 0 || !index.isValid()) {
return QVariant(); return QVariant();
} }
auto pack = modpacks.at(pos); auto pack = m_modpacks.at(pos);
if (role == Qt::ToolTipRole) { if (role == Qt::ToolTipRole) {
} }
@ -110,9 +141,9 @@ QVariant ListModel::data(const QModelIndex& index, int role) const
FilterModel::FilterModel(QObject* parent) : QSortFilterProxyModel(parent) FilterModel::FilterModel(QObject* parent) : QSortFilterProxyModel(parent)
{ {
currentSorting = Sorting::ByGameVersion; m_currentSorting = Sorting::ByGameVersion;
sortings.insert(tr("Sort by Name"), Sorting::ByName); m_sortings.insert(tr("Sort by Name"), Sorting::ByName);
sortings.insert(tr("Sort by Game Version"), Sorting::ByGameVersion); m_sortings.insert(tr("Sort by Game Version"), Sorting::ByGameVersion);
} }
bool FilterModel::lessThan(const QModelIndex& left, const QModelIndex& right) const 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>(); Modpack leftPack = sourceModel()->data(left, Qt::UserRole).value<Modpack>();
Modpack rightPack = sourceModel()->data(right, Qt::UserRole).value<Modpack>(); Modpack rightPack = sourceModel()->data(right, Qt::UserRole).value<Modpack>();
if (currentSorting == Sorting::ByGameVersion) { if (m_currentSorting == Sorting::ByGameVersion) {
Version lv(leftPack.mcVersion); Version lv(leftPack.mcVersion);
Version rv(rightPack.mcVersion); Version rv(rightPack.mcVersion);
return lv < rv; return lv < rv;
} else if (currentSorting == Sorting::ByName) { } else if (m_currentSorting == Sorting::ByName) {
return StringUtils::naturalCompare(leftPack.name, rightPack.name, Qt::CaseSensitive) >= 0; 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 bool FilterModel::filterAcceptsRow([[maybe_unused]] int sourceRow, [[maybe_unused]] const QModelIndex& sourceParent) const
{ {
if (searchTerm.isEmpty()) { if (m_searchTerm.isEmpty()) {
return true; return true;
} }
QModelIndex index = sourceModel()->index(sourceRow, 0, sourceParent); QModelIndex index = sourceModel()->index(sourceRow, 0, sourceParent);
Modpack pack = sourceModel()->data(index, Qt::UserRole).value<Modpack>(); Modpack pack = sourceModel()->data(index, Qt::UserRole).value<Modpack>();
return pack.name.contains(searchTerm, Qt::CaseInsensitive); return pack.name.contains(m_searchTerm, Qt::CaseInsensitive);
} }
void FilterModel::setSearchTerm(const QString term) void FilterModel::setSearchTerm(const QString term)
{ {
searchTerm = term.trimmed(); m_searchTerm = term.trimmed();
invalidate(); invalidate();
} }
const QMap<QString, FilterModel::Sorting> FilterModel::getAvailableSortings() const QMap<QString, FilterModel::Sorting> FilterModel::getAvailableSortings()
{ {
return sortings; return m_sortings;
} }
QString FilterModel::translateCurrentSorting() QString FilterModel::translateCurrentSorting()
{ {
return sortings.key(currentSorting); return m_sortings.key(m_currentSorting);
} }
void FilterModel::setSorting(Sorting s) void FilterModel::setSorting(Sorting s)
{ {
currentSorting = s; m_currentSorting = s;
invalidate(); invalidate();
} }
FilterModel::Sorting FilterModel::getCurrentSorting() FilterModel::Sorting FilterModel::getCurrentSorting()
{ {
return currentSorting; return m_currentSorting;
} }
void ListModel::setPath(QString path) void ListModel::setPath(QString path)
{ {
@ -176,11 +207,11 @@ void ListModel::setPath(QString path)
update(); update();
} }
QString ListModel::getPath() QString ListModel::getUserPath()
{ {
auto path = APPLICATION->settings()->get("FTBAppInstancesPath").toString(); auto path = APPLICATION->settings()->get("FTBAppInstancesPath").toString();
if (path.isEmpty() || !QFileInfo(path).exists()) if (path.isEmpty())
path = FTB_APP_PATH; path = m_instances_path;
return path; return path;
} }
} // namespace FTBImportAPP } // namespace FTBImportAPP

View File

@ -42,28 +42,29 @@ class FilterModel : public QSortFilterProxyModel {
bool lessThan(const QModelIndex& left, const QModelIndex& right) const override; bool lessThan(const QModelIndex& left, const QModelIndex& right) const override;
private: private:
QMap<QString, Sorting> sortings; QMap<QString, Sorting> m_sortings;
Sorting currentSorting; Sorting m_currentSorting;
QString searchTerm; QString m_searchTerm;
}; };
class ListModel : public QAbstractListModel { class ListModel : public QAbstractListModel {
Q_OBJECT Q_OBJECT
public: public:
ListModel(QObject* parent) : QAbstractListModel(parent) {} ListModel(QObject* parent);
virtual ~ListModel() = default; 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; } int columnCount(const QModelIndex& parent) const { return 1; }
QVariant data(const QModelIndex& index, int role) const; QVariant data(const QModelIndex& index, int role) const;
void update(); void update();
QString getPath(); QString getUserPath();
void setPath(QString path); void setPath(QString path);
private: private:
ModpackList modpacks; ModpackList m_modpacks;
const QString m_instances_path;
}; };
} // namespace FTBImportAPP } // namespace FTBImportAPP

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"
@ -303,7 +304,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

@ -0,0 +1,30 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (C) 2024 TheKodeToad <TheKodeToad@proton.me>
*
* 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/>.
*/
#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);
}

View File

@ -0,0 +1,34 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (C) 2024 TheKodeToad <TheKodeToad@proton.me>
*
* 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/>.
*/
#pragma once
#include <QProxyStyle>
#include <iostream>
/// 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;
};

View File

@ -2,6 +2,7 @@
/* /*
* Prism Launcher - Minecraft Launcher * Prism Launcher - Minecraft Launcher
* Copyright (C) 2022 Tayou <git@tayou.org> * Copyright (C) 2022 Tayou <git@tayou.org>
* Copyright (C) 2024 TheKodeToad <TheKodeToad@proton.me>
* *
* This program is free software: you can redistribute it and/or modify * 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 * it under the terms of the GNU General Public License as published by
@ -36,12 +37,13 @@
#include <QDir> #include <QDir>
#include <QStyleFactory> #include <QStyleFactory>
#include "Application.h" #include "Application.h"
#include "HintOverrideProxyStyle.h"
#include "rainbow.h" #include "rainbow.h"
void ITheme::apply(bool) void ITheme::apply(bool)
{ {
APPLICATION->setStyleSheet(QString()); APPLICATION->setStyleSheet(QString());
QApplication::setStyle(QStyleFactory::create(qtTheme())); QApplication::setStyle(new HintOverrideProxyStyle(QStyleFactory::create(qtTheme())));
if (hasColorScheme()) { if (hasColorScheme()) {
QApplication::setPalette(colorScheme()); QApplication::setPalette(colorScheme());
} }

View File

@ -2,6 +2,7 @@
/* /*
* Prism Launcher - Minecraft Launcher * Prism Launcher - Minecraft Launcher
* Copyright (C) 2022 Tayou <git@tayou.org> * Copyright (C) 2022 Tayou <git@tayou.org>
* Copyright (C) 2024 TheKodeToad <TheKodeToad@proton.me>
* *
* This program is free software: you can redistribute it and/or modify * 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 * it under the terms of the GNU General Public License as published by
@ -37,6 +38,7 @@
#include <QDebug> #include <QDebug>
#include <QStyle> #include <QStyle>
#include <QStyleFactory> #include <QStyleFactory>
#include "HintOverrideProxyStyle.h"
#include "ThemeManager.h" #include "ThemeManager.h"
SystemTheme::SystemTheme() SystemTheme::SystemTheme()
@ -64,8 +66,11 @@ void SystemTheme::apply(bool initial)
{ {
// See https://github.com/MultiMC/Launcher/issues/1790 // See https://github.com/MultiMC/Launcher/issues/1790
// or https://github.com/PrismLauncher/PrismLauncher/issues/490 // or https://github.com/PrismLauncher/PrismLauncher/issues/490
if (initial) if (initial) {
QApplication::setStyle(new HintOverrideProxyStyle(QStyleFactory::create(qtTheme())));
return; return;
}
ITheme::apply(initial); ITheme::apply(initial);
} }

View File

@ -22,6 +22,7 @@
#include <QDebug> #include <QDebug>
#include <QPainter> #include <QPainter>
#include <QTextObject> #include <QTextObject>
#include <memory>
#include "Application.h" #include "Application.h"
@ -36,6 +37,30 @@ QSizeF VariableSizedImageObject::intrinsicSize(QTextDocument* doc, int posInDocu
auto image = qvariant_cast<QImage>(format.property(ImageData)); auto image = qvariant_cast<QImage>(format.property(ImageData));
auto size = image.size(); 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. // Get the width of the text content to make the image similar sized.
// doc->textWidth() includes the margin, so we need to remove it. // doc->textWidth() includes the margin, so we need to remove it.
@ -46,6 +71,7 @@ QSizeF VariableSizedImageObject::intrinsicSize(QTextDocument* doc, int posInDocu
return { size }; return { size };
} }
void VariableSizedImageObject::drawObject(QPainter* painter, void VariableSizedImageObject::drawObject(QPainter* painter,
const QRectF& rect, const QRectF& rect,
QTextDocument* doc, QTextDocument* doc,
@ -57,7 +83,20 @@ void VariableSizedImageObject::drawObject(QPainter* painter,
if (m_fetching_images.contains(image_url)) if (m_fetching_images.contains(image_url))
return; return;
loadImage(doc, image_url, posInDocument); auto meta = std::make_shared<ImageMetadata>();
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; return;
} }
@ -72,16 +111,19 @@ void VariableSizedImageObject::flush()
m_fetching_images.clear(); m_fetching_images.clear();
} }
void VariableSizedImageObject::parseImage(QTextDocument* doc, QImage image, int posInDocument) void VariableSizedImageObject::parseImage(QTextDocument* doc, std::shared_ptr<ImageMetadata> meta)
{ {
QTextCursor cursor(doc); QTextCursor cursor(doc);
cursor.setPosition(posInDocument); cursor.setPosition(meta->posInDocument);
cursor.setKeepPositionOnInsert(true); cursor.setKeepPositionOnInsert(true);
auto image_char_format = cursor.charFormat(); auto image_char_format = cursor.charFormat();
image_char_format.setObjectType(QTextFormat::ImageObject); 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. // 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. // 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); 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<ImageMetadata> meta)
{ {
m_fetching_images.insert(source); m_fetching_images.insert(meta->url);
MetaEntryPtr entry = APPLICATION->metacache()->resolveEntry( MetaEntryPtr entry = APPLICATION->metacache()->resolveEntry(
m_meta_entry, 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()); auto job = new NetJob(QString("Load Image: %1").arg(meta->url.fileName()), APPLICATION->network());
job->addNetAction(Net::ApiDownload::makeCached(source, entry)); job->addNetAction(Net::ApiDownload::makeCached(meta->url, entry));
auto full_entry_path = entry->getFullPath(); auto full_entry_path = entry->getFullPath();
auto source_url = source; auto source_url = meta->url;
auto loadImage = [this, doc, full_entry_path, source_url, posInDocument](const QImage& image) { auto loadImage = [this, doc, full_entry_path, source_url, meta](const QImage& image) {
doc->addResource(QTextDocument::ImageResource, source_url, 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 // This size hack is needed to prevent the content from being laid out in an area smaller
// than the total width available (weird). // than the total width available (weird).

View File

@ -22,6 +22,7 @@
#include <QString> #include <QString>
#include <QTextObjectInterface> #include <QTextObjectInterface>
#include <QUrl> #include <QUrl>
#include <memory>
/** Custom image text object to be used instead of the normal one in ProjectDescriptionPage. /** 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_OBJECT
Q_INTERFACES(QTextObjectInterface) Q_INTERFACES(QTextObjectInterface)
struct ImageMetadata {
int posInDocument;
QUrl url;
QImage image;
int width;
int height;
};
public: public:
QSizeF intrinsicSize(QTextDocument* doc, int posInDocument, const QTextFormat& format) override; 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; 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: private:
/** Adds the image to the document, in the given position. /** 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<ImageMetadata> meta);
/** Loads an image from an external source, and adds it to the document. /** Loads an image from an external source, and adds it to the document.
* *
* This uses m_meta_entry to cache the image. * 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<ImageMetadata> meta);
private: private:
QString m_meta_entry; QString m_meta_entry;

View File

@ -1118,7 +1118,6 @@ void PrismUpdaterApp::backupAppDir()
"Qt*.dll", "Qt*.dll",
}); });
} }
file_list.append("portable.txt");
logUpdate("manifest.txt empty or missing. making best guess at files to back up."); 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 "))); logUpdate(tr("Backing up:\n %1").arg(file_list.join(",\n ")));

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

@ -1,7 +1,7 @@
[Desktop Entry] [Desktop Entry]
Version=1.0 Version=1.0
Name=Fjord Launcher 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 Type=Application
Terminal=false Terminal=false
Exec=@Launcher_APP_BINARY_NAME@ %U Exec=@Launcher_APP_BINARY_NAME@ %U