diff options
46 files changed, 1146 insertions, 519 deletions
diff --git a/.github/workflows/update-flake.yml b/.github/workflows/update-flake.yml index c790c9c6..ad22120e 100644 --- a/.github/workflows/update-flake.yml +++ b/.github/workflows/update-flake.yml @@ -7,10 +7,12 @@ on: workflow_dispatch: permissions: + contents: write pull-requests: write jobs: update-flake: + if: github.repository == 'PrismLauncher/PrismLauncher' runs-on: ubuntu-latest steps: diff --git a/launcher/Application.cpp b/launcher/Application.cpp index 7858d713..5aa9efc4 100644 --- a/launcher/Application.cpp +++ b/launcher/Application.cpp @@ -568,6 +568,7 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv) // Language m_settings->registerSetting("Language", QString()); + m_settings->registerSetting("UseSystemLocale", false); // Console m_settings->registerSetting("ShowConsole", false); @@ -918,12 +919,7 @@ bool Application::createSetupWizard() } return false; }(); - bool languageRequired = [&]() - { - if (settings()->get("Language").toString().isEmpty()) - return true; - return false; - }(); + bool languageRequired = settings()->get("Language").toString().isEmpty(); bool pasteInterventionRequired = settings()->get("PastebinURL") != ""; bool themeInterventionRequired = settings()->get("ApplicationTheme") == ""; bool wizardRequired = javaRequired || languageRequired || pasteInterventionRequired || themeInterventionRequired; diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index 312288a1..af3bc28e 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -514,6 +514,8 @@ set(FLAME_SOURCES modplatform/flame/FlameCheckUpdate.h modplatform/flame/FlameInstanceCreationTask.h modplatform/flame/FlameInstanceCreationTask.cpp + modplatform/flame/FlamePackExportTask.h + modplatform/flame/FlamePackExportTask.cpp ) set(MODRINTH_SOURCES @@ -907,8 +909,8 @@ SET(LAUNCHER_SOURCES ui/dialogs/EditAccountDialog.h ui/dialogs/ExportInstanceDialog.cpp ui/dialogs/ExportInstanceDialog.h - ui/dialogs/ExportMrPackDialog.cpp - ui/dialogs/ExportMrPackDialog.h + ui/dialogs/ExportPackDialog.cpp + ui/dialogs/ExportPackDialog.h ui/dialogs/IconPickerDialog.cpp ui/dialogs/IconPickerDialog.h ui/dialogs/ImportResourceDialog.cpp @@ -1055,7 +1057,7 @@ qt_wrap_ui(LAUNCHER_UI ui/dialogs/ProfileSelectDialog.ui ui/dialogs/SkinUploadDialog.ui ui/dialogs/ExportInstanceDialog.ui - ui/dialogs/ExportMrPackDialog.ui + ui/dialogs/ExportPackDialog.ui ui/dialogs/IconPickerDialog.ui ui/dialogs/ImportResourceDialog.ui ui/dialogs/MSALoginDialog.ui diff --git a/launcher/FileSystem.cpp b/launcher/FileSystem.cpp index 812d45eb..4538702f 100644 --- a/launcher/FileSystem.cpp +++ b/launcher/FileSystem.cpp @@ -778,9 +778,43 @@ bool createShortcut(QString destination, QString target, QStringList args, QStri destination = PathCombine(getDesktopDir(), RemoveInvalidFilenameChars(name)); } #if defined(Q_OS_MACOS) - destination += ".command"; + // Create the Application + QDir applicationDirectory = QStandardPaths::writableLocation(QStandardPaths::ApplicationsLocation) + "/" + BuildConfig.LAUNCHER_NAME + " Instances/"; - QFile f(destination); + if (!applicationDirectory.mkpath(".")) { + qWarning() << "Couldn't create application directory"; + return false; + } + + QDir application = applicationDirectory.path() + "/" + name + ".app/"; + + if (application.exists()) { + qWarning() << "Application already exists!"; + return false; + } + + if (!application.mkpath(".")) { + qWarning() << "Couldn't create application"; + return false; + } + + QDir content = application.path() + "/Contents/"; + QDir resources = content.path() + "/Resources/"; + QDir binaryDir = content.path() + "/MacOS/"; + QFile info = content.path() + "/Info.plist"; + + if (!(content.mkpath(".") && resources.mkpath(".") && binaryDir.mkpath("."))) { + qWarning() << "Couldn't create directories within application"; + return false; + } + info.open(QIODevice::WriteOnly | QIODevice::Text); + + QFile(icon).rename(resources.path() + "/Icon.icns"); + + // Create the Command file + QString exec = binaryDir.path() + "/Run.command"; + + QFile f(exec); f.open(QIODevice::WriteOnly | QIODevice::Text); QTextStream stream(&f); @@ -797,6 +831,28 @@ bool createShortcut(QString destination, QString target, QStringList args, QStri f.setPermissions(f.permissions() | QFileDevice::ExeOwner | QFileDevice::ExeGroup | QFileDevice::ExeOther); + // Generate the Info.plist + QTextStream infoStream(&info); + infoStream << "<?xml version=\"1.0\" encoding=\"UTF-8\"?> \n" + "<!DOCTYPE plist PUBLIC \"-//Apple Computer//DTD PLIST 1.0//EN\" " + "\"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">" + "<plist version=\"1.0\">\n" + "<dict>\n" + " <key>CFBundleExecutable</key>\n" + " <string>Run.command</string>\n" // The path to the executable + " <key>CFBundleIconFile</key>\n" + " <string>Icon.icns</string>\n" + " <key>CFBundleName</key>\n" + " <string>" << name << "</string>\n" // Name of the application + " <key>CFBundlePackageType</key>\n" + " <string>APPL</string>\n" + " <key>CFBundleShortVersionString</key>\n" + " <string>1.0</string>\n" + " <key>CFBundleVersion</key>\n" + " <string>1.0</string>\n" + "</dict>\n" + "</plist>"; + return true; #elif defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) || defined(Q_OS_OPENBSD) if (!destination.endsWith(".desktop")) // in case of isFlatpak destination is already populated diff --git a/launcher/minecraft/MinecraftInstance.cpp b/launcher/minecraft/MinecraftInstance.cpp index aab930de..4867cc7a 100644 --- a/launcher/minecraft/MinecraftInstance.cpp +++ b/launcher/minecraft/MinecraftInstance.cpp @@ -1112,36 +1112,27 @@ JavaVersion MinecraftInstance::getJavaVersion() std::shared_ptr<ModFolderModel> MinecraftInstance::loaderModList() { - if (!m_loader_mod_list) - { + if (!m_loader_mod_list) { bool is_indexed = !APPLICATION->settings()->get("ModMetadataDisabled").toBool(); m_loader_mod_list.reset(new ModFolderModel(modsRoot(), this, is_indexed)); - m_loader_mod_list->disableInteraction(isRunning()); - connect(this, &BaseInstance::runningStatusChanged, m_loader_mod_list.get(), &ModFolderModel::disableInteraction); } return m_loader_mod_list; } std::shared_ptr<ModFolderModel> MinecraftInstance::coreModList() { - if (!m_core_mod_list) - { + if (!m_core_mod_list) { bool is_indexed = !APPLICATION->settings()->get("ModMetadataDisabled").toBool(); m_core_mod_list.reset(new ModFolderModel(coreModsDir(), this, is_indexed)); - m_core_mod_list->disableInteraction(isRunning()); - connect(this, &BaseInstance::runningStatusChanged, m_core_mod_list.get(), &ModFolderModel::disableInteraction); } return m_core_mod_list; } std::shared_ptr<ModFolderModel> MinecraftInstance::nilModList() { - if (!m_nil_mod_list) - { + if (!m_nil_mod_list) { bool is_indexed = !APPLICATION->settings()->get("ModMetadataDisabled").toBool(); m_nil_mod_list.reset(new ModFolderModel(nilModsDir(), this, is_indexed, false)); - m_nil_mod_list->disableInteraction(isRunning()); - connect(this, &BaseInstance::runningStatusChanged, m_nil_mod_list.get(), &ModFolderModel::disableInteraction); } return m_nil_mod_list; } diff --git a/launcher/minecraft/PackProfile.cpp b/launcher/minecraft/PackProfile.cpp index aff05dbc..e8fd2157 100644 --- a/launcher/minecraft/PackProfile.cpp +++ b/launcher/minecraft/PackProfile.cpp @@ -65,7 +65,8 @@ static const QMap<QString, ResourceAPI::ModLoaderType> modloaderMapping{ {"net.minecraftforge", ResourceAPI::Forge}, {"net.fabricmc.fabric-loader", ResourceAPI::Fabric}, - {"org.quiltmc.quilt-loader", ResourceAPI::Quilt} + {"org.quiltmc.quilt-loader", ResourceAPI::Quilt}, + {"com.mumfrey.liteloader", ResourceAPI::LiteLoader} }; PackProfile::PackProfile(MinecraftInstance * instance) diff --git a/launcher/minecraft/mod/Mod.cpp b/launcher/minecraft/mod/Mod.cpp index fa1a0253..880dacb1 100644 --- a/launcher/minecraft/mod/Mod.cpp +++ b/launcher/minecraft/mod/Mod.cpp @@ -126,7 +126,7 @@ bool Mod::applyFilter(QRegularExpression filter) const return Resource::applyFilter(filter); } -auto Mod::destroy(QDir& index_dir, bool preserve_metadata) -> bool +auto Mod::destroy(QDir& index_dir, bool preserve_metadata, bool attempt_trash) -> bool { if (!preserve_metadata) { qDebug() << QString("Destroying metadata for '%1' on purpose").arg(name()); @@ -139,7 +139,7 @@ auto Mod::destroy(QDir& index_dir, bool preserve_metadata) -> bool } } - return Resource::destroy(); + return Resource::destroy(attempt_trash); } auto Mod::details() const -> const ModDetails& diff --git a/launcher/minecraft/mod/Mod.h b/launcher/minecraft/mod/Mod.h index d6272f4d..b67bd465 100644 --- a/launcher/minecraft/mod/Mod.h +++ b/launcher/minecraft/mod/Mod.h @@ -93,7 +93,7 @@ public: [[nodiscard]] bool applyFilter(QRegularExpression filter) const override; // Delete all the files of this mod - auto destroy(QDir& index_dir, bool preserve_metadata = false) -> bool; + auto destroy(QDir& index_dir, bool preserve_metadata = false, bool attempt_trash = true) -> bool; void finishResolvingWithDetails(ModDetails&& details); diff --git a/launcher/minecraft/mod/ModFolderModel.cpp b/launcher/minecraft/mod/ModFolderModel.cpp index af98d834..51383edf 100644 --- a/launcher/minecraft/mod/ModFolderModel.cpp +++ b/launcher/minecraft/mod/ModFolderModel.cpp @@ -199,10 +199,10 @@ Task* ModFolderModel::createParseTask(Resource& resource) bool ModFolderModel::uninstallMod(const QString& filename, bool preserve_metadata) { - for(auto mod : allMods()){ - if(mod->fileinfo().fileName() == filename){ + for(auto mod : allMods()) { + if(mod->fileinfo().fileName() == filename) { auto index_dir = indexDir(); - mod->destroy(index_dir, preserve_metadata); + mod->destroy(index_dir, preserve_metadata, false); update(); @@ -215,16 +215,11 @@ bool ModFolderModel::uninstallMod(const QString& filename, bool preserve_metadat bool ModFolderModel::deleteMods(const QModelIndexList& indexes) { - if(!m_can_interact) { - return false; - } - - if(indexes.isEmpty()) + if (indexes.isEmpty()) return true; - for (auto i: indexes) - { - if(i.column() != 0) { + for (auto i : indexes) { + if (i.column() != 0) { continue; } auto m = at(i.row()); diff --git a/launcher/minecraft/mod/Resource.cpp b/launcher/minecraft/mod/Resource.cpp index e5077260..098a617f 100644 --- a/launcher/minecraft/mod/Resource.cpp +++ b/launcher/minecraft/mod/Resource.cpp @@ -148,14 +148,10 @@ bool Resource::enable(EnableAction action) return true; } -bool Resource::destroy() +bool Resource::destroy(bool attemptTrash) { m_type = ResourceType::UNKNOWN; - - if (FS::trash(m_file_info.filePath())) - return true; - - return FS::deletePath(m_file_info.filePath()); + return (attemptTrash && FS::trash(m_file_info.filePath())) || FS::deletePath(m_file_info.filePath()); } bool Resource::isSymLinkUnder(const QString& instPath) const diff --git a/launcher/minecraft/mod/Resource.h b/launcher/minecraft/mod/Resource.h index a5e9ae91..94f3160c 100644 --- a/launcher/minecraft/mod/Resource.h +++ b/launcher/minecraft/mod/Resource.h @@ -92,7 +92,7 @@ class Resource : public QObject { } // Delete all files of this resource. - bool destroy(); + bool destroy(bool attemptTrash = true); [[nodiscard]] auto isSymLink() const -> bool { return m_file_info.isSymLink(); } diff --git a/launcher/minecraft/mod/ResourceFolderModel.cpp b/launcher/minecraft/mod/ResourceFolderModel.cpp index 7700fd36..39a61067 100644 --- a/launcher/minecraft/mod/ResourceFolderModel.cpp +++ b/launcher/minecraft/mod/ResourceFolderModel.cpp @@ -1,14 +1,15 @@ #include "ResourceFolderModel.h" +#include <QMessageBox> #include <QCoreApplication> #include <QDebug> #include <QFileInfo> #include <QIcon> +#include <QMenu> #include <QMimeData> #include <QStyle> #include <QThreadPool> #include <QUrl> -#include <QMenu> #include "Application.h" #include "FileSystem.h" @@ -18,6 +19,7 @@ #include "settings/Setting.h" #include "tasks/Task.h" +#include "ui/dialogs/CustomMessageBox.h" ResourceFolderModel::ResourceFolderModel(QDir dir, BaseInstance* instance, QObject* parent, bool create_dir) : QAbstractListModel(parent), m_dir(dir), m_instance(instance), m_watcher(this) @@ -77,10 +79,6 @@ bool ResourceFolderModel::stopWatching(const QStringList paths) bool ResourceFolderModel::installResource(QString original_path) { - if (!m_can_interact) { - return false; - } - // NOTE: fix for GH-1178: remove trailing slash to avoid issues with using the empty result of QFileInfo::fileName original_path = FS::NormalizePath(original_path); QFileInfo file_info(original_path); @@ -159,7 +157,7 @@ bool ResourceFolderModel::uninstallResource(QString file_name) { for (auto& resource : m_resources) { if (resource->fileinfo().fileName() == file_name) { - auto res = resource->destroy(); + auto res = resource->destroy(false); update(); @@ -171,9 +169,6 @@ bool ResourceFolderModel::uninstallResource(QString file_name) bool ResourceFolderModel::deleteResources(const QModelIndexList& indexes) { - if (!m_can_interact) - return false; - if (indexes.isEmpty()) return true; @@ -192,11 +187,8 @@ bool ResourceFolderModel::deleteResources(const QModelIndexList& indexes) return true; } -bool ResourceFolderModel::setResourceEnabled(const QModelIndexList &indexes, EnableAction action) +bool ResourceFolderModel::setResourceEnabled(const QModelIndexList& indexes, EnableAction action) { - if (!m_can_interact) - return false; - if (indexes.isEmpty()) return true; @@ -249,15 +241,18 @@ bool ResourceFolderModel::update() connect(m_current_update_task.get(), &Task::succeeded, this, &ResourceFolderModel::onUpdateSucceeded, Qt::ConnectionType::QueuedConnection); connect(m_current_update_task.get(), &Task::failed, this, &ResourceFolderModel::onUpdateFailed, Qt::ConnectionType::QueuedConnection); - connect(m_current_update_task.get(), &Task::finished, this, [=] { - m_current_update_task.reset(); - if (m_scheduled_update) { - m_scheduled_update = false; - update(); - } else { - emit updateFinished(); - } - }, Qt::ConnectionType::QueuedConnection); + connect( + m_current_update_task.get(), &Task::finished, this, + [=] { + m_current_update_task.reset(); + if (m_scheduled_update) { + m_scheduled_update = false; + update(); + } else { + emit updateFinished(); + } + }, + Qt::ConnectionType::QueuedConnection); QThreadPool::globalInstance()->start(m_current_update_task.get()); @@ -347,15 +342,9 @@ Qt::DropActions ResourceFolderModel::supportedDropActions() const Qt::ItemFlags ResourceFolderModel::flags(const QModelIndex& index) const { Qt::ItemFlags defaultFlags = QAbstractListModel::flags(index); - auto flags = defaultFlags; - if (!m_can_interact) { - flags &= ~Qt::ItemIsDropEnabled; - } else { - flags |= Qt::ItemIsDropEnabled; - if (index.isValid()) { - flags |= Qt::ItemIsUserCheckable; - } - } + auto flags = defaultFlags | Qt::ItemIsDropEnabled; + if (index.isValid()) + flags |= Qt::ItemIsUserCheckable; return flags; } @@ -428,16 +417,17 @@ QVariant ResourceFolderModel::data(const QModelIndex& index, int role) const if (column == NAME_COLUMN) { if (at(row).isSymLinkUnder(instDirPath())) { return m_resources[row]->internal_id() + - tr("\nWarning: This resource is symbolically linked from elsewhere. Editing it will also change the original." - "\nCanonical Path: %1") - .arg(at(row).fileinfo().canonicalFilePath());; + tr("\nWarning: This resource is symbolically linked from elsewhere. Editing it will also change the original." + "\nCanonical Path: %1") + .arg(at(row).fileinfo().canonicalFilePath()); + ; } if (at(row).isMoreThanOneHardLink()) { return m_resources[row]->internal_id() + - tr("\nWarning: This resource is hard linked elsewhere. Editing it will also change the original."); + tr("\nWarning: This resource is hard linked elsewhere. Editing it will also change the original."); } } - + return m_resources[row]->internal_id(); case Qt::DecorationRole: { if (column == NAME_COLUMN && (at(row).isSymLinkUnder(instDirPath()) || at(row).isMoreThanOneHardLink())) @@ -463,8 +453,20 @@ bool ResourceFolderModel::setData(const QModelIndex& index, const QVariant& valu if (row < 0 || row >= rowCount(index.parent()) || !index.isValid()) return false; - if (role == Qt::CheckStateRole) + if (role == Qt::CheckStateRole) { + if (m_instance != nullptr && m_instance->isRunning()) { + auto response = + CustomMessageBox::selectable(nullptr, "Confirm toggle", + "If you enable/disable this resource while the game is running it may crash your game.\n" + "Are you sure you want to do this?", + QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No, QMessageBox::No) + ->exec(); + + if (response != QMessageBox::Yes) + return false; + } return setResourceEnabled({ index }, EnableAction::TOGGLE); + } return false; } @@ -583,16 +585,6 @@ SortType ResourceFolderModel::columnToSortKey(size_t column) const return m_column_sort_keys.at(column); } -void ResourceFolderModel::enableInteraction(bool enabled) -{ - if (m_can_interact == enabled) - return; - - m_can_interact = enabled; - if (size()) - emit dataChanged(index(0), index(size() - 1)); -} - /* Standard Proxy Model for createFilterProxyModel */ [[nodiscard]] bool ResourceFolderModel::ProxyModel::filterAcceptsRow(int source_row, const QModelIndex& source_parent) const { @@ -628,6 +620,7 @@ void ResourceFolderModel::enableInteraction(bool enabled) return (compare_result.first > 0); } -QString ResourceFolderModel::instDirPath() const { +QString ResourceFolderModel::instDirPath() const +{ return QFileInfo(m_instance->instanceRoot()).absoluteFilePath(); } diff --git a/launcher/minecraft/mod/ResourceFolderModel.h b/launcher/minecraft/mod/ResourceFolderModel.h index eb1d7c4f..454b84c3 100644 --- a/launcher/minecraft/mod/ResourceFolderModel.h +++ b/launcher/minecraft/mod/ResourceFolderModel.h @@ -14,8 +14,8 @@ #include "BaseInstance.h" -#include "tasks/Task.h" #include "tasks/ConcurrentTask.h" +#include "tasks/Task.h" class QSortFilterProxyModel; @@ -141,10 +141,6 @@ class ResourceFolderModel : public QAbstractListModel { QString instDirPath() const; - public slots: - void enableInteraction(bool enabled); - void disableInteraction(bool disabled) { enableInteraction(!disabled); } - signals: void updateFinished(); @@ -193,7 +189,11 @@ 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) { Q_UNUSED(ticket); Q_UNUSED(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. @@ -203,8 +203,6 @@ class ResourceFolderModel : public QAbstractListModel { QStringList m_column_names_translated = {tr("Enable"), tr("Name"), tr("Last Modified")}; QList<QHeaderView::ResizeMode> m_column_resize_modes = { QHeaderView::ResizeToContents, QHeaderView::Stretch, QHeaderView::ResizeToContents }; - bool m_can_interact = true; - QDir m_dir; BaseInstance* m_instance; QFileSystemWatcher m_watcher; diff --git a/launcher/minecraft/mod/tasks/ModFolderLoadTask.cpp b/launcher/minecraft/mod/tasks/ModFolderLoadTask.cpp index 3677a1dc..ef353c70 100644 --- a/launcher/minecraft/mod/tasks/ModFolderLoadTask.cpp +++ b/ |
