Merge branch 'develop' into refactor-instanceview

This commit is contained in:
Sefa Eyeoglu
2022-12-07 15:22:33 +01:00
473 changed files with 25550 additions and 2685 deletions

View File

@ -1,8 +1,12 @@
// SPDX-License-Identifier: GPL-3.0-only
// SPDX-FileCopyrightText: 2022 Sefa Eyeoglu <contact@scrumplex.net>
//
// SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0
/*
* PolyMC - Minecraft Launcher
* Prism Launcher - Minecraft Launcher
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
* Copyright (C) 2022 Lenny McLennington <lenny@sneed.church>
* Copyright (C) 2022 Tayou <tayou@gmx.net>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@ -37,11 +41,16 @@
#include "Application.h"
#include "BuildConfig.h"
#include "DataMigrationTask.h"
#include "net/PasteUpload.h"
#include "pathmatcher/MultiMatcher.h"
#include "pathmatcher/SimplePrefixMatcher.h"
#include "ui/InstanceWindow.h"
#include "ui/MainWindow.h"
#include "ui/instanceview/InstancesView.h"
#include "ui/dialogs/ProgressDialog.h"
#include "ui/pages/BasePageProvider.h"
#include "ui/pages/global/LauncherPage.h"
#include "ui/pages/global/MinecraftPage.h"
@ -53,14 +62,9 @@
#include "ui/pages/global/APIPage.h"
#include "ui/pages/global/CustomCommandsPage.h"
#include "ui/themes/ITheme.h"
#include "ui/themes/SystemTheme.h"
#include "ui/themes/DarkTheme.h"
#include "ui/themes/BrightTheme.h"
#include "ui/themes/CustomTheme.h"
#ifdef Q_OS_WIN
#include "ui/WinDarkmode.h"
#include <versionhelpers.h>
#endif
#include "ui/setupwizard/SetupWizard.h"
@ -72,6 +76,8 @@
#include "ui/pagedialog/PageDialog.h"
#include "ui/themes/ThemeManager.h"
#include "ApplicationMessage.h"
#include <iostream>
@ -227,7 +233,7 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv)
setOrganizationDomain(BuildConfig.LAUNCHER_DOMAIN);
setApplicationName(BuildConfig.LAUNCHER_NAME);
setApplicationDisplayName(QString("%1 %2").arg(BuildConfig.LAUNCHER_DISPLAYNAME, BuildConfig.printableVersionString()));
setApplicationVersion(BuildConfig.printableVersionString());
setApplicationVersion(BuildConfig.printableVersionString() + "\n" + BuildConfig.GIT_COMMIT);
setDesktopFileName(BuildConfig.LAUNCHER_DESKTOPFILENAME);
startTime = QDateTime::currentDateTime();
@ -302,22 +308,6 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv)
dataPath = foo.absolutePath();
adjustedBy = "Persistent data path";
QDir polymcData(FS::PathCombine(QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation), "PolyMC"));
if (polymcData.exists()) {
dataPath = polymcData.absolutePath();
adjustedBy = "PolyMC data path";
}
#ifdef Q_OS_LINUX
// TODO: this should be removed in a future version
// TODO: provide a migration path similar to macOS migration
QDir bar(FS::PathCombine(QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation), "polymc"));
if (bar.exists()) {
dataPath = bar.absolutePath();
adjustedBy = "Legacy data path";
}
#endif
#ifndef Q_OS_MACOS
if (QFile::exists(FS::PathCombine(m_rootPath, "portable.txt"))) {
dataPath = m_rootPath;
@ -440,6 +430,15 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv)
qDebug() << "<> Log initialized.";
}
{
bool migrated = false;
if (!migrated)
migrated = handleDataMigration(dataPath, FS::PathCombine(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation), "../../PolyMC"), "PolyMC", "polymc.cfg");
if (!migrated)
migrated = handleDataMigration(dataPath, FS::PathCombine(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation), "../../multimc"), "MultiMC", "multimc.cfg");
}
{
qDebug() << BuildConfig.LAUNCHER_DISPLAYNAME << ", (c) 2013-2021 " << BuildConfig.LAUNCHER_COPYRIGHT;
@ -499,6 +498,7 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv)
// Theming
m_settings->registerSetting("IconTheme", QString("pe_colored"));
m_settings->registerSetting("ApplicationTheme", QString("system"));
m_settings->registerSetting("BackgroundCat", QString("kitteh"));
// Remembered state
m_settings->registerSetting("LastUsedGroupForNewInstance", QString());
@ -563,7 +563,7 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv)
// Memory
m_settings->registerSetting({"MinMemAlloc", "MinMemoryAlloc"}, 512);
m_settings->registerSetting({"MaxMemAlloc", "MaxMemoryAlloc"}, 4096);
m_settings->registerSetting({"MaxMemAlloc", "MaxMemoryAlloc"}, suitableMaxMem());
m_settings->registerSetting("PermGen", 128);
// Java Settings
@ -612,6 +612,8 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv)
m_settings->registerSetting("TheCat", false);
m_settings->registerSetting("InstanceDisplayMode", InstancesView::TableMode);
m_settings->registerSetting("ToolbarsLocked", false);
m_settings->registerSetting("InstSortMode", "Name");
m_settings->registerSetting("SelectedInstance", QString());
@ -746,29 +748,8 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv)
qDebug() << "<> Instance icons intialized.";
}
// Icon themes
{
// TODO: icon themes and instance icons do not mesh well together. Rearrange and fix discrepancies!
// set icon theme search path!
auto searchPaths = QIcon::themeSearchPaths();
searchPaths.append("iconthemes");
QIcon::setThemeSearchPaths(searchPaths);
qDebug() << "<> Icon themes initialized.";
}
// Initialize widget themes
{
auto insertTheme = [this](ITheme * theme)
{
m_themes.insert(std::make_pair(theme->id(), std::unique_ptr<ITheme>(theme)));
};
auto darkTheme = new DarkTheme();
insertTheme(new SystemTheme());
insertTheme(darkTheme);
insertTheme(new BrightTheme());
insertTheme(new CustomTheme(darkTheme, "custom"));
qDebug() << "<> Widget themes initialized.";
}
// Themes
m_themeManager = std::make_unique<ThemeManager>(m_mainWindow);
// initialize and load all instances
{
@ -933,18 +914,24 @@ bool Application::createSetupWizard()
return false;
}
bool Application::event(QEvent* event) {
bool Application::event(QEvent* event)
{
#ifdef Q_OS_MACOS
if (event->type() == QEvent::ApplicationStateChange) {
auto ev = static_cast<QApplicationStateChangeEvent*>(event);
if (m_prevAppState == Qt::ApplicationActive
&& ev->applicationState() == Qt::ApplicationActive) {
if (m_prevAppState == Qt::ApplicationActive && ev->applicationState() == Qt::ApplicationActive) {
emit clickedOnDock();
}
m_prevAppState = ev->applicationState();
}
#endif
if (event->type() == QEvent::FileOpen) {
auto ev = static_cast<QFileOpenEvent*>(event);
m_mainWindow->droppedURLs({ ev->url() });
}
return QApplication::event(event);
}
@ -1122,60 +1109,25 @@ std::shared_ptr<JavaInstallList> Application::javalist()
return m_javalist;
}
std::vector<ITheme *> Application::getValidApplicationThemes()
QList<ITheme*> Application::getValidApplicationThemes()
{
std::vector<ITheme *> ret;
auto iter = m_themes.cbegin();
while (iter != m_themes.cend())
{
ret.push_back((*iter).second.get());
iter++;
}
return ret;
}
bool Application::isFlatpak()
{
#ifdef Q_OS_LINUX
return QFile::exists("/.flatpak-info");
#else
return false;
#endif
return m_themeManager->getValidApplicationThemes();
}
void Application::setApplicationTheme(const QString& name, bool initial)
{
auto systemPalette = qApp->palette();
auto themeIter = m_themes.find(name);
if(themeIter != m_themes.end())
{
auto & theme = (*themeIter).second;
theme->apply(initial);
#ifdef Q_OS_WIN
if (m_mainWindow) {
if (QString::compare(theme->id(), "dark") == 0) {
WinDarkmode::setDarkWinTitlebar(m_mainWindow->winId(), true);
} else {
WinDarkmode::setDarkWinTitlebar(m_mainWindow->winId(), false);
}
}
#endif
}
else
{
qWarning() << "Tried to set invalid theme:" << name;
}
m_themeManager->setApplicationTheme(name, initial);
}
void Application::setIconTheme(const QString& name)
{
QIcon::setThemeName(name);
m_themeManager->setIconTheme(name);
}
QIcon Application::getThemedIcon(const QString& name)
{
if(name == "logo") {
return QIcon(":/org.prismlauncher.PrismLauncher.svg"); // FIXME: Make this a BuildConfig variable
return QIcon(":/" + BuildConfig.LAUNCHER_SVGFILENAME);
}
return QIcon::fromTheme(name);
}
@ -1393,10 +1345,13 @@ MainWindow* Application::showMainWindow(bool minimized)
m_mainWindow->restoreState(QByteArray::fromBase64(APPLICATION->settings()->get("MainWindowState").toByteArray()));
m_mainWindow->restoreGeometry(QByteArray::fromBase64(APPLICATION->settings()->get("MainWindowGeometry").toByteArray()));
#ifdef Q_OS_WIN
if (QString::compare(settings()->get("ApplicationTheme").toString(), "dark") == 0) {
WinDarkmode::setDarkWinTitlebar(m_mainWindow->winId(), true);
} else {
WinDarkmode::setDarkWinTitlebar(m_mainWindow->winId(), false);
if (IsWindows10OrGreater())
{
if (QString::compare(settings()->get("ApplicationTheme").toString(), "dark") == 0) {
WinDarkmode::setDarkWinTitlebar(m_mainWindow->winId(), true);
} else {
WinDarkmode::setDarkWinTitlebar(m_mainWindow->winId(), false);
}
}
#endif
if(minimized)
@ -1635,3 +1590,102 @@ QString Application::getUserAgentUncached()
return BuildConfig.USER_AGENT_UNCACHED;
}
int Application::suitableMaxMem()
{
float totalRAM = (float)Sys::getSystemRam() / (float)Sys::mebibyte;
int maxMemoryAlloc;
// If totalRAM < 6GB, use (totalRAM / 1.5), else 4GB
if (totalRAM < (4096 * 1.5))
maxMemoryAlloc = (int) (totalRAM / 1.5);
else
maxMemoryAlloc = 4096;
return maxMemoryAlloc;
}
bool Application::handleDataMigration(const QString& currentData,
const QString& oldData,
const QString& name,
const QString& configFile) const
{
QString nomigratePath = FS::PathCombine(currentData, name + "_nomigrate.txt");
QStringList configPaths = { FS::PathCombine(oldData, configFile), FS::PathCombine(oldData, BuildConfig.LAUNCHER_CONFIGFILE) };
QLocale locale;
// Is there a valid config at the old location?
bool configExists = false;
for (QString configPath : configPaths) {
configExists |= QFileInfo::exists(configPath);
}
if (!configExists || QFileInfo::exists(nomigratePath)) {
qDebug() << "<> No migration needed from" << name;
return false;
}
QString message;
bool currentExists = QFileInfo::exists(FS::PathCombine(currentData, BuildConfig.LAUNCHER_CONFIGFILE));
if (currentExists) {
message = tr("Old data from %1 was found, but you already have existing data for %2. Sadly you will need to migrate yourself. Do "
"you want to be reminded of the pending data migration next time you start %2?")
.arg(name, BuildConfig.LAUNCHER_DISPLAYNAME);
} else {
message = tr("It looks like you used %1 before. Do you want to migrate your data to the new location of %2?")
.arg(name, BuildConfig.LAUNCHER_DISPLAYNAME);
QFileInfo logInfo(FS::PathCombine(oldData, name + "-0.log"));
if (logInfo.exists()) {
QString lastModified = logInfo.lastModified().toString(locale.dateFormat());
message = tr("It looks like you used %1 on %2 before. Do you want to migrate your data to the new location of %3?")
.arg(name, lastModified, BuildConfig.LAUNCHER_DISPLAYNAME);
}
}
QMessageBox::StandardButton askMoveDialogue =
QMessageBox::question(nullptr, BuildConfig.LAUNCHER_DISPLAYNAME, message, QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes);
auto setDoNotMigrate = [&nomigratePath] {
QFile file(nomigratePath);
file.open(QIODevice::WriteOnly);
};
// create no-migrate file if user doesn't want to migrate
if (askMoveDialogue != QMessageBox::Yes) {
qDebug() << "<> Migration declined for" << name;
setDoNotMigrate();
return currentExists; // cancel further migrations, if we already have a data directory
}
if (!currentExists) {
// Migrate!
auto matcher = std::make_shared<MultiMatcher>();
matcher->add(std::make_shared<SimplePrefixMatcher>(configFile));
matcher->add(std::make_shared<SimplePrefixMatcher>(
BuildConfig.LAUNCHER_CONFIGFILE)); // it's possible that we already used that directory before
matcher->add(std::make_shared<SimplePrefixMatcher>("accounts.json"));
matcher->add(std::make_shared<SimplePrefixMatcher>("accounts/"));
matcher->add(std::make_shared<SimplePrefixMatcher>("assets/"));
matcher->add(std::make_shared<SimplePrefixMatcher>("icons/"));
matcher->add(std::make_shared<SimplePrefixMatcher>("instances/"));
matcher->add(std::make_shared<SimplePrefixMatcher>("libraries/"));
matcher->add(std::make_shared<SimplePrefixMatcher>("mods/"));
matcher->add(std::make_shared<SimplePrefixMatcher>("themes/"));
ProgressDialog diag;
DataMigrationTask task(nullptr, oldData, currentData, matcher);
if (diag.execWithTask(&task)) {
qDebug() << "<> Migration succeeded";
setDoNotMigrate();
} else {
QString reason = task.failReason();
QMessageBox::critical(nullptr, BuildConfig.LAUNCHER_DISPLAYNAME, tr("Migration failed! Reason: %1").arg(reason));
}
} else {
qWarning() << "<> Migration was skipped, due to existing data";
}
return true;
}

View File

@ -1,7 +1,8 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* PolyMC - Minecraft Launcher
* Prism Launcher - Minecraft Launcher
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
* Copyright (C) 2022 Tayou <tayou@gmx.net>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@ -68,6 +69,7 @@ class BaseDetachedToolFactory;
class TranslationsModel;
class ITheme;
class MCEditTool;
class ThemeManager;
namespace Meta {
class Index;
@ -116,11 +118,9 @@ public:
QIcon getThemedIcon(const QString& name);
bool isFlatpak();
void setIconTheme(const QString& name);
std::vector<ITheme *> getValidApplicationThemes();
QList<ITheme*> getValidApplicationThemes();
void setApplicationTheme(const QString& name, bool initial);
@ -200,6 +200,8 @@ public:
void ShowGlobalSettings(class QWidget * parent, QString open_page = QString());
int suitableMaxMem();
signals:
void updateAllowedChanged(bool status);
void globalSettingsAboutToOpen();
@ -229,6 +231,7 @@ private slots:
void setupWizardFinished(int status);
private:
bool handleDataMigration(const QString & currentData, const QString & oldData, const QString & name, const QString & configFile) const;
bool createSetupWizard();
void performMainStartupAction();
@ -257,9 +260,9 @@ private:
std::shared_ptr<JavaInstallList> m_javalist;
std::shared_ptr<TranslationsModel> m_translations;
std::shared_ptr<GenericPageProvider> m_globalSettingsProvider;
std::map<QString, std::unique_ptr<ITheme>> m_themes;
std::unique_ptr<MCEditTool> m_mcedit;
QSet<QString> m_features;
std::unique_ptr<ThemeManager> m_themeManager;
QMap<QString, std::shared_ptr<BaseProfilerFactory>> m_profilers;

View File

@ -17,13 +17,14 @@
#include <memory>
#include "BaseVersion.h"
class MinecraftInstance;
class QDir;
class QString;
class QObject;
class Task;
class BaseVersion;
typedef std::shared_ptr<BaseVersion> BaseVersionPtr;
class BaseInstaller
{
@ -35,7 +36,7 @@ public:
virtual bool add(MinecraftInstance *to);
virtual bool remove(MinecraftInstance *from);
virtual Task *createInstallTask(MinecraftInstance *instance, BaseVersionPtr version, QObject *parent) = 0;
virtual Task *createInstallTask(MinecraftInstance *instance, BaseVersion::Ptr version, QObject *parent) = 0;
protected:
virtual QString id() const = 0;

View File

@ -151,7 +151,7 @@ public:
void copyManagedPack(BaseInstance& other);
/// guess log level from a line of game log
virtual MessageLevel::Enum guessLevel(const QString &line, MessageLevel::Enum level)
virtual MessageLevel::Enum guessLevel([[maybe_unused]] const QString &line, MessageLevel::Enum level)
{
return level;
};

View File

@ -25,6 +25,7 @@
class BaseVersion
{
public:
using Ptr = std::shared_ptr<BaseVersion>;
virtual ~BaseVersion() {}
/*!
* A string used to identify this version in config files.
@ -54,6 +55,4 @@ public:
};
};
typedef std::shared_ptr<BaseVersion> BaseVersionPtr;
Q_DECLARE_METATYPE(BaseVersionPtr)
Q_DECLARE_METATYPE(BaseVersion::Ptr)

View File

@ -40,20 +40,20 @@ BaseVersionList::BaseVersionList(QObject *parent) : QAbstractListModel(parent)
{
}
BaseVersionPtr BaseVersionList::findVersion(const QString &descriptor)
BaseVersion::Ptr BaseVersionList::findVersion(const QString &descriptor)
{
for (int i = 0; i < count(); i++)
{
if (at(i)->descriptor() == descriptor)
return at(i);
}
return BaseVersionPtr();
return nullptr;
}
BaseVersionPtr BaseVersionList::getRecommended() const
BaseVersion::Ptr BaseVersionList::getRecommended() const
{
if (count() <= 0)
return BaseVersionPtr();
return nullptr;
else
return at(0);
}
@ -66,7 +66,7 @@ QVariant BaseVersionList::data(const QModelIndex &index, int role) const
if (index.row() > count())
return QVariant();
BaseVersionPtr version = at(index.row());
BaseVersion::Ptr version = at(index.row());
switch (role)
{
@ -95,12 +95,12 @@ BaseVersionList::RoleList BaseVersionList::providesRoles() const
int BaseVersionList::rowCount(const QModelIndex &parent) const
{
// Return count
return count();
return parent.isValid() ? 0 : count();
}
int BaseVersionList::columnCount(const QModelIndex &parent) const
{
return 1;
return parent.isValid() ? 0 : 1;
}
QHash<int, QByteArray> BaseVersionList::roleNames() const

View File

@ -70,7 +70,7 @@ public:
virtual bool isLoaded() = 0;
//! Gets the version at the given index.
virtual const BaseVersionPtr at(int i) const = 0;
virtual const BaseVersion::Ptr at(int i) const = 0;
//! Returns the number of versions in the list.
virtual int count() const = 0;
@ -90,13 +90,13 @@ public:
* \return A const pointer to the version with the given descriptor. NULL if
* one doesn't exist.
*/
virtual BaseVersionPtr findVersion(const QString &descriptor);
virtual BaseVersion::Ptr findVersion(const QString &descriptor);
/*!
* \brief Gets the recommended version from this list
* If the list doesn't support recommended versions, this works exactly as getLatestStable
*/
virtual BaseVersionPtr getRecommended() const;
virtual BaseVersion::Ptr getRecommended() const;
/*!
* Sorts the version list.
@ -117,5 +117,5 @@ slots:
* then copies the versions and sets their parents correctly.
* \param versions List of versions whose parents should be set.
*/
virtual void updateListData(QList<BaseVersionPtr> versions) = 0;
virtual void updateListData(QList<BaseVersion::Ptr> versions) = 0;
};

View File

@ -24,13 +24,15 @@ set(CORE_SOURCES
NullInstance.h
MMCZip.h
MMCZip.cpp
MMCStrings.h
MMCStrings.cpp
StringUtils.h
StringUtils.cpp
RuntimeContext.h
# Basic instance manipulation tasks (derived from InstanceTask)
InstanceCreationTask.h
InstanceCreationTask.cpp
InstanceCopyPrefs.h
InstanceCopyPrefs.cpp
InstanceCopyTask.h
InstanceCopyTask.cpp
InstanceImportTask.h
@ -95,6 +97,7 @@ set(PATHMATCHER_SOURCES
pathmatcher/IPathMatcher.h
pathmatcher/MultiMatcher.h
pathmatcher/RegexpMatcher.h
pathmatcher/SimplePrefixMatcher.h
)
set(NET_SOURCES
@ -539,9 +542,6 @@ set(ATLAUNCHER_SOURCES
################################ COMPILE ################################
# we need zlib
find_package(ZLIB REQUIRED)
set(LOGIC_SOURCES
${CORE_SOURCES}
${PATHMATCHER_SOURCES}
@ -576,6 +576,8 @@ SET(LAUNCHER_SOURCES
# Application base
Application.h
Application.cpp
DataMigrationTask.h
DataMigrationTask.cpp
UpdateController.cpp
UpdateController.h
ApplicationMessage.h
@ -595,9 +597,12 @@ SET(LAUNCHER_SOURCES
resources/pe_light/pe_light.qrc
resources/pe_colored/pe_colored.qrc
resources/pe_blue/pe_blue.qrc
resources/breeze_dark/breeze_dark.qrc
resources/breeze_light/breeze_light.qrc
resources/OSX/OSX.qrc
resources/iOS/iOS.qrc
resources/flat/flat.qrc
resources/flat_white/flat_white.qrc
resources/documents/documents.qrc
../${Launcher_Branding_LogoQRC}
@ -645,6 +650,8 @@ SET(LAUNCHER_SOURCES
ui/themes/ITheme.h
ui/themes/SystemTheme.cpp
ui/themes/SystemTheme.h
ui/themes/ThemeManager.cpp
ui/themes/ThemeManager.h
# Processes
LaunchController.h
@ -785,6 +792,8 @@ SET(LAUNCHER_SOURCES
ui/dialogs/ExportInstanceDialog.h
ui/dialogs/IconPickerDialog.cpp
ui/dialogs/IconPickerDialog.h
ui/dialogs/ImportResourcePackDialog.cpp
ui/dialogs/ImportResourcePackDialog.h
ui/dialogs/LoginDialog.cpp
ui/dialogs/LoginDialog.h
ui/dialogs/MSALoginDialog.cpp
@ -930,6 +939,7 @@ qt_wrap_ui(LAUNCHER_UI
ui/dialogs/SkinUploadDialog.ui
ui/dialogs/ExportInstanceDialog.ui
ui/dialogs/IconPickerDialog.ui
ui/dialogs/ImportResourcePackDialog.ui
ui/dialogs/MSALoginDialog.ui
ui/dialogs/OfflineLoginDialog.ui
ui/dialogs/AboutDialog.ui
@ -948,6 +958,8 @@ qt_add_resources(LAUNCHER_RESOURCES
resources/pe_light/pe_light.qrc
resources/pe_colored/pe_colored.qrc
resources/pe_blue/pe_blue.qrc
resources/breeze_dark/breeze_dark.qrc
resources/breeze_light/breeze_light.qrc
resources/OSX/OSX.qrc
resources/iOS/iOS.qrc
resources/flat/flat.qrc
@ -1054,96 +1066,95 @@ if(INSTALL_BUNDLE STREQUAL "full")
COMPONENT Runtime
)
# Bundle plugins
if(CMAKE_BUILD_TYPE STREQUAL "Debug" OR CMAKE_BUILD_TYPE STREQUAL "RelWithDebInfo")
# Image formats
# Image formats
install(
DIRECTORY "${QT_PLUGINS_DIR}/imageformats"
CONFIGURATIONS Debug RelWithDebInfo ""
DESTINATION ${PLUGIN_DEST_DIR}
COMPONENT Runtime
REGEX "tga|tiff|mng" EXCLUDE
)
install(
DIRECTORY "${QT_PLUGINS_DIR}/imageformats"
CONFIGURATIONS Release MinSizeRel
DESTINATION ${PLUGIN_DEST_DIR}
COMPONENT Runtime
REGEX "tga|tiff|mng" EXCLUDE
REGEX "d\\." EXCLUDE
REGEX "_debug\\." EXCLUDE
REGEX "\\.dSYM" EXCLUDE
)
# Icon engines
install(
DIRECTORY "${QT_PLUGINS_DIR}/iconengines"
CONFIGURATIONS Debug RelWithDebInfo ""
DESTINATION ${PLUGIN_DEST_DIR}
COMPONENT Runtime
REGEX "fontawesome" EXCLUDE
)
install(
DIRECTORY "${QT_PLUGINS_DIR}/iconengines"
CONFIGURATIONS Release MinSizeRel
DESTINATION ${PLUGIN_DEST_DIR}
COMPONENT Runtime
REGEX "fontawesome" EXCLUDE
REGEX "d\\." EXCLUDE
REGEX "_debug\\." EXCLUDE
REGEX "\\.dSYM" EXCLUDE
)
# Platform plugins
install(
DIRECTORY "${QT_PLUGINS_DIR}/platforms"
CONFIGURATIONS Debug RelWithDebInfo ""
DESTINATION ${PLUGIN_DEST_DIR}
COMPONENT Runtime
REGEX "minimal|linuxfb|offscreen" EXCLUDE
)
install(
DIRECTORY "${QT_PLUGINS_DIR}/platforms"
CONFIGURATIONS Release MinSizeRel
DESTINATION ${PLUGIN_DEST_DIR}
COMPONENT Runtime
REGEX "minimal|linuxfb|offscreen" EXCLUDE
REGEX "[^2]d\\." EXCLUDE
REGEX "_debug\\." EXCLUDE
REGEX "\\.dSYM" EXCLUDE
)
# Style plugins
if(EXISTS "${QT_PLUGINS_DIR}/styles")
install(
DIRECTORY "${QT_PLUGINS_DIR}/imageformats"
DIRECTORY "${QT_PLUGINS_DIR}/styles"
CONFIGURATIONS Debug RelWithDebInfo ""
DESTINATION ${PLUGIN_DEST_DIR}
COMPONENT Runtime
REGEX "tga|tiff|mng" EXCLUDE
)
# Icon engines
install(
DIRECTORY "${QT_PLUGINS_DIR}/iconengines"
DIRECTORY "${QT_PLUGINS_DIR}/styles"
CONFIGURATIONS Release MinSizeRel
DESTINATION ${PLUGIN_DEST_DIR}
COMPONENT Runtime
REGEX "fontawesome" EXCLUDE
)
# Platform plugins
install(
DIRECTORY "${QT_PLUGINS_DIR}/platforms"
DESTINATION ${PLUGIN_DEST_DIR}
COMPONENT Runtime
REGEX "minimal|linuxfb|offscreen" EXCLUDE
)
# Style plugins
if(EXISTS "${QT_PLUGINS_DIR}/styles")
install(
DIRECTORY "${QT_PLUGINS_DIR}/styles"
DESTINATION ${PLUGIN_DEST_DIR}
COMPONENT Runtime
)
endif()
# TLS plugins (Qt 6 only)
if(EXISTS "${QT_PLUGINS_DIR}/tls")
install(
DIRECTORY "${QT_PLUGINS_DIR}/tls"
DESTINATION ${PLUGIN_DEST_DIR}
COMPONENT Runtime
)
endif()
else()
# Image formats
install(
DIRECTORY "${QT_PLUGINS_DIR}/imageformats"
DESTINATION ${PLUGIN_DEST_DIR}
COMPONENT Runtime
REGEX "tga|tiff|mng" EXCLUDE
REGEX "d\\." EXCLUDE
REGEX "_debug\\." EXCLUDE
REGEX "\\.dSYM" EXCLUDE
)
# Icon engines
endif()
# TLS plugins (Qt 6 only)
if(EXISTS "${QT_PLUGINS_DIR}/tls")
install(
DIRECTORY "${QT_PLUGINS_DIR}/iconengines"
DIRECTORY "${QT_PLUGINS_DIR}/tls"
CONFIGURATIONS Debug RelWithDebInfo ""
DESTINATION ${PLUGIN_DEST_DIR}
COMPONENT Runtime
REGEX "fontawesome" EXCLUDE
REGEX "d\\." EXCLUDE
)
install(
DIRECTORY "${QT_PLUGINS_DIR}/tls"
CONFIGURATIONS Release MinSizeRel
DESTINATION ${PLUGIN_DEST_DIR}
COMPONENT Runtime
REGEX "dd\\." EXCLUDE
REGEX "_debug\\." EXCLUDE
REGEX "\\.dSYM" EXCLUDE
)
# Platform plugins
install(
DIRECTORY "${QT_PLUGINS_DIR}/platforms"
DESTINATION ${PLUGIN_DEST_DIR}
COMPONENT Runtime
REGEX "minimal|linuxfb|offscreen" EXCLUDE
REGEX "d\\." EXCLUDE
REGEX "_debug\\." EXCLUDE
REGEX "\\.dSYM" EXCLUDE
)
# Style plugins
if(EXISTS "${QT_PLUGINS_DIR}/styles")
install(
DIRECTORY "${QT_PLUGINS_DIR}/styles"
DESTINATION ${PLUGIN_DEST_DIR}
COMPONENT Runtime
REGEX "d\\." EXCLUDE
REGEX "_debug\\." EXCLUDE
REGEX "\\.dSYM" EXCLUDE
)
endif()
# TLS plugins (Qt 6 only)
if(EXISTS "${QT_PLUGINS_DIR}/tls")
install(
DIRECTORY "${QT_PLUGINS_DIR}/tls"
DESTINATION ${PLUGIN_DEST_DIR}
COMPONENT Runtime
REGEX "_debug\\." EXCLUDE
REGEX "\\.dSYM" EXCLUDE
)
endif()
endif()
configure_file(
"${CMAKE_CURRENT_SOURCE_DIR}/install_prereqs.cmake.in"

View File

@ -0,0 +1,96 @@
// SPDX-FileCopyrightText: 2022 Sefa Eyeoglu <contact@scrumplex.net>
//
// SPDX-License-Identifier: GPL-3.0-only
#include "DataMigrationTask.h"
#include "FileSystem.h"
#include <QDirIterator>
#include <QFileInfo>
#include <QMap>
#include <QtConcurrent>
DataMigrationTask::DataMigrationTask(QObject* parent,
const QString& sourcePath,
const QString& targetPath,
const IPathMatcher::Ptr pathMatcher)
: Task(parent), m_sourcePath(sourcePath), m_targetPath(targetPath), m_pathMatcher(pathMatcher), m_copy(sourcePath, targetPath)
{
m_copy.matcher(m_pathMatcher.get()).whitelist(true);
}
void DataMigrationTask::executeTask()
{
setStatus(tr("Scanning files..."));
// 1. Scan
// Check how many files we gotta copy
m_copyFuture = QtConcurrent::run(QThreadPool::globalInstance(), [&] {
return m_copy(true); // dry run to collect amount of files
});
connect(&m_copyFutureWatcher, &QFutureWatcher<bool>::finished, this, &DataMigrationTask::dryRunFinished);
connect(&m_copyFutureWatcher, &QFutureWatcher<bool>::canceled, this, &DataMigrationTask::dryRunAborted);
m_copyFutureWatcher.setFuture(m_copyFuture);
}
void DataMigrationTask::dryRunFinished()
{
disconnect(&m_copyFutureWatcher, &QFutureWatcher<bool>::finished, this, &DataMigrationTask::dryRunFinished);
disconnect(&m_copyFutureWatcher, &QFutureWatcher<bool>::canceled, this, &DataMigrationTask::dryRunAborted);
#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0))
if (!m_copyFuture.isValid() || !m_copyFuture.result()) {
#else
if (!m_copyFuture.result()) {
#endif
emitFailed(tr("Failed to scan source path."));
return;
}
// 2. Copy
// Actually copy all files now.
m_toCopy = m_copy.totalCopied();
connect(&m_copy, &FS::copy::fileCopied, [&, this](const QString& relativeName) {
QString shortenedName = relativeName;
// shorten the filename to hopefully fit into one line
if (shortenedName.length() > 50)
shortenedName = relativeName.left(20) + "" + relativeName.right(29);
setProgress(m_copy.totalCopied(), m_toCopy);
setStatus(tr("Copying %1…").arg(shortenedName));
});
m_copyFuture = QtConcurrent::run(QThreadPool::globalInstance(), [&] {
return m_copy(false); // actually copy now
});
connect(&m_copyFutureWatcher, &QFutureWatcher<bool>::finished, this, &DataMigrationTask::copyFinished);
connect(&m_copyFutureWatcher, &QFutureWatcher<bool>::canceled, this, &DataMigrationTask::copyAborted);
m_copyFutureWatcher.setFuture(m_copyFuture);
}
void DataMigrationTask::dryRunAborted()
{
emitFailed(tr("Aborted"));
}
void DataMigrationTask::copyFinished()
{
disconnect(&m_copyFutureWatcher, &QFutureWatcher<bool>::finished, this, &DataMigrationTask::copyFinished);
disconnect(&m_copyFutureWatcher, &QFutureWatcher<bool>::canceled, this, &DataMigrationTask::copyAborted);
#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0))
if (!m_copyFuture.isValid() || !m_copyFuture.result()) {
#else
if (!m_copyFuture.result()) {
#endif
emitFailed(tr("Some paths could not be copied!"));
return;
}
emitSucceeded();
}
void DataMigrationTask::copyAborted()
{
emitFailed(tr("Aborted"));
}

View File

@ -0,0 +1,42 @@
// SPDX-FileCopyrightText: 2022 Sefa Eyeoglu <contact@scrumplex.net>
//
// SPDX-License-Identifier: GPL-3.0-only
#pragma once
#include "FileSystem.h"
#include "pathmatcher/IPathMatcher.h"
#include "tasks/Task.h"
#include <QFuture>
#include <QFutureWatcher>
/*
* Migrate existing data from other MMC-like launchers.
*/
class DataMigrationTask : public Task {
Q_OBJECT
public:
explicit DataMigrationTask(QObject* parent, const QString& sourcePath, const QString& targetPath, const IPathMatcher::Ptr pathmatcher);
~DataMigrationTask() override = default;
protected:
virtual void executeTask() override;
protected slots:
void dryRunFinished();
void dryRunAborted();
void copyFinished();
void copyAborted();
private:
const QString& m_sourcePath;
const QString& m_targetPath;
const IPathMatcher::Ptr m_pathMatcher;
FS::copy m_copy;
int m_toCopy = 0;
QFuture<bool> m_copyFuture;
QFutureWatcher<bool> m_copyFutureWatcher;
};

View File

@ -119,7 +119,7 @@ bool openDirectory(const QString &path, bool ensureExists)
return QDesktopServices::openUrl(QUrl::fromLocalFile(dir.absolutePath()));
};
#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD)
if(!APPLICATION->isFlatpak())
if(!isFlatpak())
{
return IndirectOpen(f);
}
@ -140,7 +140,7 @@ bool openFile(const QString &path)
return QDesktopServices::openUrl(QUrl::fromLocalFile(path));
};
#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD)
if(!APPLICATION->isFlatpak())
if(!isFlatpak())
{
return IndirectOpen(f);
}
@ -158,7 +158,7 @@ bool openFile(const QString &application, const QString &path, const QString &wo
qDebug() << "Opening file" << path << "using" << application;
#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD)
// FIXME: the pid here is fake. So if something depends on it, it will likely misbehave
if(!APPLICATION->isFlatpak())
if(!isFlatpak())
{
return IndirectOpen([&]()
{
@ -178,7 +178,7 @@ bool run(const QString &application, const QStringList &args, const QString &wor
{
qDebug() << "Running" << application << "with args" << args.join(' ');
#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD)
if(!APPLICATION->isFlatpak())
if(!isFlatpak())
{
// FIXME: the pid here is fake. So if something depends on it, it will likely misbehave
return IndirectOpen([&]()
@ -203,7 +203,7 @@ bool openUrl(const QUrl &url)
return QDesktopServices::openUrl(url);
};
#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD)
if(!APPLICATION->isFlatpak())
if(!isFlatpak())
{
return IndirectOpen(f);
}
@ -216,4 +216,13 @@ bool openUrl(const QUrl &url)
#endif
}
bool isFlatpak()
{
#ifdef Q_OS_LINUX
return QFile::exists("/.flatpak-info");
#else
return false;
#endif
}
}

View File

@ -33,4 +33,6 @@ namespace DesktopServices
* Open the URL, most likely in a browser. Maybe.
*/
bool openUrl(const QUrl &url);
bool isFlatpak();
}

View File

@ -45,7 +45,11 @@
#include <QTextStream>
#include <QUrl>
#include "DesktopServices.h"
#include "StringUtils.h"
#if defined Q_OS_WIN32
#define WIN32_LEAN_AND_MEAN
#include <objbase.h>
#include <objidl.h>
#include <shlguid.h>
@ -78,22 +82,6 @@ namespace fs = std::filesystem;
namespace fs = ghc::filesystem;
#endif
#if defined Q_OS_WIN32
std::wstring toStdString(QString s)
{
return s.toStdWString();
}
#else
std::string toStdString(QString s)
{
return s.toStdString();
}
#endif
namespace FS {
void ensureExists(const QDir& dir)
@ -162,9 +150,13 @@ bool ensureFolderPathExists(QString foldernamepath)
return success;
}
bool copy::operator()(const QString& offset)
/// @brief Copies a directory and it's contents from src to dest
/// @param offset subdirectory form src to copy to dest
/// @return if there was an error during the filecopy
bool copy::operator()(const QString& offset, bool dryRun)
{
using copy_opts = fs::copy_options;
m_copied = 0; // reset counter
// NOTE always deep copy on windows. the alternatives are too messy.
#if defined Q_OS_WIN32
@ -182,6 +174,24 @@ bool copy::operator()(const QString& offset)
if (!m_followSymlinks)
opt |= copy_opts::copy_symlinks;
// Function that'll do the actual copying
auto copy_file = [&](QString src_path, QString relative_dst_path) {
if (m_matcher && (m_matcher->matches(relative_dst_path) != m_whitelist))
return;
auto dst_path = PathCombine(dst, relative_dst_path);
if (!dryRun) {
ensureFilePathExists(dst_path);
fs::copy(StringUtils::toStdString(src_path), StringUtils::toStdString(dst_path), opt, err);
}
if (err) {
qWarning() << "Failed to copy files:" << QString::fromStdString(err.message());
qDebug() << "Source file:" << src_path;
qDebug() << "Destination file:" << dst_path;
}
m_copied++;
emit fileCopied(relative_dst_path);
};
// We can't use copy_opts::recursive because we need to take into account the
// blacklisted paths, so we iterate over the source directory, and if there's no blacklist
@ -193,20 +203,13 @@ bool copy::operator()(const QString& offset)
auto src_path = source_it.next();
auto relative_path = src_dir.relativeFilePath(src_path);
if (m_blacklist && m_blacklist->matches(relative_path))
continue;
auto dst_path = PathCombine(dst, relative_path);
ensureFilePathExists(dst_path);
fs::copy(toStdString(src_path), toStdString(dst_path), opt, err);
if (err) {
qWarning() << "Failed to copy files:" << QString::fromStdString(err.message());
qDebug() << "Source file:" << src_path;
qDebug() << "Destination file:" << dst_path;
}
copy_file(src_path, relative_path);
}
// If the root src is not a directory, the previous iterator won't run.
if (!fs::is_directory(StringUtils::toStdString(src)))
copy_file(src, "");
return err.value() == 0;
}
@ -214,7 +217,7 @@ bool deletePath(QString path)
{
std::error_code err;
fs::remove_all(toStdString(path), err);
fs::remove_all(StringUtils::toStdString(path), err);
if (err) {
qWarning() << "Failed to remove files:" << QString::fromStdString(err.message());
@ -228,6 +231,9 @@ bool trash(QString path, QString *pathInTrash = nullptr)
#if QT_VERSION < QT_VERSION_CHECK(5, 15, 0)
return false;
#else
// FIXME: Figure out trash in Flatpak. Qt seemingly doesn't use the Trash portal
if (DesktopServices::isFlatpak())
return false;
return QFile::moveToTrash(path, pathInTrash);
#endif
}
@ -338,12 +344,37 @@ QString getDesktopDir()
}
// Cross-platform Shortcut creation
bool createShortCut(QString location, QString dest, QStringList args, QString name, QString icon)
bool createShortcut(QString destination, QString target, QStringList args, QString name, QString icon)
{
#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD)
location = PathCombine(location, name + ".desktop");
#if defined(Q_OS_MACOS)
destination += ".command";
QFile f(location);
QFile f(destination);
f.open(QIODevice::WriteOnly | QIODevice::Text);
QTextStream stream(&f);
QString argstring;
if (!args.empty())
argstring = " \"" + args.join("\" \"") + "\"";
stream << "#!/bin/bash"
<< "\n";
stream << "\""
<< target
<< "\" "
<< argstring
<< "\n";
stream.flush();
f.close();
f.setPermissions(f.permissions() | QFileDevice::ExeOwner | QFileDevice::ExeGroup | QFileDevice::ExeOther);
return true;
#elif defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) || defined(Q_OS_OPENBSD)
destination += ".desktop";
QFile f(destination);
f.open(QIODevice::WriteOnly | QIODevice::Text);
QTextStream stream(&f);
@ -355,10 +386,12 @@ bool createShortCut(QString location, QString dest, QStringList args, QString na
<< "\n";
stream << "Type=Application"
<< "\n";
stream << "TryExec=" << dest.toLocal8Bit() << "\n";
stream << "Exec=" << dest.toLocal8Bit() << argstring.toLocal8Bit() << "\n";
stream << "Exec=\"" << target.toLocal8Bit() << "\"" << argstring.toLocal8Bit() << "\n";
stream << "Name=" << name.toLocal8Bit() << "\n";
stream << "Icon=" << icon.toLocal8Bit() << "\n";
if (!icon.isEmpty())
{
stream << "Icon=" << icon.toLocal8Bit() << "\n";
}
stream.flush();
f.close();
@ -366,25 +399,132 @@ bool createShortCut(QString location, QString dest, QStringList args, QString na
f.setPermissions(f.permissions() | QFileDevice::ExeOwner | QFileDevice::ExeGroup | QFileDevice::ExeOther);
return true;
#elif defined Q_OS_WIN
// TODO: Fix
// QFile file(PathCombine(location, name + ".lnk"));
// WCHAR *file_w;
// WCHAR *dest_w;
// WCHAR *args_w;
// file.fileName().toWCharArray(file_w);
// dest.toWCharArray(dest_w);
#elif defined(Q_OS_WIN)
QFileInfo targetInfo(target);
// QString argStr;
// for (int i = 0; i < args.count(); i++)
// {
// argStr.append(args[i]);
// argStr.append(" ");
// }
// argStr.toWCharArray(args_w);
if (!targetInfo.exists())
{
qWarning() << "Target file does not exist!";
return false;
}
// return SUCCEEDED(CreateLink(file_w, dest_w, args_w));
return false;
target = targetInfo.absoluteFilePath();
if (target.length() >= MAX_PATH)
{
qWarning() << "Target file path is too long!";
return false;
}
if (!icon.isEmpty() && icon.length() >= MAX_PATH)
{
qWarning() << "Icon path is too long!";
return false;
}
destination += ".lnk";
if (destination.length() >= MAX_PATH)
{
qWarning() << "Destination path is too long!";
return false;
}
QString argStr;
int argCount = args.count();
for (int i = 0; i < argCount; i++)
{
if (args[i].contains(' '))
{
argStr.append('"').append(args[i]).append('"');
}
else
{
argStr.append(args[i]);
}
if (i < argCount - 1)
{
argStr.append(" ");
}
}
if (argStr.length() >= MAX_PATH)
{
qWarning() << "Arguments string is too long!";
return false;
}
HRESULT hres;
// ...yes, you need to initialize the entire COM stack just to make a shortcut
hres = CoInitialize(nullptr);
if (FAILED(hres))
{
qWarning() << "Failed to initialize COM!";
return false;
}
WCHAR wsz[MAX_PATH];
IShellLink* psl;
// create an IShellLink instance - this stores the shortcut's attributes
hres = CoCreateInstance(CLSID_ShellLink, NULL, CLSCTX_INPROC_SERVER, IID_IShellLink, (LPVOID*)&psl);
if (SUCCEEDED(hres))
{
wmemset(wsz, 0, MAX_PATH);
target.toWCharArray(wsz);
psl->SetPath(wsz);
wmemset(wsz, 0, MAX_PATH);
argStr.toWCharArray(wsz);
psl->SetArguments(wsz);
wmemset(wsz, 0, MAX_PATH);
targetInfo.absolutePath().toWCharArray(wsz);
psl->SetWorkingDirectory(wsz); // "Starts in" attribute
if (!icon.isEmpty())
{
wmemset(wsz, 0, MAX_PATH);
icon.toWCharArray(wsz);
psl->SetIconLocation(wsz, 0);
}
// query an IPersistFile interface from our IShellLink instance
// this is the interface that will actually let us save the shortcut to disk!
IPersistFile* ppf;
hres = psl->QueryInterface(IID_IPersistFile, (LPVOID*)&ppf);
if (SUCCEEDED(hres))
{
wmemset(wsz, 0, MAX_PATH);
destination.toWCharArray(wsz);
hres = ppf->Save(wsz, TRUE);
if (FAILED(hres))
{
qWarning() << "IPresistFile->Save() failed";
qWarning() << "hres = " << hres;
}
ppf->Release();
}
else
{
qWarning() << "Failed to query IPersistFile interface from IShellLink instance";
qWarning() << "hres = " << hres;
}
psl->Release();
}
else
{
qWarning() << "Failed to create IShellLink instance";
qWarning() << "hres = " << hres;
}
// go away COM, nobody likes you
CoUninitialize();
return SUCCEEDED(hres);
#else
qWarning("Desktop Shortcuts not supported on your platform!");
return false;
@ -401,7 +541,8 @@ bool overrideFolder(QString overwritten_path, QString override_path)
std::error_code err;
fs::copy_options opt = copy_opts::recursive | copy_opts::overwrite_existing;
fs::copy(toStdString(override_path), toStdString(overwritten_path), opt, err);
// FIXME: hello traveller! Apparently std::copy does NOT overwrite existing files on GNU libstdc++ on Windows?
fs::copy(StringUtils::toStdString(override_path), StringUtils::toStdString(overwritten_path), opt, err);
if (err) {
qCritical() << QString("Failed to apply override from %1 to %2").arg(override_path, overwritten_path);

View File

@ -40,6 +40,7 @@
#include <QDir>
#include <QFlags>
#include <QObject>
namespace FS {
@ -75,9 +76,11 @@ bool ensureFilePathExists(QString filenamepath);
*/
bool ensureFolderPathExists(QString filenamepath);
class copy {
/// @brief Copies a directory and it's contents from src to dest
class copy : public QObject {
Q_OBJECT
public:
copy(const QString& src, const QString& dst)
copy(const QString& src, const QString& dst, QObject* parent = nullptr) : QObject(parent)
{
m_src.setPath(src);
m_dst.setPath(dst);
@ -87,21 +90,35 @@ class copy {
m_followSymlinks = follow;
return *this;
}
copy& blacklist(const IPathMatcher* filter)
copy& matcher(const IPathMatcher* filter)
{
m_blacklist = filter;
m_matcher = filter;
return *this;
}
bool operator()() { return operator()(QString()); }
copy& whitelist(bool whitelist)
{
m_whitelist = whitelist;
return *this;
}
bool operator()(bool dryRun = false) { return operator()(QString(), dryRun); }
int totalCopied() { return m_copied; }
signals:
void fileCopied(const QString& relativeName);
// TODO: maybe add a "shouldCopy" signal in the future?
private:
bool operator()(const QString& offset);
bool operator()(const QString& offset, bool dryRun = false);
private:
bool m_followSymlinks = true;
const IPathMatcher* m_blacklist = nullptr;
const IPathMatcher* m_matcher = nullptr;
bool m_whitelist = false;
QDir m_src;
QDir m_dst;
int m_copied;
};
/**
@ -155,4 +172,9 @@ QString getDesktopDir();
// Overrides one folder with the contents of another, preserving items exclusive to the first folder
// Equivalent to doing QDir::rename, but allowing for overrides
bool overrideFolder(QString overwritten_path, QString override_path);
/**
* Creates a shortcut to the specified target file at the specified destination path.
*/
bool createShortcut(QString destination, QString target, QStringList args, QString name, QString icon);
}

View File

@ -0,0 +1,135 @@
//
// Created by marcelohdez on 10/22/22.
//
#include "InstanceCopyPrefs.h"
bool InstanceCopyPrefs::allTrue() const
{
return copySaves &&
keepPlaytime &&
copyGameOptions &&
copyResourcePacks &&
copyShaderPacks &&
copyServers &&
copyMods &&
copyScreenshots;
}
// Returns a single RegEx string of the selected folders/files to filter out (ex: ".minecraft/saves|.minecraft/server.dat")
QString InstanceCopyPrefs::getSelectedFiltersAsRegex() const
{
QStringList filters;
if(!copySaves)
filters << "saves";
if(!copyGameOptions)
filters << "options.txt";
if(!copyResourcePacks)
filters << "resourcepacks" << "texturepacks";
if(!copyShaderPacks)
filters << "shaderpacks";
if(!copyServers)
filters << "servers.dat" << "servers.dat_old" << "server-resource-packs";
if(!copyMods)
filters << "coremods" << "mods" << "config";
if(!copyScreenshots)
filters << "screenshots";
// If we have any filters to add, join them as a single regex string to return:
if (!filters.isEmpty()) {
const QString MC_ROOT = "[.]?minecraft/";
// Ensure first filter starts with root, then join other filters with OR regex before root (ex: ".minecraft/saves|.minecraft/mods"):
return MC_ROOT + filters.join("|" + MC_ROOT);
}
return {};
}
// ======= Getters =======
bool InstanceCopyPrefs::isCopySavesEnabled() const
{
return copySaves;
}
bool InstanceCopyPrefs::isKeepPlaytimeEnabled() const
{
return keepPlaytime;
}
bool InstanceCopyPrefs::isCopyGameOptionsEnabled() const
{
return copyGameOptions;
}
bool InstanceCopyPrefs::isCopyResourcePacksEnabled() const
{
return copyResourcePacks;
}
bool InstanceCopyPrefs::isCopyShaderPacksEnabled() const
{
return copyShaderPacks;
}
bool InstanceCopyPrefs::isCopyServersEnabled() const
{
return copyServers;
}
bool InstanceCopyPrefs::isCopyModsEnabled() const
{
return copyMods;
}
bool InstanceCopyPrefs::isCopyScreenshotsEnabled() const
{
return copyScreenshots;
}
// ======= Setters =======
void InstanceCopyPrefs::enableCopySaves(bool b)
{
copySaves = b;
}
void InstanceCopyPrefs::enableKeepPlaytime(bool b)
{
keepPlaytime = b;
}
void InstanceCopyPrefs::enableCopyGameOptions(bool b)
{
copyGameOptions = b;
}
void InstanceCopyPrefs::enableCopyResourcePacks(bool b)
{
copyResourcePacks = b;
}
void InstanceCopyPrefs::enableCopyShaderPacks(bool b)
{
copyShaderPacks = b;
}
void InstanceCopyPrefs::enableCopyServers(bool b)
{
copyServers = b;
}
void InstanceCopyPrefs::enableCopyMods(bool b)
{
copyMods = b;
}
void InstanceCopyPrefs::enableCopyScreenshots(bool b)
{
copyScreenshots = b;
}

View File

@ -0,0 +1,41 @@
//
// Created by marcelohdez on 10/22/22.
//
#pragma once
#include <QStringList>
struct InstanceCopyPrefs {
public:
[[nodiscard]] bool allTrue() const;
[[nodiscard]] QString getSelectedFiltersAsRegex() const;
// Getters
[[nodiscard]] bool isCopySavesEnabled() const;
[[nodiscard]] bool isKeepPlaytimeEnabled() const;
[[nodiscard]] bool isCopyGameOptionsEnabled() const;
[[nodiscard]] bool isCopyResourcePacksEnabled() const;
[[nodiscard]] bool isCopyShaderPacksEnabled() const;
[[nodiscard]] bool isCopyServersEnabled() const;
[[nodiscard]] bool isCopyModsEnabled() const;
[[nodiscard]] bool isCopyScreenshotsEnabled() const;
// Setters
void enableCopySaves(bool b);
void enableKeepPlaytime(bool b);
void enableCopyGameOptions(bool b);
void enableCopyResourcePacks(bool b);
void enableCopyShaderPacks(bool b);
void enableCopyServers(bool b);
void enableCopyMods(bool b);
void enableCopyScreenshots(bool b);
protected: // data
bool copySaves = true;
bool keepPlaytime = true;
bool copyGameOptions = true;
bool copyResourcePacks = true;
bool copyShaderPacks = true;
bool copyServers = true;
bool copyMods = true;
bool copyScreenshots = true;
};

View File

@ -5,15 +5,17 @@
#include "pathmatcher/RegexpMatcher.h"
#include <QtConcurrentRun>
InstanceCopyTask::InstanceCopyTask(InstancePtr origInstance, bool copySaves, bool keepPlaytime)
InstanceCopyTask::InstanceCopyTask(InstancePtr origInstance, const InstanceCopyPrefs& prefs)
{
m_origInstance = origInstance;
m_keepPlaytime = keepPlaytime;
m_keepPlaytime = prefs.isKeepPlaytimeEnabled();
if(!copySaves)
QString filters = prefs.getSelectedFiltersAsRegex();
if (!filters.isEmpty())
{
// Set regex filter:
// FIXME: get this from the original instance type...
auto matcherReal = new RegexpMatcher("[.]?minecraft/saves");
auto matcherReal = new RegexpMatcher(filters);
matcherReal->caseSensitive(false);
m_matcher.reset(matcherReal);
}
@ -23,10 +25,12 @@ void InstanceCopyTask::executeTask()
{
setStatus(tr("Copying instance %1").arg(m_origInstance->name()));
FS::copy folderCopy(m_origInstance->instanceRoot(), m_stagingPath);
folderCopy.followSymlinks(false).blacklist(m_matcher.get());
m_copyFuture = QtConcurrent::run(QThreadPool::globalInstance(), [this]{
FS::copy folderCopy(m_origInstance->instanceRoot(), m_stagingPath);
folderCopy.followSymlinks(false).matcher(m_matcher.get());
m_copyFuture = QtConcurrent::run(QThreadPool::globalInstance(), folderCopy);
return folderCopy();
});
connect(&m_copyFutureWatcher, &QFutureWatcher<bool>::finished, this, &InstanceCopyTask::copyFinished);
connect(&m_copyFutureWatcher, &QFutureWatcher<bool>::canceled, this, &InstanceCopyTask::copyAborted);
m_copyFutureWatcher.setFuture(m_copyFuture);

View File

@ -1,20 +1,21 @@
#pragma once
#include "tasks/Task.h"
#include "net/NetJob.h"
#include <QUrl>
#include <QFuture>
#include <QFutureWatcher>
#include "settings/SettingsObject.h"
#include "BaseVersion.h"
#include <QUrl>
#include "BaseInstance.h"
#include "BaseVersion.h"
#include "InstanceCopyPrefs.h"
#include "InstanceTask.h"
#include "net/NetJob.h"
#include "settings/SettingsObject.h"
#include "tasks/Task.h"
class InstanceCopyTask : public InstanceTask
{
Q_OBJECT
public:
explicit InstanceCopyTask(InstancePtr origInstance, bool copySaves, bool keepPlaytime);
explicit InstanceCopyTask(InstancePtr origInstance, const InstanceCopyPrefs& prefs);
protected:
//! Entry point for tasks.
@ -22,7 +23,8 @@ protected:
void copyFinished();
void copyAborted();
private: /* data */
private:
/* data */
InstancePtr m_origInstance;
QFuture<bool> m_copyFuture;
QFutureWatcher<bool> m_copyFutureWatcher;

View File

@ -25,9 +25,13 @@ void InstanceCreationTask::executeTask()
return;
qWarning() << "Instance creation failed!";
if (!m_error_message.isEmpty())
if (!m_error_message.isEmpty()) {
qWarning() << "Reason: " << m_error_message;
emitFailed(tr("Error while creating new instance."));
emitFailed(tr("Error while creating new instance:\n%1").arg(m_error_message));
} else {
emitFailed(tr("Error while creating new instance."));
}
return;
}

View File

@ -164,18 +164,14 @@ void InstanceImportTask::processZipPack()
}
else
{
QString mmcRoot = MMCZip::findFolderOfFileInZip(m_packZip.get(), "instance.cfg");
QString flameRoot = MMCZip::findFolderOfFileInZip(m_packZip.get(), "manifest.json");
QStringList paths_to_ignore { "overrides/" };
if (!mmcRoot.isNull())
{
if (QString mmcRoot = MMCZip::findFolderOfFileInZip(m_packZip.get(), "instance.cfg", paths_to_ignore); !mmcRoot.isNull()) {
// process as MultiMC instance/pack
qDebug() << "MultiMC:" << mmcRoot;
root = mmcRoot;
m_modpackType = ModpackType::MultiMC;
}
else if(!flameRoot.isNull())
{
} else if (QString flameRoot = MMCZip::findFolderOfFileInZip(m_packZip.get(), "manifest.json", paths_to_ignore); !flameRoot.isNull()) {
// process as Flame pack
qDebug() << "Flame:" << flameRoot;
root = flameRoot;

View File

@ -36,7 +36,7 @@
#include "JavaCommon.h"
#include "java/JavaUtils.h"
#include "ui/dialogs/CustomMessageBox.h"
#include <MMCStrings.h>
#include <QRegularExpression>
bool JavaCommon::checkJVMArgs(QString jvmargs, QWidget *parent)

View File

@ -1,8 +0,0 @@
#pragma once
#include <QString>
namespace Strings
{
int naturalCompare(const QString &s1, const QString &s2, Qt::CaseSensitivity cs);
}

View File

@ -28,11 +28,11 @@ QString Time::prettifyDuration(int64_t duration) {
int days = (int) (duration / 24);
if((hours == 0)&&(days == 0))
{
return QObject::tr("%1m %2s").arg(minutes).arg(seconds);
return QObject::tr("%1min %2s").arg(minutes).arg(seconds);
}
if (days == 0)
{
return QObject::tr("%1h %2m").arg(hours).arg(minutes);
return QObject::tr("%1h %2min").arg(hours).arg(minutes);
}
return QObject::tr("%1d %2h %3m").arg(days).arg(hours).arg(minutes);
return QObject::tr("%1d %2h %3min").arg(days).arg(hours).arg(minutes);
}

View File

@ -39,6 +39,7 @@
#include "MMCZip.h"
#include "FileSystem.h"
#include <QCoreApplication>
#include <QDebug>
// ours
@ -228,23 +229,27 @@ bool MMCZip::createModdedJar(QString sourceJarPath, QString targetJarPath, const
}
// ours
QString MMCZip::findFolderOfFileInZip(QuaZip * zip, const QString & what, const QString &root)
QString MMCZip::findFolderOfFileInZip(QuaZip* zip, const QString& what, const QStringList& ignore_paths, const QString& root)
{
QuaZipDir rootDir(zip, root);
for(auto fileName: rootDir.entryList(QDir::Files))
{
if(fileName == what)
for (auto&& fileName : rootDir.entryList(QDir::Files)) {
if (fileName == what)
return root;
QCoreApplication::processEvents();
}
for(auto fileName: rootDir.entryList(QDir::Dirs))
{
QString result = findFolderOfFileInZip(zip, what, root + fileName);
if(!result.isEmpty())
{
// Recurse the search to non-ignored subfolders
for (auto&& fileName : rootDir.entryList(QDir::Dirs)) {
if (ignore_paths.contains(fileName))
continue;
QString result = findFolderOfFileInZip(zip, what, ignore_paths, root + fileName);
if (!result.isEmpty())
return result;
}
}
return QString();
return {};
}
// ours

View File

@ -80,9 +80,11 @@ namespace MMCZip
/**
* Find a single file in archive by file name (not path)
*
* \param ignore_paths paths to skip when recursing the search
*
* \return the path prefix where the file is
*/
QString findFolderOfFileInZip(QuaZip * zip, const QString & what, const QString &root = QString(""));
QString findFolderOfFileInZip(QuaZip * zip, const QString & what, const QStringList& ignore_paths = {}, const QString &root = QString(""));
/**
* Find a multiple files of the same name in archive by file name

View File

@ -1,26 +1,28 @@
#include "MMCStrings.h"
#include "StringUtils.h"
/// If you're wondering where these came from exactly, then know you're not the only one =D
/// TAKEN FROM Qt, because it doesn't expose it intelligently
static inline QChar getNextChar(const QString &s, int location)
static inline QChar getNextChar(const QString& s, int location)
{
return (location < s.length()) ? s.at(location) : QChar();
}
/// TAKEN FROM Qt, because it doesn't expose it intelligently
int Strings::naturalCompare(const QString &s1, const QString &s2, Qt::CaseSensitivity cs)
int StringUtils::naturalCompare(const QString& s1, const QString& s2, Qt::CaseSensitivity cs)
{
for (int l1 = 0, l2 = 0; l1 <= s1.count() && l2 <= s2.count(); ++l1, ++l2)
{
int l1 = 0, l2 = 0;
while (l1 <= s1.count() && l2 <= s2.count()) {
// skip spaces, tabs and 0's
QChar c1 = getNextChar(s1, l1);
while (c1.isSpace())
c1 = getNextChar(s1, ++l1);
QChar c2 = getNextChar(s2, l2);
while (c2.isSpace())
c2 = getNextChar(s2, ++l2);
if (c1.isDigit() && c2.isDigit())
{
if (c1.isDigit() && c2.isDigit()) {
while (c1.digitValue() == 0)
c1 = getNextChar(s1, ++l1);
while (c2.digitValue() == 0)
@ -30,11 +32,8 @@ int Strings::naturalCompare(const QString &s1, const QString &s2, Qt::CaseSensit
int lookAheadLocation2 = l2;
int currentReturnValue = 0;
// find the last digit, setting currentReturnValue as we go if it isn't equal
for (QChar lookAhead1 = c1, lookAhead2 = c2;
(lookAheadLocation1 <= s1.length() && lookAheadLocation2 <= s2.length());
lookAhead1 = getNextChar(s1, ++lookAheadLocation1),
lookAhead2 = getNextChar(s2, ++lookAheadLocation2))
{
for (QChar lookAhead1 = c1, lookAhead2 = c2; (lookAheadLocation1 <= s1.length() && lookAheadLocation2 <= s2.length());
lookAhead1 = getNextChar(s1, ++lookAheadLocation1), lookAhead2 = getNextChar(s2, ++lookAheadLocation2)) {
bool is1ADigit = !lookAhead1.isNull() && lookAhead1.isDigit();
bool is2ADigit = !lookAhead2.isNull() && lookAhead2.isDigit();
if (!is1ADigit && !is2ADigit)
@ -43,14 +42,10 @@ int Strings::naturalCompare(const QString &s1, const QString &s2, Qt::CaseSensit
return -1;
if (!is2ADigit)
return 1;
if (currentReturnValue == 0)
{
if (lookAhead1 < lookAhead2)
{
if (currentReturnValue == 0) {
if (lookAhead1 < lookAhead2) {
currentReturnValue = -1;
}
else if (lookAhead1 > lookAhead2)
{
} else if (lookAhead1 > lookAhead2) {
currentReturnValue = 1;
}
}
@ -58,19 +53,24 @@ int Strings::naturalCompare(const QString &s1, const QString &s2, Qt::CaseSensit
if (currentReturnValue != 0)
return currentReturnValue;
}
if (cs == Qt::CaseInsensitive)
{
if (cs == Qt::CaseInsensitive) {
if (!c1.isLower())
c1 = c1.toLower();
if (!c2.isLower())
c2 = c2.toLower();
}
int r = QString::localeAwareCompare(c1, c2);
if (r < 0)
return -1;
if (r > 0)
return 1;
l1 += 1;
l2 += 1;
}
// The two strings are the same (02 == 2) so fall back to the normal sort
return QString::compare(s1, s2, cs);
}

32
launcher/StringUtils.h Normal file
View File

@ -0,0 +1,32 @@
#pragma once
#include <QString>
namespace StringUtils {
#if defined Q_OS_WIN32
using string = std::wstring;
inline string toStdString(QString s)
{
return s.toStdWString();
}
inline QString fromStdString(string s)
{
return QString::fromStdWString(s);
}
#else
using string = std::string;
inline string toStdString(QString s)
{
return s.toStdString();
}
inline QString fromStdString(string s)
{
return QString::fromStdString(s);
}
#endif
int naturalCompare(const QString& s1, const QString& s2, Qt::CaseSensitivity cs);
} // namespace StringUtils

View File

@ -311,14 +311,14 @@ QModelIndex VersionProxyModel::index(int row, int column, const QModelIndex &par
int VersionProxyModel::columnCount(const QModelIndex &parent) const
{
return m_columns.size();
return parent.isValid() ? 0 : m_columns.size();
}
int VersionProxyModel::rowCount(const QModelIndex &parent) const
{
if(sourceModel())
{
return sourceModel()->rowCount();
return sourceModel()->rowCount(parent);
}
return 0;
}

View File

@ -242,7 +242,7 @@ Qt::DropActions IconList::supportedDropActions() const
return Qt::CopyAction;
}
bool IconList::dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent)
bool IconList::dropMimeData(const QMimeData *data, Qt::DropAction action, [[maybe_unused]] int row, [[maybe_unused]] int column, [[maybe_unused]] const QModelIndex &parent)
{
if (action == Qt::IgnoreAction)
return true;
@ -302,7 +302,7 @@ QVariant IconList::data(const QModelIndex &index, int role) const
int IconList::rowCount(const QModelIndex &parent) const
{
return icons.size();
return parent.isValid() ? 0 : icons.size();
}
void IconList::installIcons(const QStringList &iconFiles)

View File

@ -1,9 +1,10 @@
#include "JavaInstall.h"
#include <MMCStrings.h>
#include "StringUtils.h"
bool JavaInstall::operator<(const JavaInstall &rhs)
{
auto archCompare = Strings::naturalCompare(arch, rhs.arch, Qt::CaseInsensitive);
auto archCompare = StringUtils::naturalCompare(arch, rhs.arch, Qt::CaseInsensitive);
if(archCompare != 0)
return archCompare < 0;
if(id < rhs.id)
@ -14,7 +15,7 @@ bool JavaInstall::operator<(const JavaInstall &rhs)
{
return false;
}
return Strings::naturalCompare(path, rhs.path, Qt::CaseInsensitive) < 0;
return StringUtils::naturalCompare(path, rhs.path, Qt::CaseInsensitive) < 0;
}
bool JavaInstall::operator==(const JavaInstall &rhs)

View File

@ -41,7 +41,6 @@
#include "java/JavaInstallList.h"
#include "java/JavaCheckerJob.h"
#include "java/JavaUtils.h"
#include "MMCStrings.h"
#include "minecraft/VersionFilterData.h"
JavaInstallList::JavaInstallList(QObject *parent) : BaseVersionList(parent)
@ -73,7 +72,7 @@ void JavaInstallList::load()
}
}
const BaseVersionPtr JavaInstallList::at(int i) const
const BaseVersion::Ptr JavaInstallList::at(int i) const
{
return m_vlist.at(i);
}
@ -122,7 +121,7 @@ BaseVersionList::RoleList JavaInstallList::providesRoles() const
}
void JavaInstallList::updateListData(QList<BaseVersionPtr> versions)
void JavaInstallList::updateListData(QList<BaseVersion::Ptr> versions)
{
beginResetModel();
m_vlist = versions;
@ -137,7 +136,7 @@ void JavaInstallList::updateListData(QList<BaseVersionPtr> versions)
m_loadTask.reset();
}
bool sortJavas(BaseVersionPtr left, BaseVersionPtr right)
bool sortJavas(BaseVersion::Ptr left, BaseVersion::Ptr right)
{
auto rleft = std::dynamic_pointer_cast<JavaInstall>(right);
auto rright = std::dynamic_pointer_cast<JavaInstall>(left);
@ -210,11 +209,11 @@ void JavaListLoadTask::javaCheckerFinished()
}
}
QList<BaseVersionPtr> javas_bvp;
QList<BaseVersion::Ptr> javas_bvp;
for (auto java : candidates)
{
//qDebug() << java->id << java->arch << " at " << java->path;
BaseVersionPtr bp_java = std::dynamic_pointer_cast<BaseVersion>(java);
BaseVersion::Ptr bp_java = std::dynamic_pointer_cast<BaseVersion>(java);
if (bp_java)
{

View File

@ -42,7 +42,7 @@ public:
Task::Ptr getLoadTask() override;
bool isLoaded() override;
const BaseVersionPtr at(int i) const override;
const BaseVersion::Ptr at(int i) const override;
int count() const override;
void sortVersions() override;
@ -50,7 +50,7 @@ public:
RoleList providesRoles() const override;
public slots:
void updateListData(QList<BaseVersionPtr> versions) override;
void updateListData(QList<BaseVersion::Ptr> versions) override;
protected:
void load();
@ -59,7 +59,7 @@ protected:
protected:
Status m_status = Status::NotDone;
shared_qobject_ptr<JavaListLoadTask> m_loadTask;
QList<BaseVersionPtr> m_vlist;
QList<BaseVersion::Ptr> m_vlist;
};
class JavaListLoadTask : public Task

View File

@ -439,19 +439,28 @@ QList<QString> JavaUtils::FindJavaPaths()
javas.append(FS::PathCombine(prefix, "bin/java"));
}
};
// java installed in a snap is installed in the standard directory, but underneath $SNAP
auto snap = qEnvironmentVariable("SNAP");
auto scanJavaDirs = [&](const QString & dirPath)
{
scanJavaDir(dirPath);
if (!snap.isNull()) {
scanJavaDir(snap + dirPath);
}
};
// oracle RPMs
scanJavaDir("/usr/java");
scanJavaDirs("/usr/java");
// general locations used by distro packaging
scanJavaDir("/usr/lib/jvm");
scanJavaDir("/usr/lib64/jvm");
scanJavaDir("/usr/lib32/jvm");
scanJavaDirs("/usr/lib/jvm");
scanJavaDirs("/usr/lib64/jvm");
scanJavaDirs("/usr/lib32/jvm");
// javas stored in Prism Launcher's folder
scanJavaDir("java");
scanJavaDirs("java");
// manually installed JDKs in /opt
scanJavaDir("/opt/jdk");
scanJavaDir("/opt/jdks");
scanJavaDirs("/opt/jdk");
scanJavaDirs("/opt/jdks");
// flatpak
scanJavaDir("/app/jdk");
scanJavaDirs("/app/jdk");
javas = addJavasFromEnv(javas);
javas.removeDuplicates();
return javas;

View File

@ -1,5 +1,6 @@
#include "JavaVersion.h"
#include <MMCStrings.h>
#include "StringUtils.h"
#include <QRegularExpression>
#include <QString>
@ -98,12 +99,12 @@ bool JavaVersion::operator<(const JavaVersion &rhs)
else if(thisPre && rhsPre)
{
// both are prereleases - use natural compare...
return Strings::naturalCompare(m_prerelease, rhs.m_prerelease, Qt::CaseSensitive) < 0;
return StringUtils::naturalCompare(m_prerelease, rhs.m_prerelease, Qt::CaseSensitive) < 0;
}
// neither is prerelease, so they are the same -> this cannot be less than rhs
return false;
}
else return Strings::naturalCompare(m_string, rhs.m_string, Qt::CaseSensitive) < 0;
else return StringUtils::naturalCompare(m_string, rhs.m_string, Qt::CaseSensitive) < 0;
}
bool JavaVersion::operator==(const JavaVersion &rhs)

View File

@ -37,7 +37,6 @@
#include "launch/LaunchTask.h"
#include "MessageLevel.h"
#include "MMCStrings.h"
#include "java/JavaChecker.h"
#include "tasks/Task.h"
#include <QDebug>

View File

@ -81,14 +81,19 @@ int main(int argc, char *argv[])
Q_INIT_RESOURCE(pe_light);
Q_INIT_RESOURCE(pe_blue);
Q_INIT_RESOURCE(pe_colored);
Q_INIT_RESOURCE(breeze_dark);
Q_INIT_RESOURCE(breeze_light);
Q_INIT_RESOURCE(OSX);
Q_INIT_RESOURCE(iOS);
Q_INIT_RESOURCE(flat);
Q_INIT_RESOURCE(flat_white);
return app.exec();
}
case Application::Failed:
return 1;
case Application::Succeeded:
return 0;
default:
return -1;
}
}

View File

@ -24,7 +24,7 @@ Index::Index(QObject *parent)
: QAbstractListModel(parent)
{
}
Index::Index(const QVector<VersionListPtr> &lists, QObject *parent)
Index::Index(const QVector<VersionList::Ptr> &lists, QObject *parent)
: QAbstractListModel(parent), m_lists(lists)
{
for (int i = 0; i < m_lists.size(); ++i)
@ -41,7 +41,7 @@ QVariant Index::data(const QModelIndex &index, int role) const
return QVariant();
}
VersionListPtr list = m_lists.at(index.row());
VersionList::Ptr list = m_lists.at(index.row());
switch (role)
{
case Qt::DisplayRole:
@ -58,11 +58,11 @@ QVariant Index::data(const QModelIndex &index, int role) const
}
int Index::rowCount(const QModelIndex &parent) const
{
return m_lists.size();
return parent.isValid() ? 0 : m_lists.size();
}
int Index::columnCount(const QModelIndex &parent) const
{
return 1;
return parent.isValid() ? 0 : 1;
}
QVariant Index::headerData(int section, Qt::Orientation orientation, int role) const
{
@ -81,9 +81,9 @@ bool Index::hasUid(const QString &uid) const
return m_uids.contains(uid);
}
VersionListPtr Index::get(const QString &uid)
VersionList::Ptr Index::get(const QString &uid)
{
VersionListPtr out = m_uids.value(uid, nullptr);
VersionList::Ptr out = m_uids.value(uid, nullptr);
if(!out)
{
out = std::make_shared<VersionList>(uid);
@ -92,7 +92,7 @@ VersionListPtr Index::get(const QString &uid)
return out;
}
VersionPtr Index::get(const QString &uid, const QString &version)
Version::Ptr Index::get(const QString &uid, const QString &version)
{
auto list = get(uid);
return list->getVersion(version);
@ -105,7 +105,7 @@ void Index::parse(const QJsonObject& obj)
void Index::merge(const std::shared_ptr<Index> &other)
{
const QVector<VersionListPtr> lists = std::dynamic_pointer_cast<Index>(other)->m_lists;
const QVector<VersionList::Ptr> lists = std::dynamic_pointer_cast<Index>(other)->m_lists;
// initial load, no need to merge
if (m_lists.isEmpty())
{
@ -120,7 +120,7 @@ void Index::merge(const std::shared_ptr<Index> &other)
}
else
{
for (const VersionListPtr &list : lists)
for (const VersionList::Ptr &list : lists)
{
if (m_uids.contains(list->uid()))
{
@ -138,7 +138,7 @@ void Index::merge(const std::shared_ptr<Index> &other)
}
}
void Index::connectVersionList(const int row, const VersionListPtr &list)
void Index::connectVersionList(const int row, const VersionList::Ptr &list)
{
connect(list.get(), &VersionList::nameChanged, this, [this, row]()
{

View File

@ -19,20 +19,19 @@
#include <memory>
#include "BaseEntity.h"
#include "meta/VersionList.h"
class Task;
namespace Meta
{
using VersionListPtr = std::shared_ptr<class VersionList>;
using VersionPtr = std::shared_ptr<class Version>;
class Index : public QAbstractListModel, public BaseEntity
{
Q_OBJECT
public:
explicit Index(QObject *parent = nullptr);
explicit Index(const QVector<VersionListPtr> &lists, QObject *parent = nullptr);
explicit Index(const QVector<VersionList::Ptr> &lists, QObject *parent = nullptr);
enum
{
@ -49,21 +48,21 @@ public:
QString localFilename() const override { return "index.json"; }
// queries
VersionListPtr get(const QString &uid);
VersionPtr get(const QString &uid, const QString &version);
VersionList::Ptr get(const QString &uid);
Version::Ptr get(const QString &uid, const QString &version);
bool hasUid(const QString &uid) const;
QVector<VersionListPtr> lists() const { return m_lists; }
QVector<VersionList::Ptr> lists() const { return m_lists; }
public: // for usage by parsers only
void merge(const std::shared_ptr<Index> &other);
void parse(const QJsonObject &obj) override;
private:
QVector<VersionListPtr> m_lists;
QHash<QString, VersionListPtr> m_uids;
QVector<VersionList::Ptr> m_lists;
QHash<QString, VersionList::Ptr> m_uids;
void connectVersionList(const int row, const VersionListPtr &list);
void connectVersionList(const int row, const VersionList::Ptr &list);
};
}

View File

@ -37,11 +37,11 @@ MetadataVersion currentFormatVersion()
static std::shared_ptr<Index> parseIndexInternal(const QJsonObject &obj)
{
const QVector<QJsonObject> objects = requireIsArrayOf<QJsonObject>(obj, "packages");
QVector<VersionListPtr> lists;
QVector<VersionList::Ptr> lists;
lists.reserve(objects.size());
std::transform(objects.begin(), objects.end(), std::back_inserter(lists), [](const QJsonObject &obj)
{
VersionListPtr list = std::make_shared<VersionList>(requireString(obj, "uid"));
VersionList::Ptr list = std::make_shared<VersionList>(requireString(obj, "uid"));
list->setName(ensureString(obj, "name", QString()));
return list;
});
@ -49,9 +49,9 @@ static std::shared_ptr<Index> parseIndexInternal(const QJsonObject &obj)
}
// Version
static VersionPtr parseCommonVersion(const QString &uid, const QJsonObject &obj)
static Version::Ptr parseCommonVersion(const QString &uid, const QJsonObject &obj)
{
VersionPtr version = std::make_shared<Version>(uid, requireString(obj, "version"));
Version::Ptr version = std::make_shared<Version>(uid, requireString(obj, "version"));
version->setTime(QDateTime::fromString(requireString(obj, "releaseTime"), Qt::ISODate).toMSecsSinceEpoch() / 1000);
version->setType(ensureString(obj, "type", QString()));
version->setRecommended(ensureBoolean(obj, QString("recommended"), false));
@ -63,9 +63,9 @@ static VersionPtr parseCommonVersion(const QString &uid, const QJsonObject &obj)
return version;
}
static std::shared_ptr<Version> parseVersionInternal(const QJsonObject &obj)
static Version::Ptr parseVersionInternal(const QJsonObject &obj)
{
VersionPtr version = parseCommonVersion(requireString(obj, "uid"), obj);
Version::Ptr version = parseCommonVersion(requireString(obj, "uid"), obj);
version->setData(OneSixVersionFormat::versionFileFromJson(QJsonDocument(obj),
QString("%1/%2.json").arg(version->uid(), version->version()),
@ -74,12 +74,12 @@ static std::shared_ptr<Version> parseVersionInternal(const QJsonObject &obj)
}
// Version list / package
static std::shared_ptr<VersionList> parseVersionListInternal(const QJsonObject &obj)
static VersionList::Ptr parseVersionListInternal(const QJsonObject &obj)
{
const QString uid = requireString(obj, "uid");
const QVector<QJsonObject> versionsRaw = requireIsArrayOf<QJsonObject>(obj, "versions");
QVector<VersionPtr> versions;
QVector<Version::Ptr> versions;
versions.reserve(versionsRaw.size());
std::transform(versionsRaw.begin(), versionsRaw.end(), std::back_inserter(versions), [uid](const QJsonObject &vObj)
{
@ -88,7 +88,7 @@ static std::shared_ptr<VersionList> parseVersionListInternal(const QJsonObject &
return version;
});
VersionListPtr list = std::make_shared<VersionList>(uid);
VersionList::Ptr list = std::make_shared<VersionList>(uid);
list->setName(ensureString(obj, "name", QString()));
list->setVersions(versions);
return list;

View File

@ -60,11 +60,6 @@ struct Require
QString suggests;
};
inline Q_DECL_PURE_FUNCTION uint qHash(const Require &key, uint seed = 0) Q_DECL_NOTHROW
{
return qHash(key.uid, seed);
}
using RequireSet = std::set<Require>;
void parseIndex(const QJsonObject &obj, Index *ptr);

View File

@ -54,7 +54,7 @@ void Meta::Version::parse(const QJsonObject& obj)
parseVersion(obj, this);
}
void Meta::Version::mergeFromList(const Meta::VersionPtr& other)
void Meta::Version::mergeFromList(const Meta::Version::Ptr& other)
{
if(other->m_providesRecommendations)
{
@ -85,7 +85,7 @@ void Meta::Version::mergeFromList(const Meta::VersionPtr& other)
}
}
void Meta::Version::merge(const VersionPtr &other)
void Meta::Version::merge(const Version::Ptr &other)
{
mergeFromList(other);
if(other->m_data)

View File

@ -30,13 +30,14 @@
namespace Meta
{
using VersionPtr = std::shared_ptr<class Version>;
class Version : public QObject, public BaseVersion, public BaseEntity
{
Q_OBJECT
public: /* con/des */
public:
using Ptr = std::shared_ptr<Version>;
explicit Version(const QString &uid, const QString &version);
virtual ~Version();
@ -78,8 +79,8 @@ public: /* con/des */
return m_data != nullptr;
}
void merge(const VersionPtr &other);
void mergeFromList(const VersionPtr &other);
void merge(const Version::Ptr &other);
void mergeFromList(const Version::Ptr &other);
void parse(const QJsonObject &obj) override;
QString localFilename() const override;
@ -113,4 +114,4 @@ private:
};
}
Q_DECLARE_METATYPE(Meta::VersionPtr)
Q_DECLARE_METATYPE(Meta::Version::Ptr)

View File

@ -40,7 +40,7 @@ bool VersionList::isLoaded()
return BaseEntity::isLoaded();
}
const BaseVersionPtr VersionList::at(int i) const
const BaseVersion::Ptr VersionList::at(int i) const
{
return m_versions.at(i);
}
@ -52,7 +52,7 @@ int VersionList::count() const
void VersionList::sortVersions()
{
beginResetModel();
std::sort(m_versions.begin(), m_versions.end(), [](const VersionPtr &a, const VersionPtr &b)
std::sort(m_versions.begin(), m_versions.end(), [](const Version::Ptr &a, const Version::Ptr &b)
{
return *a.get() < *b.get();
});
@ -66,7 +66,7 @@ QVariant VersionList::data(const QModelIndex &index, int role) const
return QVariant();
}
VersionPtr version = m_versions.at(index.row());
Version::Ptr version = m_versions.at(index.row());
switch (role)
{
@ -129,9 +129,9 @@ QString VersionList::humanReadable() const
return m_name.isEmpty() ? m_uid : m_name;
}
VersionPtr VersionList::getVersion(const QString &version)
Version::Ptr VersionList::getVersion(const QString &version)
{
VersionPtr out = m_lookup.value(version, nullptr);
Version::Ptr out = m_lookup.value(version, nullptr);
if(!out)
{
out = std::make_shared<Version>(m_uid, version);
@ -143,7 +143,7 @@ VersionPtr VersionList::getVersion(const QString &version)
bool VersionList::hasVersion(QString version) const
{
auto ver = std::find_if(m_versions.constBegin(), m_versions.constEnd(),
[&](Meta::VersionPtr const& a){ return a->version() == version; });
[&](Meta::Version::Ptr const& a){ return a->version() == version; });
return (ver != m_versions.constEnd());
}
@ -153,11 +153,11 @@ void VersionList::setName(const QString &name)
emit nameChanged(name);
}
void VersionList::setVersions(const QVector<VersionPtr> &versions)
void VersionList::setVersions(const QVector<Version::Ptr> &versions)
{
beginResetModel();
m_versions = versions;
std::sort(m_versions.begin(), m_versions.end(), [](const VersionPtr &a, const VersionPtr &b)
std::sort(m_versions.begin(), m_versions.end(), [](const Version::Ptr &a, const Version::Ptr &b)
{
return a->rawTime() > b->rawTime();
});
@ -168,7 +168,7 @@ void VersionList::setVersions(const QVector<VersionPtr> &versions)
}
// FIXME: this is dumb, we have 'recommended' as part of the metadata already...
auto recommendedIt = std::find_if(m_versions.constBegin(), m_versions.constEnd(), [](const VersionPtr &ptr) { return ptr->type() == "release"; });
auto recommendedIt = std::find_if(m_versions.constBegin(), m_versions.constEnd(), [](const Version::Ptr &ptr) { return ptr->type() == "release"; });
m_recommended = recommendedIt == m_versions.constEnd() ? nullptr : *recommendedIt;
endResetModel();
}
@ -179,7 +179,7 @@ void VersionList::parse(const QJsonObject& obj)
}
// FIXME: this is dumb, we have 'recommended' as part of the metadata already...
static const Meta::VersionPtr &getBetterVersion(const Meta::VersionPtr &a, const Meta::VersionPtr &b)
static const Meta::Version::Ptr &getBetterVersion(const Meta::Version::Ptr &a, const Meta::Version::Ptr &b)
{
if(!a)
return b;
@ -194,7 +194,7 @@ static const Meta::VersionPtr &getBetterVersion(const Meta::VersionPtr &a, const
return (a->type() == "release" ? a : b);
}
void VersionList::mergeFromIndex(const VersionListPtr &other)
void VersionList::mergeFromIndex(const VersionList::Ptr &other)
{
if (m_name != other->m_name)
{
@ -202,7 +202,7 @@ void VersionList::mergeFromIndex(const VersionListPtr &other)
}
}
void VersionList::merge(const VersionListPtr &other)
void VersionList::merge(const VersionList::Ptr &other)
{
if (m_name != other->m_name)
{
@ -216,7 +216,7 @@ void VersionList::merge(const VersionListPtr &other)
{
qWarning() << "Empty list loaded ...";
}
for (const VersionPtr &version : other->m_versions)
for (const Version::Ptr &version : other->m_versions)
{
// we already have the version. merge the contents
if (m_lookup.contains(version->version()))
@ -235,7 +235,7 @@ void VersionList::merge(const VersionListPtr &other)
endResetModel();
}
void VersionList::setupAddedVersion(const int row, const VersionPtr &version)
void VersionList::setupAddedVersion(const int row, const Version::Ptr &version)
{
// FIXME: do not disconnect from everythin, disconnect only the lambdas here
version->disconnect();
@ -244,7 +244,7 @@ void VersionList::setupAddedVersion(const int row, const VersionPtr &version)
connect(version.get(), &Version::typeChanged, this, [this, row]() { emit dataChanged(index(row), index(row), QVector<int>() << TypeRole); });
}
BaseVersionPtr VersionList::getRecommended() const
BaseVersion::Ptr VersionList::getRecommended() const
{
return m_recommended;
}

View File

@ -20,10 +20,10 @@
#include <QJsonObject>
#include <memory>
#include "meta/Version.h"
namespace Meta
{
using VersionPtr = std::shared_ptr<class Version>;
using VersionListPtr = std::shared_ptr<class VersionList>;
class VersionList : public BaseVersionList, public BaseEntity
{
@ -33,6 +33,8 @@ class VersionList : public BaseVersionList, public BaseEntity
public:
explicit VersionList(const QString &uid, QObject *parent = nullptr);
using Ptr = std::shared_ptr<VersionList>;
enum Roles
{
UidRole = Qt::UserRole + 100,
@ -43,11 +45,11 @@ public:
Task::Ptr getLoadTask() override;
bool isLoaded() override;
const BaseVersionPtr at(int i) const override;
const BaseVersion::Ptr at(int i) const override;
int count() const override;
void sortVersions() override;
BaseVersionPtr getRecommended() const override;
BaseVersion::Ptr getRecommended() const override;
QVariant data(const QModelIndex &index, int role) const override;
RoleList providesRoles() const override;
@ -65,38 +67,38 @@ public:
}
QString humanReadable() const;
VersionPtr getVersion(const QString &version);
Version::Ptr getVersion(const QString &version);
bool hasVersion(QString version) const;
QVector<VersionPtr> versions() const
QVector<Version::Ptr> versions() const
{
return m_versions;
}
public: // for usage only by parsers
void setName(const QString &name);
void setVersions(const QVector<VersionPtr> &versions);
void merge(const VersionListPtr &other);
void mergeFromIndex(const VersionListPtr &other);
void setVersions(const QVector<Version::Ptr> &versions);
void merge(const VersionList::Ptr &other);
void mergeFromIndex(const VersionList::Ptr &other);
void parse(const QJsonObject &obj) override;
signals:
void nameChanged(const QString &name);
protected slots:
void updateListData(QList<BaseVersionPtr>) override
void updateListData(QList<BaseVersion::Ptr>) override
{
}
private:
QVector<VersionPtr> m_versions;
QHash<QString, VersionPtr> m_lookup;
QVector<Version::Ptr> m_versions;
QHash<QString, Version::Ptr> m_lookup;
QString m_uid;
QString m_name;
VersionPtr m_recommended;
Version::Ptr m_recommended;
void setupAddedVersion(const int row, const VersionPtr &version);
void setupAddedVersion(const int row, const Version::Ptr &version);
};
}
Q_DECLARE_METATYPE(Meta::VersionListPtr)
Q_DECLARE_METATYPE(Meta::VersionList::Ptr)

View File

@ -10,7 +10,7 @@ typedef std::shared_ptr<Agent> AgentPtr;
class Agent {
public:
Agent(LibraryPtr library, QString &argument)
Agent(LibraryPtr library, const QString &argument)
{
m_library = library;
m_argument = argument;

View File

@ -1,8 +1,9 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* PolyMC - Minecraft Launcher
* Prism Launcher - Minecraft Launcher
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
* Copyright (C) 2022 Jamie Mansfield <jmansfield@cadixdev.org>
* Copyright (C) 2022 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
@ -43,7 +44,6 @@
#include "settings/SettingsObject.h"
#include "Application.h"
#include "MMCStrings.h"
#include "pathmatcher/RegexpMatcher.h"
#include "pathmatcher/MultiMatcher.h"
#include "FileSystem.h"
@ -439,6 +439,17 @@ QStringList MinecraftInstance::javaArguments()
return args;
}
QString MinecraftInstance::getLauncher()
{
auto profile = m_components->getProfile();
// use legacy launcher if the traits are set
if (profile->getTraits().contains("legacyLaunch") || profile->getTraits().contains("alphaLaunch"))
return "legacy";
return "standard";
}
QMap<QString, QString> MinecraftInstance::getVariables()
{
QMap<QString, QString> out;
@ -630,26 +641,13 @@ QString MinecraftInstance::createLaunchScript(AuthSessionPtr session, MinecraftS
launchScript += "sessionId " + session->session + "\n";
}
// libraries and class path.
{
QStringList jars, nativeJars;
profile->getLibraryFiles(runtimeContext(), jars, nativeJars, getLocalLibraryPath(), binRoot());
for(auto file: jars)
{
launchScript += "cp " + file + "\n";
}
for(auto file: nativeJars)
{
launchScript += "ext " + file + "\n";
}
launchScript += "natives " + getNativePath() + "\n";
}
for (auto trait : profile->getTraits())
{
launchScript += "traits " + trait + "\n";
}
launchScript += "launcher onesix\n";
launchScript += "launcher " + getLauncher() + "\n";
// qDebug() << "Generated launch script:" << launchScript;
return launchScript;
}
@ -785,6 +783,8 @@ QStringList MinecraftInstance::verboseDescription(AuthSessionPtr session, Minecr
out << "Window size: " + QString::number(width) + " x " + QString::number(height);
}
out << "";
out << "Launcher: " + getLauncher();
out << "";
return out;
}
@ -1096,8 +1096,6 @@ std::shared_ptr<ResourcePackFolderModel> MinecraftInstance::resourcePackList() c
if (!m_resource_pack_list)
{
m_resource_pack_list.reset(new ResourcePackFolderModel(resourcePacksDir()));
m_resource_pack_list->enableInteraction(!isRunning());
connect(this, &BaseInstance::runningStatusChanged, m_resource_pack_list.get(), &ResourcePackFolderModel::disableInteraction);
}
return m_resource_pack_list;
}
@ -1107,8 +1105,6 @@ std::shared_ptr<TexturePackFolderModel> MinecraftInstance::texturePackList() con
if (!m_texture_pack_list)
{
m_texture_pack_list.reset(new TexturePackFolderModel(texturePacksDir()));
m_texture_pack_list->disableInteraction(isRunning());
connect(this, &BaseInstance::runningStatusChanged, m_texture_pack_list.get(), &ModFolderModel::disableInteraction);
}
return m_texture_pack_list;
}
@ -1118,8 +1114,6 @@ std::shared_ptr<ShaderPackFolderModel> MinecraftInstance::shaderPackList() const
if (!m_shader_pack_list)
{
m_shader_pack_list.reset(new ShaderPackFolderModel(shaderPacksDir()));
m_shader_pack_list->disableInteraction(isRunning());
connect(this, &BaseInstance::runningStatusChanged, m_shader_pack_list.get(), &ModFolderModel::disableInteraction);
}
return m_shader_pack_list;
}

View File

@ -1,7 +1,8 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* PolyMC - Minecraft Launcher
* Prism Launcher - Minecraft Launcher
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
* Copyright (C) 2022 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
@ -130,6 +131,7 @@ public:
QString createLaunchScript(AuthSessionPtr session, MinecraftServerTargetPtr serverToJoin);
/// get arguments passed to java
QStringList javaArguments();
QString getLauncher();
/// get variables for launch command variable substitution/environment
QMap<QString, QString> getVariables() override;

View File

@ -135,7 +135,7 @@ QJsonObject libDownloadInfoToJson(MojangLibraryDownloadInfo::Ptr libinfo)
{
out.insert("artifact", downloadInfoToJson(libinfo->artifact));
}
if(libinfo->classifiers.size())
if(!libinfo->classifiers.isEmpty())
{
QJsonObject classifiersOut;
for(auto iter = libinfo->classifiers.begin(); iter != libinfo->classifiers.end(); iter++)
@ -297,7 +297,7 @@ void MojangVersionFormat::writeVersionProperties(const VersionFile* in, QJsonObj
{
out.insert("assetIndex", assetIndexToJson(in->mojangAssetIndex));
}
if(in->mojangDownloads.size())
if(!in->mojangDownloads.isEmpty())
{
QJsonObject downloadsOut;
for(auto iter = in->mojangDownloads.begin(); iter != in->mojangDownloads.end(); iter++)
@ -306,6 +306,15 @@ void MojangVersionFormat::writeVersionProperties(const VersionFile* in, QJsonObj
}
out.insert("downloads", downloadsOut);
}
if(!in->compatibleJavaMajors.isEmpty())
{
QJsonArray compatibleJavaMajorsOut;
for(auto compatibleJavaMajor : in->compatibleJavaMajors)
{
compatibleJavaMajorsOut.append(compatibleJavaMajor);
}
out.insert("compatibleJavaMajors", compatibleJavaMajorsOut);
}
}
QJsonDocument MojangVersionFormat::versionFileToJson(const VersionFilePtr &patch)
@ -396,7 +405,7 @@ QJsonObject MojangVersionFormat::libraryToJson(Library *library)
iter++;
}
libRoot.insert("natives", nativeList);
if (library->m_extractExcludes.size())
if (!library->m_extractExcludes.isEmpty())
{
QJsonArray excludes;
QJsonObject extract;
@ -408,7 +417,7 @@ QJsonObject MojangVersionFormat::libraryToJson(Library *library)
libRoot.insert("extract", extract);
}
}
if (library->m_rules.size())
if (!library->m_rules.isEmpty())
{
QJsonArray allRules;
for (auto &rule : library->m_rules)

View File

@ -63,13 +63,13 @@ LibraryPtr OneSixVersionFormat::libraryFromJson(ProblemContainer & problems, con
QJsonObject OneSixVersionFormat::libraryToJson(Library *library)
{
QJsonObject libRoot = MojangVersionFormat::libraryToJson(library);
if (library->m_absoluteURL.size())
if (!library->m_absoluteURL.isEmpty())
libRoot.insert("MMC-absoluteUrl", library->m_absoluteURL);
if (library->m_hint.size())
if (!library->m_hint.isEmpty())
libRoot.insert("MMC-hint", library->m_hint);
if (library->m_filename.size())
if (!library->m_filename.isEmpty())
libRoot.insert("MMC-filename", library->m_filename);
if (library->m_displayname.size())
if (!library->m_displayname.isEmpty())
libRoot.insert("MMC-displayname", library->m_displayname);
return libRoot;
}
@ -225,11 +225,10 @@ VersionFilePtr OneSixVersionFormat::versionFileFromJson(const QJsonDocument &doc
{
QJsonObject agentObj = requireObject(agentVal);
auto lib = libraryFromJson(*out, agentObj, filename);
QString arg = "";
if (agentObj.contains("argument"))
{
readString(agentObj, "argument", arg);
}
readString(agentObj, "argument", arg);
AgentPtr agent(new Agent(lib, arg));
out->agents.append(agent);
}
@ -332,6 +331,20 @@ QJsonDocument OneSixVersionFormat::versionFileToJson(const VersionFilePtr &patch
writeString(root, "appletClass", patch->appletClass);
writeStringList(root, "+tweakers", patch->addTweakers);
writeStringList(root, "+traits", patch->traits.values());
writeStringList(root, "+jvmArgs", patch->addnJvmArguments);
if (!patch->agents.isEmpty())
{
QJsonArray array;
for (auto value: patch->agents)
{
QJsonObject agentOut = OneSixVersionFormat::libraryToJson(value->library().get());
if (!value->argument().isEmpty())
agentOut.insert("argument", value->argument());
array.append(agentOut);
}
root.insert("+agents", array);
}
if (!patch->libraries.isEmpty())
{
QJsonArray array;

View File

@ -1,7 +1,8 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* PolyMC - Minecraft Launcher
* Prism Launcher - Minecraft Launcher
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
* Copyright (C) 2022 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
@ -47,7 +48,6 @@
#include "Exception.h"
#include "minecraft/OneSixVersionFormat.h"
#include "FileSystem.h"
#include "meta/Index.h"
#include "minecraft/MinecraftInstance.h"
#include "Json.h"
@ -55,7 +55,6 @@
#include "PackProfile_p.h"
#include "ComponentUpdateTask.h"
#include "Application.h"
#include "modplatform/ModAPI.h"
static const QMap<QString, ModAPI::ModLoaderType> modloaderMapping{
@ -613,7 +612,7 @@ QVariant PackProfile::data(const QModelIndex &index, int role) const
bool PackProfile::setData(const QModelIndex& index, const QVariant& value, int role)
{
if (!index.isValid() || index.row() < 0 || index.row() >= rowCount(index))
if (!index.isValid() || index.row() < 0 || index.row() >= rowCount(index.parent()))
{
return false;
}
@ -675,12 +674,12 @@ Qt::ItemFlags PackProfile::flags(const QModelIndex &index) const
int PackProfile::rowCount(const QModelIndex &parent) const
{
return d->components.size();
return parent.isValid() ? 0 : d->components.size();
}
int PackProfile::columnCount(const QModelIndex &parent) const
{
return NUM_COLUMNS;
return parent.isValid() ? 0 : NUM_COLUMNS;
}
void PackProfile::move(const int index, const MoveDirection direction)
@ -738,6 +737,11 @@ void PackProfile::installCustomJar(QString selectedFile)
installCustomJar_internal(selectedFile);
}
void PackProfile::installAgents(QStringList selectedFiles)
{
installAgents_internal(selectedFiles);
}
bool PackProfile::installEmpty(const QString& uid, const QString& name)
{
QString patchDir = FS::PathCombine(d->m_instance->instanceRoot(), "patches");
@ -832,18 +836,14 @@ bool PackProfile::installJarMods_internal(QStringList filepaths)
for(auto filepath:filepaths)
{
QFileInfo sourceInfo(filepath);
auto uuid = QUuid::createUuid();
QString id = uuid.toString().remove('{').remove('}');
QString id = QUuid::createUuid().toString(QUuid::WithoutBraces);
QString target_filename = id + ".jar";
QString target_id = "org.multimc.jarmod." + id;
QString target_id = "custom.jarmod." + id;
QString target_name = sourceInfo.completeBaseName() + " (jar mod)";
QString finalPath = FS::PathCombine(d->m_instance->jarModsDir(), target_filename);
QFileInfo targetInfo(finalPath);
if(targetInfo.exists())
{
return false;
}
Q_ASSERT(!targetInfo.exists());
if (!QFile::copy(sourceInfo.absoluteFilePath(),QFileInfo(finalPath).absoluteFilePath()))
{
@ -852,7 +852,7 @@ bool PackProfile::installJarMods_internal(QStringList filepaths)
auto f = std::make_shared<VersionFile>();
auto jarMod = std::make_shared<Library>();
jarMod->setRawName(GradleSpecifier("org.multimc.jarmods:" + id + ":1"));
jarMod->setRawName(GradleSpecifier("custom.jarmods:" + id + ":1"));
jarMod->setFilename(target_filename);
jarMod->setDisplayName(sourceInfo.completeBaseName());
jarMod->setHint("local");
@ -892,7 +892,7 @@ bool PackProfile::installCustomJar_internal(QString filepath)
return false;
}
auto specifier = GradleSpecifier("org.multimc:customjar:1");
auto specifier = GradleSpecifier("custom:customjar:1");
QFileInfo sourceInfo(filepath);
QString target_filename = specifier.getFileName();
QString target_id = specifier.artifactId();
@ -939,6 +939,64 @@ bool PackProfile::installCustomJar_internal(QString filepath)
return true;
}
bool PackProfile::installAgents_internal(QStringList filepaths)
{
// FIXME code duplication
const QString patchDir = FS::PathCombine(d->m_instance->instanceRoot(), "patches");
if (!FS::ensureFolderPathExists(patchDir))
return false;
const QString libDir = d->m_instance->getLocalLibraryPath();
if (!FS::ensureFolderPathExists(libDir))
return false;
for (const QString& source : filepaths) {
const QFileInfo sourceInfo(source);
const QString id = QUuid::createUuid().toString(QUuid::WithoutBraces);
const QString targetBaseName = id + ".jar";
const QString targetId = "custom.agent." + id;
const QString targetName = sourceInfo.completeBaseName() + " (agent)";
const QString target = FS::PathCombine(d->m_instance->getLocalLibraryPath(), targetBaseName);
const QFileInfo targetInfo(target);
Q_ASSERT(!targetInfo.exists());
if (!QFile::copy(source, target))
return false;
auto versionFile = std::make_shared<VersionFile>();
auto agent = std::make_shared<Library>();
agent->setRawName("custom.agents:" + id + ":1");
agent->setFilename(targetBaseName);
agent->setDisplayName(sourceInfo.completeBaseName());
agent->setHint("local");
versionFile->agents.append(std::make_shared<Agent>(agent, QString()));
versionFile->name = targetName;
versionFile->uid = targetId;
QFile patchFile(FS::PathCombine(patchDir, targetId + ".json"));
if (!patchFile.open(QFile::WriteOnly)) {
qCritical() << "Error opening" << patchFile.fileName() << "for reading:" << patchFile.errorString();
return false;
}
patchFile.write(OneSixVersionFormat::versionFileToJson(versionFile).toJson());
patchFile.close();
appendComponent(new Component(this, versionFile->uid, versionFile));
}
scheduleSave();
invalidateLaunchProfile();
return true;
}
std::shared_ptr<LaunchProfile> PackProfile::getProfile() const
{
if(!d->m_profile)

View File

@ -1,7 +1,8 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* PolyMC - Minecraft Launcher
* Prism Launcher - Minecraft Launcher
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
* Copyright (C) 2022 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
@ -85,6 +86,9 @@ public:
/// install a jar/zip as a replacement for the main jar
void installCustomJar(QString selectedFile);
/// install Java agent files
void installAgents(QStringList selectedFiles);
enum MoveDirection { MoveUp, MoveDown };
/// move component file # up or down the list
void move(const int index, const MoveDirection direction);
@ -167,6 +171,7 @@ private:
bool load();
bool installJarMods_internal(QStringList filepaths);
bool installCustomJar_internal(QString filepath);
bool installAgents_internal(QStringList filepaths);
bool removeComponent_internal(ComponentPtr patch);
private: /* data */

View File

@ -104,7 +104,7 @@ public:
class ImplicitRule : public Rule
{
protected:
virtual bool applies(const Library *, const RuntimeContext & runtimeContext)
virtual bool applies(const Library *, [[maybe_unused]] const RuntimeContext & runtimeContext)
{
return true;
}

View File

@ -7,7 +7,7 @@
#include "minecraft/PackProfile.h"
#include "settings/INISettingsObject.h"
VanillaCreationTask::VanillaCreationTask(BaseVersionPtr version, QString loader, BaseVersionPtr loader_version)
VanillaCreationTask::VanillaCreationTask(BaseVersion::Ptr version, QString loader, BaseVersion::Ptr loader_version)
: InstanceCreationTask(), m_version(std::move(version)), m_using_loader(true), m_loader(std::move(loader)), m_loader_version(std::move(loader_version))
{}

View File

@ -7,16 +7,16 @@
class VanillaCreationTask final : public InstanceCreationTask {
Q_OBJECT
public:
VanillaCreationTask(BaseVersionPtr version) : InstanceCreationTask(), m_version(std::move(version)) {}
VanillaCreationTask(BaseVersionPtr version, QString loader, BaseVersionPtr loader_version);
VanillaCreationTask(BaseVersion::Ptr version) : InstanceCreationTask(), m_version(std::move(version)) {}
VanillaCreationTask(BaseVersion::Ptr version, QString loader, BaseVersion::Ptr loader_version);
bool createInstance() override;
private:
// Version to update to / create of the instance.
BaseVersionPtr m_version;
BaseVersion::Ptr m_version;
bool m_using_loader = false;
QString m_loader;
BaseVersionPtr m_loader_version;
BaseVersion::Ptr m_loader_version;
};

View File

@ -173,7 +173,7 @@ bool WorldList::resetIcon(int row)
int WorldList::columnCount(const QModelIndex &parent) const
{
return 4;
return parent.isValid()? 0 : 4;
}
QVariant WorldList::data(const QModelIndex &index, int role) const
@ -398,8 +398,8 @@ void WorldList::installWorld(QFileInfo filename)
w.install(m_dir.absolutePath());
}
bool WorldList::dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column,
const QModelIndex &parent)
bool WorldList::dropMimeData(const QMimeData *data, Qt::DropAction action, [[maybe_unused]] int row, [[maybe_unused]] int column,
[[maybe_unused]] const QModelIndex &parent)
{
if (action == Qt::IgnoreAction)
return true;

View File

@ -54,7 +54,7 @@ public:
virtual int rowCount(const QModelIndex &parent = QModelIndex()) const
{
return size();
return parent.isValid() ? 0 : static_cast<int>(size());
};
virtual QVariant headerData(int section, Qt::Orientation orientation,
int role = Qt::DisplayRole) const;

View File

@ -408,20 +408,20 @@ QVariant AccountList::headerData(int section, Qt::Orientation orientation, int r
}
}
int AccountList::rowCount(const QModelIndex &) const
int AccountList::rowCount(const QModelIndex &parent) const
{
// Return count
return count();
return parent.isValid() ? 0 : count();
}
int AccountList::columnCount(const QModelIndex &) const
int AccountList::columnCount(const QModelIndex &parent) const
{
return NUM_COLUMNS;
return parent.isValid() ? 0 : NUM_COLUMNS;
}
Qt::ItemFlags AccountList::flags(const QModelIndex &index) const
{
if (index.row() < 0 || index.row() >= rowCount(index) || !index.isValid())
if (index.row() < 0 || index.row() >= rowCount(index.parent()) || !index.isValid())
{
return Qt::NoItemFlags;
}

View File

@ -71,5 +71,7 @@ void VerifyJavaInstall::executeTask() {
{
emit logLine(tr("Java version %1").arg(major), MessageLevel::Error);
}
emit logLine(tr("Go to instance Java settings to change your Java version or disable the Java compatibility check if you know what you're doing."), MessageLevel::Error);
emitFailed(QString("Incompatible Java major version"));
}

View File

@ -144,7 +144,7 @@ QVariant ModFolderModel::headerData(int section, Qt::Orientation orientation, in
int ModFolderModel::columnCount(const QModelIndex &parent) const
{
return NUM_COLUMNS;
return parent.isValid() ? 0 : NUM_COLUMNS;
}
Task* ModFolderModel::createUpdateTask()

View File

@ -426,7 +426,7 @@ QVariant ResourceFolderModel::data(const QModelIndex& index, int role) const
bool ResourceFolderModel::setData(const QModelIndex& index, const QVariant& value, int role)
{
int row = index.row();
if (row < 0 || row >= rowCount(index) || !index.isValid())
if (row < 0 || row >= rowCount(index.parent()) || !index.isValid())
return false;
if (role == Qt::CheckStateRole)

View File

@ -90,8 +90,8 @@ class ResourceFolderModel : public QAbstractListModel {
/* Basic columns */
enum Columns { ACTIVE_COLUMN = 0, NAME_COLUMN, DATE_COLUMN, NUM_COLUMNS };
[[nodiscard]] int rowCount(const QModelIndex& = {}) const override { return size(); }
[[nodiscard]] int columnCount(const QModelIndex& = {}) const override { return NUM_COLUMNS; };
[[nodiscard]] int rowCount(const QModelIndex& parent = {}) const override { return parent.isValid() ? 0 : static_cast<int>(size()); }
[[nodiscard]] int columnCount(const QModelIndex& parent = {}) const override { return parent.isValid() ? 0 : NUM_COLUMNS; };
[[nodiscard]] Qt::DropActions supportedDropActions() const override;
@ -176,7 +176,7 @@ class ResourceFolderModel : public QAbstractListModel {
* if the resource is complex and has more stuff to parse.
*/
virtual void onParseSucceeded(int ticket, QString resource_id);
virtual void onParseFailed(int ticket, QString resource_id) {}
virtual void onParseFailed(int ticket, QString resource_id) { Q_UNUSED(ticket); Q_UNUSED(resource_id); }
protected:
// Represents the relationship between a column's index (represented by the list index), and it's sorting key.

View File

@ -15,7 +15,7 @@ static const QMap<int, std::pair<Version, Version>> s_pack_format_versions = {
{ 3, { Version("1.11"), Version("1.12.2") } }, { 4, { Version("1.13"), Version("1.14.4") } },
{ 5, { Version("1.15"), Version("1.16.1") } }, { 6, { Version("1.16.2"), Version("1.16.5") } },
{ 7, { Version("1.17"), Version("1.17.1") } }, { 8, { Version("1.18"), Version("1.18.2") } },
{ 9, { Version("1.19"), Version("1.19.2") } },
{ 9, { Version("1.19"), Version("1.19.2") } }, { 11, { Version("1.19.3"), Version("1.19.3") } },
};
void ResourcePack::setPackFormat(int new_format_id)
@ -114,3 +114,8 @@ bool ResourcePack::applyFilter(QRegularExpression filter) const
return Resource::applyFilter(filter);
}
bool ResourcePack::valid() const
{
return m_pack_format != 0;
}

View File

@ -42,6 +42,8 @@ class ResourcePack : public Resource {
/** Thread-safe. */
void setImage(QImage new_image);
bool valid() const override;
[[nodiscard]] auto compare(Resource const& other, SortType type) const -> std::pair<int, bool> override;
[[nodiscard]] bool applyFilter(QRegularExpression filter) const override;

View File

@ -137,7 +137,7 @@ QVariant ResourcePackFolderModel::headerData(int section, Qt::Orientation orient
int ResourcePackFolderModel::columnCount(const QModelIndex& parent) const
{
return NUM_COLUMNS;
return parent.isValid() ? 0 : NUM_COLUMNS;
}
Task* ResourcePackFolderModel::createUpdateTask()

View File

@ -62,3 +62,8 @@ QPixmap TexturePack::image(QSize size)
TexturePackUtils::process(*this);
return image(size);
}
bool TexturePack::valid() const
{
return m_description != nullptr;
}

View File

@ -48,6 +48,8 @@ class TexturePack : public Resource {
/** Thread-safe. */
void setImage(QImage new_image);
bool valid() const override;
protected:
mutable QMutex m_data_lock;

View File

@ -121,7 +121,7 @@ ModDetails ReadMCModTOML(QByteArray contents)
return {};
}
auto modsTable = tomlModsTable0->as_table();
if (!tomlModsTable0) {
if (!modsTable) {
qWarning() << "Corrupted mods.toml? [[mods]] was not a table!";
return {};
}

View File

@ -36,7 +36,7 @@ LocalModUpdateTask::LocalModUpdateTask(QDir index_dir, ModPlatform::IndexedPack&
}
#ifdef Q_OS_WIN32
SetFileAttributesA(index_dir.path().toStdString().c_str(), FILE_ATTRIBUTE_HIDDEN | FILE_ATTRIBUTE_NOT_CONTENT_INDEXED);
SetFileAttributesW(index_dir.path().toStdWString().c_str(), FILE_ATTRIBUTE_HIDDEN | FILE_ATTRIBUTE_NOT_CONTENT_INDEXED);
#endif
}

View File

@ -28,14 +28,14 @@
namespace ResourcePackUtils {
bool process(ResourcePack& pack)
bool process(ResourcePack& pack, ProcessingLevel level)
{
switch (pack.type()) {
case ResourceType::FOLDER:
ResourcePackUtils::processFolder(pack);
ResourcePackUtils::processFolder(pack, level);
return true;
case ResourceType::ZIPFILE:
ResourcePackUtils::processZIP(pack);
ResourcePackUtils::processZIP(pack, level);
return true;
default:
qWarning() << "Invalid type for resource pack parse task!";
@ -43,7 +43,7 @@ bool process(ResourcePack& pack)
}
}
void processFolder(ResourcePack& pack)
void processFolder(ResourcePack& pack, ProcessingLevel level)
{
Q_ASSERT(pack.type() == ResourceType::FOLDER);
@ -60,6 +60,9 @@ void processFolder(ResourcePack& pack)
mcmeta_file.close();
}
if (level == ProcessingLevel::BasicInfoOnly)
return;
QFileInfo image_file_info(FS::PathCombine(pack.fileinfo().filePath(), "pack.png"));
if (image_file_info.isFile()) {
QFile mcmeta_file(image_file_info.filePath());
@ -74,7 +77,7 @@ void processFolder(ResourcePack& pack)
}
}
void processZIP(ResourcePack& pack)
void processZIP(ResourcePack& pack, ProcessingLevel level)
{
Q_ASSERT(pack.type() == ResourceType::ZIPFILE);
@ -98,6 +101,11 @@ void processZIP(ResourcePack& pack)
file.close();
}
if (level == ProcessingLevel::BasicInfoOnly) {
zip.close();
return;
}
if (zip.setCurrentFile("pack.png")) {
if (!file.open(QIODevice::ReadOnly)) {
qCritical() << "Failed to open file in zip.";
@ -138,6 +146,13 @@ void processPackPNG(ResourcePack& pack, QByteArray&& raw_data)
qWarning() << "Failed to parse pack.png.";
}
}
bool validate(QFileInfo file)
{
ResourcePack rp{ file };
return ResourcePackUtils::process(rp, ProcessingLevel::BasicInfoOnly) && rp.valid();
}
} // namespace ResourcePackUtils
LocalResourcePackParseTask::LocalResourcePackParseTask(int token, ResourcePack& rp)
@ -152,8 +167,6 @@ bool LocalResourcePackParseTask::abort()
void LocalResourcePackParseTask::executeTask()
{
Q_ASSERT(m_resource_pack.valid());
if (!ResourcePackUtils::process(m_resource_pack))
return;

View File

@ -26,13 +26,19 @@
#include "tasks/Task.h"
namespace ResourcePackUtils {
bool process(ResourcePack& pack);
void processZIP(ResourcePack& pack);
void processFolder(ResourcePack& pack);
enum class ProcessingLevel { Full, BasicInfoOnly };
bool process(ResourcePack& pack, ProcessingLevel level = ProcessingLevel::Full);
void processZIP(ResourcePack& pack, ProcessingLevel level = ProcessingLevel::Full);
void processFolder(ResourcePack& pack, ProcessingLevel level = ProcessingLevel::Full);
void processMCMeta(ResourcePack& pack, QByteArray&& raw_data);
void processPackPNG(ResourcePack& pack, QByteArray&& raw_data);
/** Checks whether a file is valid as a resource pack or not. */
bool validate(QFileInfo file);
} // namespace ResourcePackUtils
class LocalResourcePackParseTask : public Task {

View File

@ -28,14 +28,14 @@
namespace TexturePackUtils {
bool process(TexturePack& pack)
bool process(TexturePack& pack, ProcessingLevel level)
{
switch (pack.type()) {
case ResourceType::FOLDER:
TexturePackUtils::processFolder(pack);
TexturePackUtils::processFolder(pack, level);
return true;
case ResourceType::ZIPFILE:
TexturePackUtils::processZIP(pack);
TexturePackUtils::processZIP(pack, level);
return true;
default:
qWarning() << "Invalid type for resource pack parse task!";
@ -43,7 +43,7 @@ bool process(TexturePack& pack)
}
}
void processFolder(TexturePack& pack)
void processFolder(TexturePack& pack, ProcessingLevel level)
{
Q_ASSERT(pack.type() == ResourceType::FOLDER);
@ -60,6 +60,9 @@ void processFolder(TexturePack& pack)
mcmeta_file.close();
}
if (level == ProcessingLevel::BasicInfoOnly)
return;
QFileInfo image_file_info(FS::PathCombine(pack.fileinfo().filePath(), "pack.png"));
if (image_file_info.isFile()) {
QFile mcmeta_file(image_file_info.filePath());
@ -74,7 +77,7 @@ void processFolder(TexturePack& pack)
}
}
void processZIP(TexturePack& pack)
void processZIP(TexturePack& pack, ProcessingLevel level)
{
Q_ASSERT(pack.type() == ResourceType::ZIPFILE);
@ -98,6 +101,11 @@ void processZIP(TexturePack& pack)
file.close();
}
if (level == ProcessingLevel::BasicInfoOnly) {
zip.close();
return;
}
if (zip.setCurrentFile("pack.png")) {
if (!file.open(QIODevice::ReadOnly)) {
qCritical() << "Failed to open file in zip.";
@ -129,6 +137,13 @@ void processPackPNG(TexturePack& pack, QByteArray&& raw_data)
qWarning() << "Failed to parse pack.png.";
}
}
bool validate(QFileInfo file)
{
TexturePack rp{ file };
return TexturePackUtils::process(rp, ProcessingLevel::BasicInfoOnly) && rp.valid();
}
} // namespace TexturePackUtils
LocalTexturePackParseTask::LocalTexturePackParseTask(int token, TexturePack& rp)
@ -143,8 +158,6 @@ bool LocalTexturePackParseTask::abort()
void LocalTexturePackParseTask::executeTask()
{
Q_ASSERT(m_texture_pack.valid());
if (!TexturePackUtils::process(m_texture_pack))
return;

View File

@ -27,13 +27,19 @@
#include "tasks/Task.h"
namespace TexturePackUtils {
bool process(TexturePack& pack);
void processZIP(TexturePack& pack);
void processFolder(TexturePack& pack);
enum class ProcessingLevel { Full, BasicInfoOnly };
bool process(TexturePack& pack, ProcessingLevel level = ProcessingLevel::Full);
void processZIP(TexturePack& pack, ProcessingLevel level = ProcessingLevel::Full);
void processFolder(TexturePack& pack, ProcessingLevel level = ProcessingLevel::Full);
void processPackTXT(TexturePack& pack, QByteArray&& raw_data);
void processPackPNG(TexturePack& pack, QByteArray&& raw_data);
/** Checks whether a file is valid as a texture pack or not. */
bool validate(QFileInfo file);
} // namespace TexturePackUtils
class LocalTexturePackParseTask : public Task {

View File

@ -39,7 +39,7 @@
#include <QList>
#include <list>
#include "Version.h"
#include "../Version.h"
#include "net/NetJob.h"
namespace ModPlatform {

View File

@ -58,7 +58,7 @@
namespace ATLauncher {
static Meta::VersionPtr getComponentVersion(const QString& uid, const QString& version);
static Meta::Version::Ptr getComponentVersion(const QString& uid, const QString& version);
PackInstallTask::PackInstallTask(UserInteractionSupport *support, QString packName, QString version, InstallMode installMode)
{
@ -1037,7 +1037,7 @@ void PackInstallTask::install()
emitSucceeded();
}
static Meta::VersionPtr getComponentVersion(const QString& uid, const QString& version)
static Meta::Version::Ptr getComponentVersion(const QString& uid, const QString& version)
{
auto vlist = APPLICATION->metadataIndex()->get(uid);
if (!vlist)

View File

@ -68,7 +68,7 @@ public:
* Requests a user interaction to select a component version from a given version list
* and constrained to a given Minecraft version.
*/
virtual QString chooseVersion(Meta::VersionListPtr vlist, QString minecraftVersion) = 0;
virtual QString chooseVersion(Meta::VersionList::Ptr vlist, QString minecraftVersion) = 0;
/**
* Requests a user interaction to display a message.
@ -137,8 +137,8 @@ private:
QString archivePath;
QStringList jarmods;
Meta::VersionPtr minecraftVersion;
QMap<QString, Meta::VersionPtr> componentsToInstall;
Meta::Version::Ptr minecraftVersion;
QMap<QString, Meta::Version::Ptr> componentsToInstall;
QFuture<std::optional<QStringList>> m_extractFuture;
QFutureWatcher<std::optional<QStringList>> m_extractFutureWatcher;

View File

@ -3,6 +3,8 @@
#include "Json.h"
#include "net/Upload.h"
#include "modplatform/modrinth/ModrinthPackIndex.h"
Flame::FileResolvingTask::FileResolvingTask(const shared_qobject_ptr<QNetworkAccessManager>& network, Flame::Manifest& toProcess)
: m_network(network), m_toProcess(toProcess)
{}
@ -40,12 +42,25 @@ void Flame::FileResolvingTask::executeTask()
void Flame::FileResolvingTask::netJobFinished()
{
setProgress(1, 3);
int index = 0;
// job to check modrinth for blocked projects
m_checkJob = new NetJob("Modrinth check", m_network);
blockedProjects = QMap<File *,QByteArray *>();
auto doc = Json::requireDocument(*result);
auto array = Json::requireArray(doc.object()["data"]);
QJsonDocument doc;
QJsonArray array;
try {
doc = Json::requireDocument(*result);
array = Json::requireArray(doc.object()["data"]);
} catch (Json::JsonException& e) {
qCritical() << "Non-JSON data returned from the CF API";
qCritical() << e.cause();
emitFailed(tr("Invalid data returned from the API."));
return;
}
for (QJsonValueRef file : array) {
auto fileid = Json::requireInteger(Json::requireObject(file)["id"]);
auto& out = m_toProcess.files[fileid];
@ -66,7 +81,6 @@ void Flame::FileResolvingTask::netJobFinished()
blockedProjects.insert(&out, output);
}
}
index++;
}
connect(m_checkJob.get(), &NetJob::finished, this, &Flame::FileResolvingTask::modrinthCheckFinished);
@ -84,18 +98,21 @@ void Flame::FileResolvingTask::modrinthCheckFinished() {
delete bytes;
continue;
}
QJsonDocument doc = QJsonDocument::fromJson(*bytes);
auto obj = doc.object();
auto array = Json::requireArray(obj,"files");
for (auto file: array) {
auto fileObj = Json::requireObject(file);
auto primary = Json::requireBoolean(fileObj,"primary");
if (primary) {
out->url = Json::requireUrl(fileObj,"url");
qDebug() << "Found alternative on modrinth " << out->fileName;
break;
}
auto file = Modrinth::loadIndexedPackVersion(obj);
// If there's more than one mod loader for this version, we can't know for sure
// which file is relative to each loader, so it's best to not use any one and
// let the user download it manually.
if (file.loaders.size() <= 1) {
out->url = file.downloadUrl;
qDebug() << "Found alternative on modrinth " << out->fileName;
} else {
out->resolved = false;
}
delete bytes;
}
//copy to an output list and filter out projects found on modrinth

View File

@ -1,3 +1,38 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* This file incorporates work covered by the following copyright and
* permission notice:
*
* Copyright 2013-2021 MultiMC Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "FlameInstanceCreationTask.h"
#include "modplatform/flame/FlameAPI.h"
@ -72,7 +107,8 @@ bool FlameCreationTask::updateInstance()
tr("One or more of your instances are from this same modpack%1. Do you want to create a "
"separate instance, or update the existing one?\n\nNOTE: Make sure you made a backup of your important instance data before "
"updating, as worlds can be corrupted and some configuration may be lost (due to pack overrides).")
.arg(version_str), QMessageBox::Information, QMessageBox::Ok | QMessageBox::Reset | QMessageBox::Abort);
.arg(version_str),
QMessageBox::Information, QMessageBox::Ok | QMessageBox::Reset | QMessageBox::Abort);
info->setButtonText(QMessageBox::Ok, tr("Update existing instance"));
info->setButtonText(QMessageBox::Abort, tr("Create new instance"));
info->setButtonText(QMessageBox::Reset, tr("Cancel"));
@ -197,10 +233,10 @@ bool FlameCreationTask::updateInstance()
m_process_update_file_info_job = nullptr;
} else {
// We don't have an old index file, so we may duplicate stuff!
auto dialog = CustomMessageBox::selectable(m_parent,
tr("No index file."),
tr("We couldn't find a suitable index file for the older version. This may cause some of the files to be duplicated. Do you want to continue?"),
QMessageBox::Warning, QMessageBox::Ok | QMessageBox::Cancel);
auto dialog = CustomMessageBox::selectable(m_parent, tr("No index file."),
tr("We couldn't find a suitable index file for the older version. This may cause some "
"of the files to be duplicated. Do you want to continue?"),
QMessageBox::Warning, QMessageBox::Ok | QMessageBox::Cancel);
if (dialog->exec() == QDialog::DialogCode::Rejected) {
m_abort = true;
@ -338,6 +374,7 @@ bool FlameCreationTask::createInstance()
connect(m_mod_id_resolver.get(), &Flame::FileResolvingTask::failed, [&](QString reason) {
m_mod_id_resolver.reset();
setError(tr("Unable to resolve mod IDs:\n") + reason);
loop.quit();
});
connect(m_mod_id_resolver.get(), &Flame::FileResolvingTask::progress, this, &FlameCreationTask::setProgress);
connect(m_mod_id_resolver.get(), &Flame::FileResolvingTask::status, this, &FlameCreationTask::setStatus);
@ -372,27 +409,35 @@ void FlameCreationTask::idResolverSucceeded(QEventLoop& loop)
auto results = m_mod_id_resolver->getResults();
// first check for blocked mods
QString text;
QList<QUrl> urls;
QList<BlockedMod> blocked_mods;
auto anyBlocked = false;
for (const auto& result : results.files.values()) {
if (!result.resolved || result.url.isEmpty()) {
text += QString("%1: <a href='%2'>%2</a><br/>").arg(result.fileName, result.websiteUrl);
urls.append(QUrl(result.websiteUrl));
BlockedMod blocked_mod;
blocked_mod.name = result.fileName;
blocked_mod.websiteUrl = result.websiteUrl;
blocked_mod.hash = result.hash;
blocked_mod.matched = false;
blocked_mod.localPath = "";
blocked_mods.append(blocked_mod);
anyBlocked = true;
}
}
if (anyBlocked) {
qWarning() << "Blocked mods found, displaying mod list";
auto message_dialog = new BlockedModsDialog(m_parent, tr("Blocked mods found"),
tr("The following mods were blocked on third party launchers.<br/>"
"You will need to manually download them and add them to the modpack"),
text,
urls);
message_dialog->setModal(true);
BlockedModsDialog message_dialog(m_parent, tr("Blocked mods found"),
tr("The following files are not available for download in third party launchers.<br/>"
"You will need to manually download them and add them to the instance."),
blocked_mods);
if (message_dialog->exec()) {
message_dialog.setModal(true);
if (message_dialog.exec()) {
qDebug() << "Post dialog blocked mods list: " << blocked_mods;
copyBlockedMods(blocked_mods);
setupDownloadJob(loop);
} else {
m_mod_id_resolver.reset();
@ -404,6 +449,38 @@ void FlameCreationTask::idResolverSucceeded(QEventLoop& loop)
}
}
/// @brief copy the matched blocked mods to the instance staging area
/// @param blocked_mods list of the blocked mods and their matched paths
void FlameCreationTask::copyBlockedMods(QList<BlockedMod> const& blocked_mods)
{
setStatus(tr("Copying Blocked Mods..."));
setAbortable(false);
int i = 0;
int total = blocked_mods.length();
setProgress(i, total);
for (auto const& mod : blocked_mods) {
if (!mod.matched) {
qDebug() << mod.name << "was not matched to a local file, skipping copy";
continue;
}
auto dest_path = FS::PathCombine(m_stagingPath, "minecraft", "mods", mod.name);
setStatus(tr("Copying Blocked Mods (%1 out of %2 are done)").arg(QString::number(i), QString::number(total)));
qDebug() << "Will try to copy" << mod.localPath << "to" << dest_path;
if (!FS::copy(mod.localPath, dest_path)()) {
qDebug() << "Copy of" << mod.localPath << "to" << dest_path << "Failed";
}
i++;
setProgress(i, total);
}
setAbortable(true);
}
void FlameCreationTask::setupDownloadJob(QEventLoop& loop)
{
m_files_job = new NetJob(tr("Mod download"), APPLICATION->network());
@ -442,14 +519,12 @@ void FlameCreationTask::setupDownloadJob(QEventLoop& loop)
}
m_mod_id_resolver.reset();
connect(m_files_job.get(), &NetJob::succeeded, this, [&]() {
m_files_job.reset();
});
connect(m_files_job.get(), &NetJob::succeeded, this, [&]() { m_files_job.reset(); });
connect(m_files_job.get(), &NetJob::failed, [&](QString reason) {
m_files_job.reset();
setError(reason);
});
connect(m_files_job.get(), &NetJob::progress, [&](qint64 current, qint64 total) { setProgress(current, total); });
connect(m_files_job.get(), &NetJob::progress, this, &FlameCreationTask::setProgress);
connect(m_files_job.get(), &NetJob::finished, &loop, &QEventLoop::quit);
setStatus(tr("Downloading mods..."));

View File

@ -1,3 +1,38 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* This file incorporates work covered by the following copyright and
* permission notice:
*
* Copyright 2013-2021 MultiMC Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#pragma once
#include "InstanceCreationTask.h"
@ -10,6 +45,8 @@
#include "net/NetJob.h"
#include "ui/dialogs/BlockedModsDialog.h"
class FlameCreationTask final : public InstanceCreationTask {
Q_OBJECT
@ -29,6 +66,7 @@ class FlameCreationTask final : public InstanceCreationTask {
private slots:
void idResolverSucceeded(QEventLoop&);
void setupDownloadJob(QEventLoop&);
void copyBlockedMods(QList<BlockedMod> const& blocked_mods);
private:
QWidget* m_parent = nullptr;

View File

@ -4,6 +4,7 @@
#include <QFile>
#include "FileSystem.h"
#include "StringUtils.h"
#include <MurmurHash2.h>
@ -35,6 +36,18 @@ Hasher::Ptr createFlameHasher(QString file_path)
return new FlameHasher(file_path);
}
Hasher::Ptr createBlockedModHasher(QString file_path, ModPlatform::Provider provider)
{
return new BlockedModHasher(file_path, provider);
}
Hasher::Ptr createBlockedModHasher(QString file_path, ModPlatform::Provider provider, QString type)
{
auto hasher = new BlockedModHasher(file_path, provider);
hasher->useHashType(type);
return hasher;
}
void ModrinthHasher::executeTask()
{
QFile file(m_path);
@ -66,7 +79,7 @@ void FlameHasher::executeTask()
// CF-specific
auto should_filter_out = [](char c) { return (c == 9 || c == 10 || c == 13 || c == 32); };
std::ifstream file_stream(m_path.toStdString(), std::ifstream::binary);
std::ifstream file_stream(StringUtils::toStdString(m_path), std::ifstream::binary);
// TODO: This is very heavy work, but apparently QtConcurrent can't use move semantics, so we can't boop this to another thread.
// How do we make this non-blocking then?
m_hash = QString::number(MurmurHash2(std::move(file_stream), 4 * MiB, should_filter_out));
@ -78,4 +91,50 @@ void FlameHasher::executeTask()
}
}
BlockedModHasher::BlockedModHasher(QString file_path, ModPlatform::Provider provider)
: Hasher(file_path), provider(provider) {
setObjectName(QString("BlockedModHasher: %1").arg(file_path));
hash_type = ProviderCaps.hashType(provider).first();
}
void BlockedModHasher::executeTask()
{
QFile file(m_path);
try {
file.open(QFile::ReadOnly);
} catch (FS::FileSystemException& e) {
qCritical() << QString("Failed to open JAR file in %1").arg(m_path);
qCritical() << QString("Reason: ") << e.cause();
emitFailed("Failed to open file for hashing.");
return;
}
m_hash = ProviderCaps.hash(provider, &file, hash_type);
file.close();
if (m_hash.isEmpty()) {
emitFailed("Empty hash!");
} else {
emitSucceeded();
}
}
QStringList BlockedModHasher::getHashTypes() {
return ProviderCaps.hashType(provider);
}
bool BlockedModHasher::useHashType(QString type) {
auto types = ProviderCaps.hashType(provider);
if (types.contains(type)) {
hash_type = type;
return true;
}
qDebug() << "Bad hash type " << type << " for provider";
return false;
}
} // namespace Hashing

View File

@ -40,8 +40,23 @@ class ModrinthHasher : public Hasher {
void executeTask() override;
};
class BlockedModHasher : public Hasher {
public:
BlockedModHasher(QString file_path, ModPlatform::Provider provider);
void executeTask() override;
QStringList getHashTypes();
bool useHashType(QString type);
private:
ModPlatform::Provider provider;
QString hash_type;
};
Hasher::Ptr createHasher(QString file_path, ModPlatform::Provider provider);
Hasher::Ptr createFlameHasher(QString file_path);
Hasher::Ptr createModrinthHasher(QString file_path);
Hasher::Ptr createBlockedModHasher(QString file_path, ModPlatform::Provider provider);
Hasher::Ptr createBlockedModHasher(QString file_path, ModPlatform::Provider provider, QString type);
} // namespace Hashing

View File

@ -15,6 +15,7 @@ void NetworkModAPI::searchMods(CallerType* caller, SearchArgs&& args) const
QObject::connect(netJob, &NetJob::started, caller, [caller, netJob] { caller->setActiveJob(netJob); });
QObject::connect(netJob, &NetJob::failed, caller, &CallerType::searchRequestFailed);
QObject::connect(netJob, &NetJob::aborted, caller, &CallerType::searchRequestAborted);
QObject::connect(netJob, &NetJob::succeeded, caller, [caller, response] {
QJsonParseError parse_error{};
QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error);

View File

@ -1,6 +1,6 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* PolyMC - Minecraft Launcher
* Prism Launcher - Minecraft Launcher
* Copyright (C) 2022 flowln <flowlnlnln@gmail.com>
* Copyright (c) 2022 Jamie Mansfield <jmansfield@cadixdev.org>
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
@ -176,8 +176,6 @@ void PackInstallTask::resolveMods()
void PackInstallTask::onResolveModsSucceeded()
{
QString text;
QList<QUrl> urls;
auto anyBlocked = false;
Flame::Manifest results = m_mod_id_resolver_task->getResults();
@ -191,11 +189,15 @@ void PackInstallTask::onResolveModsSucceeded()
// First check for blocked mods
if (!results_file.resolved || results_file.url.isEmpty()) {
QString type(local_file.type);
BlockedMod blocked_mod;
blocked_mod.name = local_file.name;
blocked_mod.websiteUrl = results_file.websiteUrl;
blocked_mod.hash = results_file.hash;
blocked_mod.matched = false;
blocked_mod.localPath = "";
m_blocked_mods.append(blocked_mod);
type[0] = type[0].toUpper();
text += QString("%1: %2 - <a href='%3'>%3</a><br/>").arg(type, local_file.name, results_file.websiteUrl);
urls.append(QUrl(results_file.websiteUrl));
anyBlocked = true;
} else {
local_file.url = results_file.url.toString();
@ -207,16 +209,20 @@ void PackInstallTask::onResolveModsSucceeded()
if (anyBlocked) {
qDebug() << "Blocked files found, displaying file list";
auto message_dialog = new BlockedModsDialog(m_parent, tr("Blocked files found"),
tr("The following files are not available for download in third party launchers.<br/>"
"You will need to manually download them and add them to the instance."),
text,
urls);
BlockedModsDialog message_dialog(m_parent, tr("Blocked files found"),
tr("The following files are not available for download in third party launchers.<br/>"
"You will need to manually download them and add them to the instance."),
m_blocked_mods);
if (message_dialog->exec() == QDialog::Accepted)
message_dialog.setModal(true);
if (message_dialog.exec() == QDialog::Accepted) {
qDebug() << "Post dialog blocked mods list: " << m_blocked_mods;
createInstance();
else
} else {
abort();
}
} else {
createInstance();
}
@ -320,6 +326,9 @@ void PackInstallTask::downloadPack()
void PackInstallTask::onModDownloadSucceeded()
{
m_net_job.reset();
if (!m_blocked_mods.isEmpty()) {
copyBlockedMods();
}
emitSucceeded();
}
@ -343,4 +352,35 @@ void PackInstallTask::onModDownloadFailed(QString reason)
emitFailed(reason);
}
/// @brief copy the matched blocked mods to the instance staging area
void PackInstallTask::copyBlockedMods()
{
setStatus(tr("Copying Blocked Mods..."));
setAbortable(false);
int i = 0;
int total = m_blocked_mods.length();
setProgress(i, total);
for (auto const& mod : m_blocked_mods) {
if (!mod.matched) {
qDebug() << mod.name << "was not matched to a local file, skipping copy";
continue;
}
auto dest_path = FS::PathCombine(m_stagingPath, ".minecraft", "mods", mod.name);
setStatus(tr("Copying Blocked Mods (%1 out of %2 are done)").arg(QString::number(i), QString::number(total)));
qDebug() << "Will try to copy" << mod.localPath << "to" << dest_path;
if (!FS::copy(mod.localPath, dest_path)()) {
qDebug() << "Copy of" << mod.localPath << "to" << dest_path << "Failed";
}
i++;
setProgress(i, total);
}
setAbortable(true);
}
} // namespace ModpacksCH

View File

@ -1,6 +1,6 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* PolyMC - Minecraft Launcher
* Prism Launcher - Minecraft Launcher
* Copyright (C) 2022 flowln <flowlnlnln@gmail.com>
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
*
@ -43,6 +43,7 @@
#include "QObjectPtr.h"
#include "modplatform/flame/FileResolvingTask.h"
#include "net/NetJob.h"
#include "ui/dialogs/BlockedModsDialog.h"
#include <QWidget>
@ -76,6 +77,7 @@ private:
void resolveMods();
void createInstance();
void downloadPack();
void copyBlockedMods();
private:
NetJob::Ptr m_net_job = nullptr;
@ -90,6 +92,7 @@ private:
Version m_version;
QMap<QString, QString> m_files_to_copy;
QList<BlockedMod> m_blocked_mods;
//FIXME: nuke
QWidget* m_parent;

View File

@ -1,20 +1,20 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* PolyMC - Minecraft Launcher
* Copyright (c) 2022 flowln <flowlnlnln@gmail.com>
*
* 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/>.
*/
* PolyMC - Minecraft Launcher
* Copyright (c) 2022 flowln <flowlnlnln@gmail.com>
*
* 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 "ModrinthPackIndex.h"
#include "ModrinthAPI.h"
@ -35,7 +35,7 @@ void Modrinth::loadIndexedPack(ModPlatform::IndexedPack& pack, QJsonObject& obj)
pack.provider = ModPlatform::Provider::MODRINTH;
pack.name = Json::requireString(obj, "title");
pack.slug = Json::ensureString(obj, "slug", "");
if (!pack.slug.isEmpty())
pack.websiteUrl = "https://modrinth.com/mod/" + pack.slug;
@ -59,23 +59,23 @@ void Modrinth::loadIndexedPack(ModPlatform::IndexedPack& pack, QJsonObject& obj)
void Modrinth::loadExtraPackData(ModPlatform::IndexedPack& pack, QJsonObject& obj)
{
pack.extraData.issuesUrl = Json::ensureString(obj, "issues_url");
if(pack.extraData.issuesUrl.endsWith('/'))
if (pack.extraData.issuesUrl.endsWith('/'))
pack.extraData.issuesUrl.chop(1);
pack.extraData.sourceUrl = Json::ensureString(obj, "source_url");
if(pack.extraData.sourceUrl.endsWith('/'))
if (pack.extraData.sourceUrl.endsWith('/'))
pack.extraData.sourceUrl.chop(1);
pack.extraData.wikiUrl = Json::ensureString(obj, "wiki_url");
if(pack.extraData.wikiUrl.endsWith('/'))
if (pack.extraData.wikiUrl.endsWith('/'))
pack.extraData.wikiUrl.chop(1);
pack.extraData.discordUrl = Json::ensureString(obj, "discord_url");
if(pack.extraData.discordUrl.endsWith('/'))
if (pack.extraData.discordUrl.endsWith('/'))
pack.extraData.discordUrl.chop(1);
auto donate_arr = Json::ensureArray(obj, "donation_urls");
for(auto d : donate_arr){
for (auto d : donate_arr) {
auto d_obj = Json::requireObject(d);
ModPlatform::DonationData donate;
@ -104,7 +104,7 @@ void Modrinth::loadIndexedPackVersions(ModPlatform::IndexedPack& pack,
auto obj = versionIter.toObject();
auto file = loadIndexedPackVersion(obj);
if(file.fileId.isValid()) // Heuristic to check if the returned value is valid
if (file.fileId.isValid()) // Heuristic to check if the returned value is valid
unsortedVersions.append(file);
}
auto orderSortPredicate = [](const ModPlatform::IndexedVersion& a, const ModPlatform::IndexedVersion& b) -> bool {
@ -116,7 +116,8 @@ void Modrinth::loadIndexedPackVersions(ModPlatform::IndexedPack& pack,
pack.versionsLoaded = true;
}
auto Modrinth::loadIndexedPackVersion(QJsonObject &obj, QString preferred_hash_type, QString preferred_file_name) -> ModPlatform::IndexedVersion
auto Modrinth::loadIndexedPackVersion(QJsonObject& obj, QString preferred_hash_type, QString preferred_file_name)
-> ModPlatform::IndexedVersion
{
ModPlatform::IndexedVersion file;
@ -141,6 +142,12 @@ auto Modrinth::loadIndexedPackVersion(QJsonObject &obj, QString preferred_hash_t
auto files = Json::requireArray(obj, "files");
int i = 0;
if (files.empty()) {
// This should not happen normally, but check just in case
qWarning() << "Modrinth returned an unexpected empty list of files:" << obj;
return {};
}
// Find correct file (needed in cases where one version may have multiple files)
// Will default to the last one if there's no primary (though I think Modrinth requires that
// at least one file is primary, idk)
@ -167,7 +174,7 @@ auto Modrinth::loadIndexedPackVersion(QJsonObject &obj, QString preferred_hash_t
file.fileName = Json::requireString(parent, "filename");
file.is_preferred = Json::requireBoolean(parent, "primary") || (files.count() == 1);
auto hash_list = Json::requireObject(parent, "hashes");
if (hash_list.contains(preferred_hash_type)) {
file.hash = Json::requireString(hash_list, preferred_hash_type);
file.hash_type = preferred_hash_type;

View File

@ -140,7 +140,7 @@ auto loadIndexedVersion(QJsonObject &obj) -> ModpackVersion
for (auto file_iter : files) {
File indexed_file;
auto parent = Json::requireObject(file_iter);
auto is_primary = Json::ensureBoolean(parent, "primary", false);
auto is_primary = Json::ensureBoolean(parent, (const QString)QStringLiteral("primary"), false);
if (!is_primary) {
auto filename = Json::ensureString(parent, "filename");
// Checking suffix here is fine because it's the response from Modrinth,

View File

@ -22,10 +22,14 @@
#include <QDir>
#include <QObject>
#include <toml++/toml.h>
#include "FileSystem.h"
#include "StringUtils.h"
#include "minecraft/mod/Mod.h"
#include "modplatform/ModIndex.h"
#include <toml++/toml.h>
namespace Packwiz {
auto getRealIndexName(QDir& index_dir, QString normalized_fname, bool should_find_match) -> QString
@ -63,22 +67,22 @@ static inline auto indexFileName(QString const& mod_slug) -> QString
static ModPlatform::ProviderCapabilities ProviderCaps;
// Helper functions for extracting data from the TOML file
auto stringEntry(toml::table table, const std::string entry_name) -> QString
auto stringEntry(toml::table table, QString entry_name) -> QString
{
auto node = table[entry_name];
auto node = table[StringUtils::toStdString(entry_name)];
if (!node) {
qCritical() << QString::fromStdString("Failed to read str property '" + entry_name + "' in mod metadata.");
qCritical() << "Failed to read str property '" + entry_name + "' in mod metadata.";
return {};
}
return QString::fromStdString(node.value_or(""));
return node.value_or("");
}
auto intEntry(toml::table table, const std::string entry_name) -> int
auto intEntry(toml::table table, QString entry_name) -> int
{
auto node = table[entry_name];
auto node = table[StringUtils::toStdString(entry_name)];
if (!node) {
qCritical() << QString::fromStdString("Failed to read int property '" + entry_name + "' in mod metadata.");
qCritical() << "Failed to read int property '" + entry_name + "' in mod metadata.";
return {};
}
@ -145,6 +149,8 @@ void V1::updateModIndex(QDir& index_dir, Mod& mod)
// they want to do!
if (index_file.exists()) {
index_file.remove();
} else {
FS::ensureFilePathExists(index_file.fileName());
}
if (!index_file.open(QIODevice::ReadWrite)) {
@ -228,14 +234,14 @@ auto V1::getIndexForMod(QDir& index_dir, QString slug) -> Mod
toml::table table;
#if TOML_EXCEPTIONS
try {
table = toml::parse_file(index_dir.absoluteFilePath(real_fname).toStdString());
table = toml::parse_file(StringUtils::toStdString(index_dir.absoluteFilePath(real_fname)));
} catch (const toml::parse_error& err) {
qWarning() << QString("Could not open file %1!").arg(normalized_fname);
qWarning() << "Reason: " << QString(err.what());
return {};
}
#else
table = toml::parse_file(index_dir.absoluteFilePath(real_fname).toStdString());
table = toml::parse_file(StringUtils::toStdString(index_dir.absoluteFilePath(real_fname)));
if (!table) {
qWarning() << QString("Could not open file %1!").arg(normalized_fname);
qWarning() << "Reason: " << QString(table.error().what());

View File

@ -24,7 +24,6 @@
#include <QUrl>
#include <QVariant>
struct toml_table_t;
class QDir;
// Mod from launcher/minecraft/mod/Mod.h
@ -34,9 +33,6 @@ namespace Packwiz {
auto getRealIndexName(QDir& index_dir, QString normalized_index_name, bool should_match = false) -> QString;
auto stringEntry(toml_table_t* parent, const char* entry_name) -> QString;
auto intEntry(toml_table_t* parent, const char* entry_name) -> int;
class V1 {
public:
struct Mod {

View File

@ -47,7 +47,7 @@
auto MetaEntry::getFullPath() -> QString
{
// FIXME: make local?
return FS::PathCombine(basePath, relativePath);
return FS::PathCombine(m_basePath, m_relativePath);
}
HttpMetaCache::HttpMetaCache(QString path) : QObject(), m_index_file(path)
@ -99,7 +99,7 @@ auto HttpMetaCache::resolveEntry(QString base, QString resource_path, QString ex
return staleEntry(base, resource_path);
}
if (!expected_etag.isEmpty() && expected_etag != entry->etag) {
if (!expected_etag.isEmpty() && expected_etag != entry->m_etag) {
// if the etag doesn't match expected, we disown the entry
selected_base.entry_list.remove(resource_path);
return staleEntry(base, resource_path);
@ -107,17 +107,17 @@ auto HttpMetaCache::resolveEntry(QString base, QString resource_path, QString ex
// if the file changed, check md5sum
qint64 file_last_changed = finfo.lastModified().toUTC().toMSecsSinceEpoch();
if (file_last_changed != entry->local_changed_timestamp) {
if (file_last_changed != entry->m_local_changed_timestamp) {
QFile input(real_path);
input.open(QIODevice::ReadOnly);
QString md5sum = QCryptographicHash::hash(input.readAll(), QCryptographicHash::Md5).toHex().constData();
if (entry->md5sum != md5sum) {
if (entry->m_md5sum != md5sum) {
selected_base.entry_list.remove(resource_path);
return staleEntry(base, resource_path);
}
// md5sums matched... keep entry and save the new state to file
entry->local_changed_timestamp = file_last_changed;
entry->m_local_changed_timestamp = file_last_changed;
SaveEventually();
}
@ -130,23 +130,23 @@ auto HttpMetaCache::resolveEntry(QString base, QString resource_path, QString ex
}
// entry passed all the checks we cared about.
entry->basePath = getBasePath(base);
entry->m_basePath = getBasePath(base);
return entry;
}
auto HttpMetaCache::updateEntry(MetaEntryPtr stale_entry) -> bool
{
if (!m_entries.contains(stale_entry->baseId)) {
qCritical() << "Cannot add entry with unknown base: " << stale_entry->baseId.toLocal8Bit();
if (!m_entries.contains(stale_entry->m_baseId)) {
qCritical() << "Cannot add entry with unknown base: " << stale_entry->m_baseId.toLocal8Bit();
return false;
}
if (stale_entry->stale) {
if (stale_entry->m_stale) {
qCritical() << "Cannot add stale entry: " << stale_entry->getFullPath().toLocal8Bit();
return false;
}
m_entries[stale_entry->baseId].entry_list[stale_entry->relativePath] = stale_entry;
m_entries[stale_entry->m_baseId].entry_list[stale_entry->m_relativePath] = stale_entry;
SaveEventually();
return true;
@ -157,7 +157,7 @@ auto HttpMetaCache::evictEntry(MetaEntryPtr entry) -> bool
if (!entry)
return false;
entry->stale = true;
entry->m_stale = true;
SaveEventually();
return true;
}
@ -169,7 +169,7 @@ void HttpMetaCache::evictAll()
qDebug() << "Evicting base" << base;
for (MetaEntryPtr entry : map.entry_list) {
if (!evictEntry(entry))
qWarning() << "Unexpected missing cache entry" << entry->basePath;
qWarning() << "Unexpected missing cache entry" << entry->m_basePath;
}
}
}
@ -177,10 +177,10 @@ void HttpMetaCache::evictAll()
auto HttpMetaCache::staleEntry(QString base, QString resource_path) -> MetaEntryPtr
{
auto foo = new MetaEntry();
foo->baseId = base;
foo->basePath = getBasePath(base);
foo->relativePath = resource_path;
foo->stale = true;
foo->m_baseId = base;
foo->m_basePath = getBasePath(base);
foo->m_relativePath = resource_path;
foo->m_stale = true;
return MetaEntryPtr(foo);
}
@ -235,23 +235,23 @@ void HttpMetaCache::Load()
auto& entrymap = m_entries[base];
auto foo = new MetaEntry();
foo->baseId = base;
foo->relativePath = Json::ensureString(element_obj, "path");
foo->md5sum = Json::ensureString(element_obj, "md5sum");
foo->etag = Json::ensureString(element_obj, "etag");
foo->local_changed_timestamp = Json::ensureDouble(element_obj, "last_changed_timestamp");
foo->remote_changed_timestamp = Json::ensureString(element_obj, "remote_changed_timestamp");
foo->m_baseId = base;
foo->m_relativePath = Json::ensureString(element_obj, "path");
foo->m_md5sum = Json::ensureString(element_obj, "md5sum");
foo->m_etag = Json::ensureString(element_obj, "etag");
foo->m_local_changed_timestamp = Json::ensureDouble(element_obj, "last_changed_timestamp");
foo->m_remote_changed_timestamp = Json::ensureString(element_obj, "remote_changed_timestamp");
foo->makeEternal(Json::ensureBoolean(element_obj, "eternal", false));
foo->makeEternal(Json::ensureBoolean(element_obj, (const QString)QStringLiteral("eternal"), false));
if (!foo->isEternal()) {
foo->current_age = Json::ensureDouble(element_obj, "current_age");
foo->max_age = Json::ensureDouble(element_obj, "max_age");
foo->m_current_age = Json::ensureDouble(element_obj, "current_age");
foo->m_max_age = Json::ensureDouble(element_obj, "max_age");
}
// presumed innocent until closer examination
foo->stale = false;
foo->m_stale = false;
entrymap.entry_list[foo->relativePath] = MetaEntryPtr(foo);
entrymap.entry_list[foo->m_relativePath] = MetaEntryPtr(foo);
}
}
@ -276,23 +276,23 @@ void HttpMetaCache::SaveNow()
for (auto group : m_entries) {
for (auto entry : group.entry_list) {
// do not save stale entries. they are dead.
if (entry->stale) {
if (entry->m_stale) {
continue;
}
QJsonObject entryObj;
Json::writeString(entryObj, "base", entry->baseId);
Json::writeString(entryObj, "path", entry->relativePath);
Json::writeString(entryObj, "md5sum", entry->md5sum);
Json::writeString(entryObj, "etag", entry->etag);
entryObj.insert("last_changed_timestamp", QJsonValue(double(entry->local_changed_timestamp)));
if (!entry->remote_changed_timestamp.isEmpty())
entryObj.insert("remote_changed_timestamp", QJsonValue(entry->remote_changed_timestamp));
Json::writeString(entryObj, "base", entry->m_baseId);
Json::writeString(entryObj, "path", entry->m_relativePath);
Json::writeString(entryObj, "md5sum", entry->m_md5sum);
Json::writeString(entryObj, "etag", entry->m_etag);
entryObj.insert("last_changed_timestamp", QJsonValue(double(entry->m_local_changed_timestamp)));
if (!entry->m_remote_changed_timestamp.isEmpty())
entryObj.insert("remote_changed_timestamp", QJsonValue(entry->m_remote_changed_timestamp));
if (entry->isEternal()) {
entryObj.insert("eternal", true);
} else {
entryObj.insert("current_age", QJsonValue(double(entry->current_age)));
entryObj.insert("max_age", QJsonValue(double(entry->max_age)));
entryObj.insert("current_age", QJsonValue(double(entry->m_current_age)));
entryObj.insert("max_age", QJsonValue(double(entry->m_max_age)));
}
entriesArr.append(entryObj);
}

View File

@ -49,47 +49,47 @@ class MetaEntry {
MetaEntry() = default;
public:
auto isStale() -> bool { return stale; }
void setStale(bool stale) { this->stale = stale; }
auto isStale() -> bool { return m_stale; }
void setStale(bool stale) { m_stale = stale; }
auto getFullPath() -> QString;
auto getRemoteChangedTimestamp() -> QString { return remote_changed_timestamp; }
void setRemoteChangedTimestamp(QString remote_changed_timestamp) { this->remote_changed_timestamp = remote_changed_timestamp; }
void setLocalChangedTimestamp(qint64 timestamp) { local_changed_timestamp = timestamp; }
auto getRemoteChangedTimestamp() -> QString { return m_remote_changed_timestamp; }
void setRemoteChangedTimestamp(QString remote_changed_timestamp) { m_remote_changed_timestamp = remote_changed_timestamp; }
void setLocalChangedTimestamp(qint64 timestamp) { m_local_changed_timestamp = timestamp; }
auto getETag() -> QString { return etag; }
void setETag(QString etag) { this->etag = etag; }
auto getETag() -> QString { return m_etag; }
void setETag(QString etag) { m_etag = etag; }
auto getMD5Sum() -> QString { return md5sum; }
void setMD5Sum(QString md5sum) { this->md5sum = md5sum; }
auto getMD5Sum() -> QString { return m_md5sum; }
void setMD5Sum(QString md5sum) { m_md5sum = md5sum; }
/* Whether the entry expires after some time (false) or not (true). */
void makeEternal(bool eternal) { is_eternal = eternal; }
[[nodiscard]] bool isEternal() const { return is_eternal; }
void makeEternal(bool eternal) { m_is_eternal = eternal; }
[[nodiscard]] bool isEternal() const { return m_is_eternal; }
auto getCurrentAge() -> qint64 { return current_age; }
void setCurrentAge(qint64 age) { current_age = age; }
auto getCurrentAge() -> qint64 { return m_current_age; }
void setCurrentAge(qint64 age) { m_current_age = age; }
auto getMaximumAge() -> qint64 { return max_age; }
void setMaximumAge(qint64 age) { max_age = age; }
auto getMaximumAge() -> qint64 { return m_max_age; }
void setMaximumAge(qint64 age) { m_max_age = age; }
bool isExpired(qint64 offset) { return !is_eternal && (current_age >= max_age - offset); };
bool isExpired(qint64 offset) { return !m_is_eternal && (m_current_age >= m_max_age - offset); };
protected:
QString baseId;
QString basePath;
QString relativePath;
QString md5sum;
QString etag;
QString m_baseId;
QString m_basePath;
QString m_relativePath;
QString m_md5sum;
QString m_etag;
qint64 local_changed_timestamp = 0;
QString remote_changed_timestamp; // QString for now, RFC 2822 encoded time
qint64 current_age = 0;
qint64 max_age = 0;
bool is_eternal = false;
qint64 m_local_changed_timestamp = 0;
QString m_remote_changed_timestamp; // QString for now, RFC 2822 encoded time
qint64 m_current_age = 0;
qint64 m_max_age = 0;
bool m_is_eternal = false;
bool stale = true;
bool m_stale = true;
};
using MetaEntryPtr = std::shared_ptr<MetaEntry>;

View File

@ -0,0 +1,25 @@
// SPDX-FileCopyrightText: 2022 Sefa Eyeoglu <contact@scrumplex.net>
//
// SPDX-License-Identifier: GPL-3.0-only
#include <QRegularExpression>
#include "IPathMatcher.h"
class SimplePrefixMatcher : public IPathMatcher {
public:
virtual ~SimplePrefixMatcher(){};
SimplePrefixMatcher(const QString& prefix)
{
m_prefix = prefix;
m_isPrefix = prefix.endsWith('/');
}
virtual bool matches(const QString& string) const override
{
if (m_isPrefix)
return string.startsWith(m_prefix);
return string == m_prefix;
}
QString m_prefix;
bool m_isPrefix = false;
};

View File

@ -38,5 +38,7 @@
<file>scalable/tag.svg</file>
<file>scalable/export.svg</file>
<file>scalable/rename.svg</file>
<file>scalable/launch.svg</file>
<file>scalable/shortcut.svg</file>
</qresource>
</RCC>

View File

@ -0,0 +1,33 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
version="1.1"
id="Calque_1"
x="0px"
y="0px"
viewBox="0 0 32.099998 32"
enable-background="new 0 0 32 32"
xml:space="preserve"
width="32.099998"
height="32"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><defs
id="defs45" />
<rect
fill-rule="evenodd"
clip-rule="evenodd"
fill="none"
width="32"
height="32"
id="rect1776"
x="0.1"
y="0" /><path
fill="#b6b5b6"
d="M 30,28.4 H 2 c -1.1,0 -2,-0.9 -2,-2 v -18 c 0,-1.1 0.9,-2 2,-2 h 28 c 1.1,0 2,0.9 2,2 v 18 c 0,1.1 -0.9,2 -2,2 z"
id="path1778" /><path
fill="#fbfbfb"
d="M 30,27.4 H 2 c -1.1,0 -2,-0.9 -2,-2 v -17 c 0,-1.1 0.9,-2 2,-2 h 28 c 1.1,0 2,0.9 2,2 v 17 c 0,1.1 -0.9,2 -2,2 z"
id="path1780" /><path
id="path11889"
style="color:#000000;fill:#585858;fill-opacity:1;stroke-width:0.999994;stroke-linecap:round;stroke-linejoin:round;paint-order:markers stroke fill"
d="m 11.520289,8.4637512 c -0.06474,-0.00183 -0.129661,-0.00125 -0.194234,0.00117 C 9.7763279,8.5230552 8.3049,9.7900155 8.3049,11.461507 v 10.877094 c -6e-7,2.228655 2.616628,3.738089 4.545774,2.622156 l 9.398109,-5.436205 c 1.928423,-1.115516 1.928423,-4.133479 0,-5.248993 L 12.850674,8.839348 C 12.428673,8.595238 11.973493,8.4765655 11.520289,8.4637512 Z m -0.184873,1.2309276 c 0.307885,1.456e-4 0.612521,0.080796 0.882242,0.2363567 a 0.89871379,0.89871379 0 0 0 0.0012,0 l 9.398109,5.4362075 c 1.143234,0.659415 1.143234,2.406201 0,3.065618 l -9.398109,5.436206 a 0.89871379,0.89871379 0 0 0 -0.0012,0 C 11.076744,24.528497 9.5659235,23.656375 9.56625,22.338599 V 11.461504 c 3.043e-4,-0.851902 0.622381,-1.5925143 1.461434,-1.7399133 0.102209,-0.018038 0.205103,-0.026958 0.307732,-0.026912 z" /></svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -37,17 +37,21 @@
</cc:Agent>
</dc:contributor>
<dc:source>https://github.com/PrismLauncher/PrismLauncher</dc:source>
<dc:rights>
<cc:Agent>
<dc:title>CC BY-SA 4.0</dc:title>
</cc:Agent>
</dc:rights>
<dc:publisher>
<cc:Agent>
<dc:title>Prism Launcher</dc:title>
</cc:Agent>
</dc:publisher>
<cc:license rdf:resource="http://creativecommons.org/licenses/by-sa/4.0/"/>
</cc:Work>
<cc:License rdf:about="http://creativecommons.org/licenses/by-sa/4.0/">
<cc:permits rdf:resource="http://creativecommons.org/ns#Reproduction"/>
<cc:permits rdf:resource="http://creativecommons.org/ns#Distribution"/>
<cc:requires rdf:resource="http://creativecommons.org/ns#Notice"/>
<cc:requires rdf:resource="http://creativecommons.org/ns#Attribution"/>
<cc:permits rdf:resource="http://creativecommons.org/ns#DerivativeWorks"/>
<cc:requires rdf:resource="http://creativecommons.org/ns#ShareAlike"/>
</cc:License>
</rdf:RDF>
</metadata>
</svg>

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

View File

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 24 24" enable-background="new 0 0 24 24" xml:space="preserve">
<rect fill="none" width="24" height="24"/>
<g id="_x35__1_">
<g>
<path fill="#585858" d="M9.5,9.5C9.8,9.5,10,9.2,10,9l0-2.4l7.6,7.3c0.2,0.2,0.5,0.2,0.7,0c0.2-0.2,0.2-0.5,0-0.7L10.8,6L13,6
c0.3,0,0.5-0.2,0.5-0.5S13.3,5,13,5H9.5C9.2,5,9,5.2,9,5.5V9C9,9.2,9.2,9.5,9.5,9.5z M21,5h-5.5v1H21c0.5,0,1,0.5,1,1l0,10
c0,0.5-0.4,1-1,1l-10,0c-0.5,0-1-0.5-1-1v-5.5H9V17c0,1.1,1.1,2,2.2,2H21c1.1,0,2-0.9,2-2V7.2C23,6.1,22.1,5,21,5z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 886 B

View File

@ -1,8 +1,17 @@
<!DOCTYPE RCC>
<RCC version="1.0">
<qresource prefix="/backgrounds">
<file alias="kitteh">catbgrnd2.png</file>
<file alias="catmas">catmas.png</file>
<file alias="cattiversary">cattiversary.png</file>
<file alias="kitteh">kitteh.png</file>
<file alias="kitteh-xmas">kitteh-xmas.png</file>
<file alias="kitteh-bday">kitteh-bday.png</file>
<file alias="kitteh-spooky">kitteh-spooky.png</file>
<file alias="rory">rory.png</file>
<file alias="rory-xmas">rory-xmas.png</file>
<file alias="rory-bday">rory-bday.png</file>
<file alias="rory-spooky">rory-spooky.png</file>
<file alias="rory-flat">rory-flat.png</file>
<file alias="rory-flat-xmas">rory-flat-xmas.png</file>
<file alias="rory-flat-bday">rory-flat-bday.png</file>
<file alias="rory-flat-spooky">rory-flat-spooky.png</file>
</qresource>
</RCC>

View File

Before

Width:  |  Height:  |  Size: 97 KiB

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

View File

Before

Width:  |  Height:  |  Size: 71 KiB

After

Width:  |  Height:  |  Size: 71 KiB

View File

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

Some files were not shown because too many files have changed in this diff Show More