Merge remote-tracking branch 'prismlauncher/release-8.x' into develop
This commit is contained in:
commit
51da756e19
@ -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")
|
||||
|
@ -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
|
||||
|
@ -6,6 +6,8 @@
|
||||
<string>A Minecraft mod wants to access your camera.</string>
|
||||
<key>NSMicrophoneUsageDescription</key>
|
||||
<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>
|
||||
<string>NSApplication</string>
|
||||
<key>NSHighResolutionCapable</key>
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -378,6 +378,7 @@ enum class FilesystemType {
|
||||
HFSX,
|
||||
FUSEBLK,
|
||||
F2FS,
|
||||
BCACHEFS,
|
||||
UNKNOWN
|
||||
};
|
||||
|
||||
@ -406,6 +407,7 @@ static const QMap<FilesystemType, QStringList> 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<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
|
||||
|
@ -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;
|
||||
|
||||
|
@ -288,9 +288,7 @@ std::optional<QStringList> 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;
|
||||
|
||||
|
@ -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;
|
||||
|
||||
|
@ -212,3 +212,25 @@ QPair<QString, QString> 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 </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;
|
||||
}
|
@ -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, const QRegularExpression& re);
|
||||
|
||||
QString htmlListPatch(QString htmlStr);
|
||||
|
||||
} // namespace StringUtils
|
||||
|
@ -207,7 +207,7 @@ QList<JavaInstallPtr> 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<QString> JavaUtils::FindJavaPaths()
|
||||
QList<JavaInstallPtr> ADOPTIUMJDK64s =
|
||||
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
|
||||
QList<JavaInstallPtr> MICROSOFTJDK64s =
|
||||
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(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<QString> 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<QString> 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<QString> 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<QString> 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<QString> 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");
|
||||
|
@ -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();
|
||||
|
@ -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")
|
||||
|
@ -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<int, std::pair<Version, Version>> 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<int, std::pair<Version, Version>> 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)
|
||||
{
|
||||
|
@ -11,7 +11,7 @@
|
||||
#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<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") } },
|
||||
{ 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") } },
|
||||
{ 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") } }
|
||||
{ 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)
|
||||
|
@ -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)
|
||||
|
@ -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";
|
||||
}
|
||||
|
@ -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")) {
|
||||
|
@ -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<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("modlist.html", generateHTML());
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -200,7 +200,7 @@ void ModrinthPackExportTask::buildZip()
|
||||
{
|
||||
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->setExcludeFiles(resolvedFiles.keys());
|
||||
|
@ -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");
|
||||
|
||||
|
@ -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,7 +159,25 @@ 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:")) &&
|
||||
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-")) {
|
||||
@ -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 {
|
||||
// <Technic library name prefix> -> <our component name>
|
||||
static QMap<QString, QString> loaderMap{ { "net.minecraftforge:minecraftforge:", "net.minecraftforge" },
|
||||
|
@ -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) {
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -51,7 +51,7 @@
|
||||
Net::NetRequest::Ptr ImgurAlbumCreation::make(std::shared_ptr<ImgurAlbumCreation::Result> output, QList<ScreenShot::Ptr> screenshots)
|
||||
{
|
||||
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_screenshots = screenshots;
|
||||
return up;
|
||||
@ -72,7 +72,7 @@ void ImgurAlbumCreation::init()
|
||||
qDebug() << "Setting up imgur upload";
|
||||
auto api_headers = new Net::StaticHeaderProxy(
|
||||
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" } });
|
||||
addHeaderProxy(api_headers);
|
||||
}
|
||||
|
@ -50,9 +50,8 @@
|
||||
void ImgurUpload::init()
|
||||
{
|
||||
qDebug() << "Setting up imgur upload";
|
||||
auto api_headers = new Net::StaticHeaderProxy(
|
||||
QList<Net::HeaderPair>{ { "Authorization", QString("Client-ID %1").arg(BuildConfig.IMGUR_CLIENT_ID).toStdString().c_str() },
|
||||
{ "Accept", "application/json" } });
|
||||
auto api_headers = new Net::StaticHeaderProxy(QList<Net::HeaderPair>{
|
||||
{ "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<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));
|
||||
return up;
|
||||
}
|
||||
|
@ -38,6 +38,7 @@
|
||||
#include "Application.h"
|
||||
#include "BuildConfig.h"
|
||||
#include "Markdown.h"
|
||||
#include "StringUtils.h"
|
||||
#include "ui_AboutDialog.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));
|
||||
|
||||
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);
|
||||
|
||||
|
@ -40,6 +40,7 @@
|
||||
#include <QMimeData>
|
||||
#include <QPushButton>
|
||||
#include <QStandardPaths>
|
||||
#include <QTimer>
|
||||
|
||||
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)
|
||||
@ -60,8 +61,13 @@ BlockedModsDialog::BlockedModsDialog(QWidget* parent, const QString& title, cons
|
||||
|
||||
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();
|
||||
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("<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);
|
||||
@ -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!)
|
||||
|
@ -146,7 +146,7 @@ void ExportInstanceDialog::doExport()
|
||||
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,
|
||||
[this, output](QString reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->show(); });
|
||||
|
@ -22,6 +22,7 @@
|
||||
#include <QTextEdit>
|
||||
#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;
|
||||
|
@ -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);
|
||||
|
@ -52,6 +52,7 @@
|
||||
#include <QFileDialog>
|
||||
#include <QLayout>
|
||||
#include <QPushButton>
|
||||
#include <QScreen>
|
||||
#include <QValidator>
|
||||
#include <utility>
|
||||
|
||||
@ -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<QString, QString>& extra_info,
|
||||
@ -125,7 +127,17 @@ NewInstanceDialog::NewInstanceDialog(const QString& initialGroup,
|
||||
|
||||
updateDialogState();
|
||||
|
||||
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()
|
||||
|
@ -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]() {
|
||||
|
@ -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;
|
||||
|
@ -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!"
|
||||
"</h4>");
|
||||
|
||||
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();
|
||||
}
|
||||
|
@ -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())
|
||||
|
@ -24,29 +24,21 @@
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QTabWidget" name="tabWidget">
|
||||
<property name="currentIndex">
|
||||
<number>0</number>
|
||||
<widget class="QScrollArea" name="scrollArea">
|
||||
<property name="widgetResizable">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<widget class="QWidget" name="tab">
|
||||
<attribute name="title">
|
||||
<string notr="true"/>
|
||||
</attribute>
|
||||
<layout class="QGridLayout" name="gridLayout_2">
|
||||
<item row="2" column="0">
|
||||
<widget class="Line" name="line">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Minimum" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
<widget class="QWidget" name="content">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>813</width>
|
||||
<height>605</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="0">
|
||||
<layout class="QVBoxLayout" name="verticalLayout_3">
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="minecraftLayout">
|
||||
<item>
|
||||
<widget class="VersionSelectWidget" name="versionList" native="true">
|
||||
@ -93,16 +85,6 @@
|
||||
</property>
|
||||
</widget>
|
||||
</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>
|
||||
<widget class="QCheckBox" name="betaFilter">
|
||||
<property name="text">
|
||||
@ -157,7 +139,20 @@
|
||||
</item>
|
||||
</layout>
|
||||
</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">
|
||||
<item>
|
||||
<widget class="VersionSelectWidget" name="loaderVersionList" native="true">
|
||||
@ -283,10 +278,8 @@
|
||||
</customwidget>
|
||||
</customwidgets>
|
||||
<tabstops>
|
||||
<tabstop>tabWidget</tabstop>
|
||||
<tabstop>releaseFilter</tabstop>
|
||||
<tabstop>snapshotFilter</tabstop>
|
||||
<tabstop>oldSnapshotFilter</tabstop>
|
||||
<tabstop>betaFilter</tabstop>
|
||||
<tabstop>alphaFilter</tabstop>
|
||||
<tabstop>experimentsFilter</tabstop>
|
||||
|
@ -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 += "<hr>";
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
|
@ -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<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) {
|
||||
ui->versionSelectionBox->addItem(version.version);
|
||||
|
@ -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 += "<hr>";
|
||||
text += api.getModDescription(current.addonId).toUtf8();
|
||||
|
||||
ui->packDescription->setHtml(text + current.description);
|
||||
ui->packDescription->setHtml(StringUtils::htmlListPatch(text + current.description));
|
||||
ui->packDescription->flush();
|
||||
}
|
||||
|
@ -21,6 +21,7 @@
|
||||
#include "ui_ImportFTBPage.h"
|
||||
|
||||
#include <QFileDialog>
|
||||
#include <QFileInfo>
|
||||
#include <QWidget>
|
||||
#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);
|
||||
});
|
||||
|
@ -24,45 +24,76 @@
|
||||
#include <QIcon>
|
||||
#include <QProcessEnvironment>
|
||||
#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());
|
||||
auto currentPath = directoryIterator.next();
|
||||
if (!wasPathAdded(currentPath)) {
|
||||
auto modpack = parseDirectory(currentPath);
|
||||
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();
|
||||
}
|
||||
@ -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>();
|
||||
Modpack rightPack = sourceModel()->data(right, Qt::UserRole).value<Modpack>();
|
||||
|
||||
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<Modpack>();
|
||||
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<QString, FilterModel::Sorting> 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
|
@ -42,28 +42,29 @@ class FilterModel : public QSortFilterProxyModel {
|
||||
bool lessThan(const QModelIndex& left, const QModelIndex& right) const override;
|
||||
|
||||
private:
|
||||
QMap<QString, Sorting> sortings;
|
||||
Sorting currentSorting;
|
||||
QString searchTerm;
|
||||
QMap<QString, Sorting> 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
|
@ -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 <b>" + pack->author + "</b>" + "<br>Minecraft " + pack->mcVersion + "<br>" + "<br>" +
|
||||
pack->description + "<ul><li>" + pack->mods.replace(";", "</li><li>") + "</li></ul>");
|
||||
currentModpackInfo->setHtml(StringUtils::htmlListPatch("Pack by <b>" + pack->author + "</b>" + "<br>Minecraft " + pack->mcVersion +
|
||||
"<br>" + "<br>" + pack->description + "<ul><li>" +
|
||||
pack->mods.replace(";", "</li><li>") + "</li></ul>"));
|
||||
bool currentAdded = false;
|
||||
|
||||
for (int i = 0; i < pack->oldVersions.size(); i++) {
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
|
@ -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 += "<br><br>";
|
||||
|
||||
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) {
|
||||
|
30
launcher/ui/themes/HintOverrideProxyStyle.cpp
Normal file
30
launcher/ui/themes/HintOverrideProxyStyle.cpp
Normal 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);
|
||||
}
|
34
launcher/ui/themes/HintOverrideProxyStyle.h
Normal file
34
launcher/ui/themes/HintOverrideProxyStyle.h
Normal 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;
|
||||
};
|
@ -2,6 +2,7 @@
|
||||
/*
|
||||
* Prism Launcher - Minecraft Launcher
|
||||
* 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
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@ -36,12 +37,13 @@
|
||||
#include <QDir>
|
||||
#include <QStyleFactory>
|
||||
#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());
|
||||
}
|
||||
|
@ -2,6 +2,7 @@
|
||||
/*
|
||||
* Prism Launcher - Minecraft Launcher
|
||||
* 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
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@ -37,6 +38,7 @@
|
||||
#include <QDebug>
|
||||
#include <QStyle>
|
||||
#include <QStyleFactory>
|
||||
#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);
|
||||
}
|
||||
|
||||
|
@ -22,6 +22,7 @@
|
||||
#include <QDebug>
|
||||
#include <QPainter>
|
||||
#include <QTextObject>
|
||||
#include <memory>
|
||||
|
||||
#include "Application.h"
|
||||
|
||||
@ -36,6 +37,30 @@ QSizeF VariableSizedImageObject::intrinsicSize(QTextDocument* doc, int posInDocu
|
||||
|
||||
auto image = qvariant_cast<QImage>(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<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;
|
||||
}
|
||||
|
||||
@ -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<ImageMetadata> 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<ImageMetadata> 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).
|
||||
|
@ -22,6 +22,7 @@
|
||||
#include <QString>
|
||||
#include <QTextObjectInterface>
|
||||
#include <QUrl>
|
||||
#include <memory>
|
||||
|
||||
/** 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<ImageMetadata> 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<ImageMetadata> meta);
|
||||
|
||||
private:
|
||||
QString m_meta_entry;
|
||||
|
@ -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 ")));
|
||||
|
@ -26,6 +26,7 @@
|
||||
|
||||
#include <QTextBrowser>
|
||||
#include "Markdown.h"
|
||||
#include "StringUtils.h"
|
||||
|
||||
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)
|
||||
@ -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<GitHubReleaseAsset>& assets, QWidget* parent)
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user