diff options
Diffstat (limited to 'launcher/ui')
44 files changed, 1841 insertions, 275 deletions
diff --git a/launcher/ui/InstanceWindow.cpp b/launcher/ui/InstanceWindow.cpp index 09ce0d67..c62b370f 100644 --- a/launcher/ui/InstanceWindow.cpp +++ b/launcher/ui/InstanceWindow.cpp @@ -132,6 +132,12 @@ InstanceWindow::InstanceWindow(InstancePtr instance, QWidget *parent) { connect(m_instance.get(), &BaseInstance::statusChanged, this, &InstanceWindow::on_instanceStatusChanged); } + + // add ourself as the modpack page's instance window + { + static_cast<ManagedPackPage*>(m_container->getPage("managed_pack"))->setInstanceWindow(this); + } + show(); } diff --git a/launcher/ui/MainWindow.cpp b/launcher/ui/MainWindow.cpp index 0595634f..6eaaac97 100644 --- a/launcher/ui/MainWindow.cpp +++ b/launcher/ui/MainWindow.cpp @@ -39,6 +39,7 @@ #include "Application.h" #include "BuildConfig.h" +#include "FileSystem.h" #include "MainWindow.h" @@ -71,6 +72,7 @@ #include <BaseInstance.h> #include <InstanceList.h> +#include <minecraft/MinecraftInstance.h> #include <MMCZip.h> #include <icons/IconList.h> #include <java/JavaUtils.h> @@ -103,8 +105,14 @@ #include "ui/dialogs/CopyInstanceDialog.h" #include "ui/dialogs/EditAccountDialog.h" #include "ui/dialogs/ExportInstanceDialog.h" +#include "ui/dialogs/ImportResourcePackDialog.h" #include "ui/themes/ITheme.h" +#include <minecraft/mod/ResourcePackFolderModel.h> +#include <minecraft/mod/tasks/LocalResourcePackParseTask.h> +#include <minecraft/mod/TexturePackFolderModel.h> +#include <minecraft/mod/tasks/LocalTexturePackParseTask.h> + #include "KonamiCode.h" #include "InstanceImportTask.h" @@ -235,6 +243,7 @@ public: TranslatedAction actionLaunchInstanceOffline; TranslatedAction actionLaunchInstanceDemo; TranslatedAction actionExportInstance; + TranslatedAction actionCreateInstanceShortcut; QVector<TranslatedAction *> all_actions; LabeledToolButton *renameButton = nullptr; @@ -535,8 +544,9 @@ public: fileMenu->addAction(actionChangeInstGroup); fileMenu->addAction(actionViewSelectedInstFolder); fileMenu->addAction(actionExportInstance); - fileMenu->addAction(actionDeleteInstance); fileMenu->addAction(actionCopyInstance); + fileMenu->addAction(actionDeleteInstance); + fileMenu->addAction(actionCreateInstanceShortcut); fileMenu->addSeparator(); fileMenu->addAction(actionSettings); @@ -622,6 +632,7 @@ public: actionExportInstance->setEnabled(enabled); actionDeleteInstance->setEnabled(enabled); actionCopyInstance->setEnabled(enabled); + actionCreateInstanceShortcut->setEnabled(enabled); } void createStatusBar(QMainWindow *MainWindow) @@ -760,6 +771,13 @@ public: actionCopyInstance->setIcon(APPLICATION->getThemedIcon("copy")); all_actions.append(&actionCopyInstance); + actionCreateInstanceShortcut = TranslatedAction(MainWindow); + actionCreateInstanceShortcut->setObjectName(QStringLiteral("actionCreateInstanceShortcut")); + actionCreateInstanceShortcut.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Create Shortcut")); + actionCreateInstanceShortcut.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Creates a shortcut on your desktop to launch the selected instance.")); + actionCreateInstanceShortcut->setIcon(APPLICATION->getThemedIcon("shortcut")); + all_actions.append(&actionCreateInstanceShortcut); + setInstanceActionsEnabled(false); } @@ -798,6 +816,8 @@ public: instanceToolBar->addAction(actionCopyInstance); instanceToolBar->addAction(actionDeleteInstance); + instanceToolBar->addAction(actionCreateInstanceShortcut); // TODO find better position for this + QLayout * lay = instanceToolBar->layout(); for(int i = 0; i < lay->count(); i++) { @@ -1586,7 +1606,7 @@ InstanceView background-image: url(:/backgrounds/%1); background-attachment: fixed; background-clip: padding; - background-position: bottom left; + background-position: bottom right; background-repeat: none; background-color:palette(base); })") @@ -1717,17 +1737,41 @@ void MainWindow::on_actionAddInstance_triggered() void MainWindow::droppedURLs(QList<QUrl> urls) { - for(auto & url:urls) - { - if(url.isLocalFile()) - { - addInstance(url.toLocalFile()); - } - else - { + // NOTE: This loop only processes one dropped file! + for (auto& url : urls) { + // The isLocalFile() check below doesn't work as intended without an explicit scheme. + if (url.scheme().isEmpty()) + url.setScheme("file"); + + if (!url.isLocalFile()) { // probably instance/modpack addInstance(url.toString()); + break; + } + + auto localFileName = url.toLocalFile(); + QFileInfo localFileInfo(localFileName); + + bool isResourcePack = ResourcePackUtils::validate(localFileInfo); + bool isTexturePack = TexturePackUtils::validate(localFileInfo); + + if (!isResourcePack && !isTexturePack) { // probably instance/modpack + addInstance(localFileName); + break; } - // Only process one dropped file... + + ImportResourcePackDialog dlg(this); + + if (dlg.exec() != QDialog::Accepted) + break; + + qDebug() << "Adding resource/texture pack" << localFileName << "to" << dlg.selectedInstanceKey; + + auto inst = APPLICATION->instances()->getInstanceById(dlg.selectedInstanceKey); + auto minecraftInst = std::dynamic_pointer_cast<MinecraftInstance>(inst); + if (isResourcePack) + minecraftInst->resourcePackList()->installResource(localFileName); + else if (isTexturePack) + minecraftInst->texturePackList()->installResource(localFileName); break; } } @@ -1965,27 +2009,25 @@ void MainWindow::on_actionAbout_triggered() void MainWindow::on_actionDeleteInstance_triggered() { - if (!m_selectedInstance) - { + if (!m_selectedInstance) { return; } auto id = m_selectedInstance->id(); - if (APPLICATION->instances()->trashInstance(id)) { - ui->actionUndoTrashInstance->setEnabled(APPLICATION->instances()->trashedSomething()); - return; - } - - auto response = CustomMessageBox::selectable( - this, - tr("CAREFUL!"), - tr("About to delete: %1\nThis is permanent and will completely delete the instance.\n\nAre you sure?").arg(m_selectedInstance->name()), - QMessageBox::Warning, - QMessageBox::Yes | QMessageBox::No, - QMessageBox::No - )->exec(); - if (response == QMessageBox::Yes) - { + + auto response = + CustomMessageBox::selectable(this, tr("CAREFUL!"), + tr("About to delete: %1\nThis may be permanent and will completely delete the instance.\n\nAre you sure?") + .arg(m_selectedInstance->name()), + QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No, QMessageBox::No) + ->exec(); + + if (response == QMessageBox::Yes) { + if (APPLICATION->instances()->trashInstance(id)) { + ui->actionUndoTrashInstance->setEnabled(APPLICATION->instances()->trashedSomething()); + return; + } + APPLICATION->instances()->deleteInstance(id); } } @@ -2083,6 +2125,145 @@ void MainWindow::on_actionKillInstance_triggered() } } +void MainWindow::on_actionCreateInstanceShortcut_triggered() +{ + if (m_selectedInstance) + { + auto desktopPath = FS::getDesktopDir(); + if (desktopPath.isEmpty()) { + // TODO come up with an alternative solution (open "save file" dialog) + QMessageBox::critical(this, tr("Create instance shortcut"), tr("Couldn't find desktop?!")); + return; + } + +#if defined(Q_OS_MACOS) + QString appPath = QApplication::applicationFilePath(); + if (appPath.startsWith("/private/var/")) { + QMessageBox::critical(this, tr("Create instance shortcut"), tr("The launcher is in the folder it was extracted from, therefore it cannot create shortcuts.")); + return; + } + + if (FS::createShortcut(FS::PathCombine(desktopPath, m_selectedInstance->name()), + appPath, { "--launch", m_selectedInstance->id() }, + m_selectedInstance->name(), "")) { + QMessageBox::information(this, tr("Create instance shortcut"), tr("Created a shortcut to this instance on your desktop!")); + } + else + { + QMessageBox::critical(this, tr("Create instance shortcut"), tr("Failed to create instance shortcut!")); + } +#elif defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) || defined(Q_OS_OPENBSD) + QString appPath = QApplication::applicationFilePath(); + if (appPath.startsWith("/tmp/.mount_")) { + // AppImage! + appPath = QProcessEnvironment::systemEnvironment().value(QStringLiteral("APPIMAGE")); + if (appPath.isEmpty()) + { + QMessageBox::critical(this, tr("Create instance shortcut"), tr("Launcher is running as misconfigured AppImage? ($APPIMAGE environment variable is missing)")); + } + else if (appPath.endsWith("/")) + { + appPath.chop(1); + } + } + + auto icon = APPLICATION->icons()->icon(m_selectedInstance->iconKey()); + if (icon == nullptr) + { + icon = APPLICATION->icons()->icon("grass"); + } + + QString iconPath = FS::PathCombine(m_selectedInstance->instanceRoot(), "icon.png"); + + QFile iconFile(iconPath); + if (!iconFile.open(QFile::WriteOnly)) + { + QMessageBox::critical(this, tr("Create instance shortcut"), tr("Failed to create icon for shortcut.")); + return; + } + bool success = icon->icon().pixmap(64, 64).save(&iconFile, "PNG"); + iconFile.close(); + + if (!success) + { + iconFile.remove(); + QMessageBox::critical(this, tr("Create instance shortcut"), tr("Failed to create icon for shortcut.")); + return; + } + + QString desktopFilePath = FS::PathCombine(desktopPath, m_selectedInstance->name() + ".desktop"); + QStringList args; + if (DesktopServices::isFlatpak()) { + QFileDialog fileDialog; + // workaround to make sure the portal file dialog opens in the desktop directory + fileDialog.setDirectoryUrl(desktopPath); + desktopFilePath = fileDialog.getSaveFileName( + this, tr("Create Shortcut"), desktopFilePath, + tr("Desktop Entries (*.desktop)")); + if (desktopFilePath.isEmpty()) + return; // file dialog canceled by user + appPath = "flatpak"; + QString flatpakAppId = BuildConfig.LAUNCHER_DESKTOPFILENAME; + flatpakAppId.remove(".desktop"); + args.append({ "run", flatpakAppId }); + } + args.append({ "--launch", m_selectedInstance->id() }); + if (FS::createShortcut(desktopFilePath, appPath, args, m_selectedInstance->name(), iconPath)) { + QMessageBox::information(this, tr("Create instance shortcut"), tr("Created a shortcut to this instance on your desktop!")); + } + else + { + iconFile.remove(); + QMessageBox::critical(this, tr("Create instance shortcut"), tr("Failed to create instance shortcut!")); + } +#elif defined(Q_OS_WIN) + auto icon = APPLICATION->icons()->icon(m_selectedInstance->iconKey()); + if (icon == nullptr) + { + icon = APPLICATION->icons()->icon("grass"); + } + + QString iconPath = FS::PathCombine(m_selectedInstance->instanceRoot(), "icon.ico"); + + // part of fix for weird bug involving the window icon being replaced + // dunno why it happens, but this 2-line fix seems to be enough, so w/e + auto appIcon = APPLICATION->getThemedIcon("logo"); + + QFile iconFile(iconPath); + if (!iconFile.open(QFile::WriteOnly)) + { + QMessageBox::critical(this, tr("Create instance shortcut"), tr("Failed to create icon for shortcut.")); + return; + } + bool success = icon->icon().pixmap(64, 64).save(&iconFile, "ICO"); + iconFile.close(); + + // restore original window icon + QGuiApplication::setWindowIcon(appIcon); + + if (!success) + { + iconFile.remove(); + QMessageBox::critical(this, tr("Create instance shortcut"), tr("Failed to create icon for shortcut.")); + return; + } + + if (FS::createShortcut(FS::PathCombine(desktopPath, m_selectedInstance->name()), + QApplication::applicationFilePath(), { "--launch", m_selectedInstance->id() }, + m_selectedInstance->name(), iconPath)) { + QMessageBox::information(this, tr("Create instance shortcut"), tr("Created a shortcut to this instance on your desktop!")); + } + else + { + iconFile.remove(); + QMessageBox::critical(this, tr("Create instance shortcut"), tr("Failed to create instance shortcut!")); + } +#else + QMessageBox::critical(this, tr("Create instance shortcut"), tr("Not supported on your platform!")); +#endif + } +} + void MainWindow::taskEnd() { QObject *sender = QObject::sender(); diff --git a/launcher/ui/MainWindow.h b/launcher/ui/MainWindow.h index 53db4919..babc5e03 100644 --- a/launcher/ui/MainWindow.h +++ b/launcher/ui/MainWindow.h @@ -160,6 +160,8 @@ private slots: void on_actionEditInstance_triggered(); + void on_actionCreateInstanceShortcut_triggered(); + void taskEnd(); /** diff --git a/launcher/ui/dialogs/BlockedModsDialog.cpp b/launcher/ui/dialogs/BlockedModsDialog.cpp index eeeeb709..0480ced7 100644 --- a/launcher/ui/dialogs/BlockedModsDialog.cpp +++ b/launcher/ui/dialogs/BlockedModsDialog.cpp @@ -1,7 +1,33 @@ +// SPDX-FileCopyrightText: 2022 Sefa Eyeoglu <contact@scrumplex.net> +// SPDX-FileCopyrightText: 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> +// SPDX-FileCopyrightText: 2022 kumquat-ir <66188216+kumquat-ir@users.noreply.github.com> +// +// SPDX-License-Identifier: GPL-3.0-only + +/* + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> + * Copyright (C) 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> + * Copyright (C) 2022 kumquat-ir <66188216+kumquat-ir@users.noreply.github.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 "BlockedModsDialog.h" #include "ui_BlockedModsDialog.h" #include "Application.h" +#include "modplatform/helpers/HashUtils.h" #include <QDebug> #include <QDesktopServices> @@ -13,6 +39,7 @@ #include <QFileInfo> #include <QMimeData> #include <QPushButton> +#include <QMimeData> #include <QStandardPaths> BlockedModsDialog::BlockedModsDialog(QWidget* parent, const QString& title, const QString& text, QList<BlockedMod>& mods) @@ -23,8 +50,8 @@ BlockedModsDialog::BlockedModsDialog(QWidget* parent, const QString& title, cons ui->setupUi(this); - auto openAllButton = ui->buttonBox->addButton(tr("Open All"), QDialogButtonBox::ActionRole); - connect(openAllButton, &QPushButton::clicked, this, &BlockedModsDialog::openAll); + m_openMissingButton = ui->buttonBox->addButton(tr("Open Missing"), QDialogButtonBox::ActionRole); + connect(m_openMissingButton, &QPushButton::clicked, this, [this]() { openAll(true); }); auto downloadFolderButton = ui->buttonBox->addButton(tr("Add Download Folder"), QDialogButtonBox::ActionRole); connect(downloadFolderButton, &QPushButton::clicked, this, &BlockedModsDialog::addDownloadFolder); @@ -38,15 +65,8 @@ BlockedModsDialog::BlockedModsDialog(QWidget* parent, const QString& title, cons this->setWindowTitle(title); ui->labelDescription->setText(text); - ui->labelExplain->setText( - QString(tr("Your configured global mods folder and default downloads folder " - "are automatically checked for the downloaded mods and they will be copied to the instance if found.<br/>" - "Optionally, you may drag and drop the downloaded mods onto this dialog or add a folder to watch " - "if you did not download the mods to a default location.")) - .arg(APPLICATION->settings()->get("CentralModsDir").toString(), - QStandardPaths::writableLocation(QStandardPaths::DownloadLocation))); - - // force all URL handeling as external + + // force all URL handling as external connect(ui->textBrowserWatched, &QTextBrowser::anchorClicked, this, [](const QUrl url) { QDesktopServices::openUrl(url); }); setAcceptDrops(true); @@ -68,7 +88,15 @@ void BlockedModsDialog::dragEnterEvent(QDragEnterEvent* e) void BlockedModsDialog::dropEvent(QDropEvent* e) { - for (const QUrl& url : e->mimeData()->urls()) { + for (QUrl& url : e->mimeData()->urls()) { + if (url.scheme().isEmpty()) { // ensure isLocalFile() works correctly + url.setScheme("file"); + } + + if (!url.isLocalFile()) { // can't drop external files here. + continue; + } + QString filePath = url.toLocalFile(); qDebug() << "[Blocked Mods Dialog] Dropped file:" << filePath; addHashTask(filePath); @@ -83,10 +111,18 @@ void BlockedModsDialog::dropEvent(QDropEvent* e) update(); } -void BlockedModsDialog::openAll() +void BlockedModsDialog::done(int r) +{ + QDialog::done(r); + disconnect(&m_watcher, &QFileSystemWatcher::directoryChanged, this, &BlockedModsDialog::directoryChanged); +} + +void BlockedModsDialog::openAll(bool missingOnly) { for (auto& mod : m_mods) { - QDesktopServices::openUrl(mod.websiteUrl); + if (!missingOnly || !mod.matched) { + QDesktopServices::openUrl(mod.websiteUrl); + } } } @@ -129,8 +165,10 @@ void BlockedModsDialog::update() if (allModsMatched()) { ui->labelModsFound->setText("<span style=\"color:green\">✔</span>" + tr("All mods found")); + m_openMissingButton->setDisabled(true); } else { ui->labelModsFound->setText(tr("Please download the missing mods.")); + m_openMissingButton->setDisabled(false); } } @@ -241,14 +279,24 @@ void BlockedModsDialog::checkMatchHash(QString hash, QString path) /// @return boolean: did the path match the name of a blocked mod? bool BlockedModsDialog::checkValidPath(QString path) { - QFileInfo file = QFileInfo(path); - QString filename = file.fileName(); + const QFileInfo file = QFileInfo(path); + const QString filename = file.fileName(); + QString laxFilename(filename); + laxFilename.replace('+', ' '); + + auto compare = [](QString fsfilename, QString metadataFilename) { + return metadataFilename.compare(fsfilename, Qt::CaseInsensitive) == 0; + }; for (auto& mod : m_mods) { - if (mod.name.compare(filename, Qt::CaseInsensitive) == 0) { + if (compare(filename, mod.name)) { qDebug() << "[Blocked Mods Dialog] Name match found:" << mod.name << "| From path:" << path; return true; } + if (compare(laxFilename, mod.name)) { + qDebug() << "[Blocked Mods Dialog] Lax name match found:" << mod.name << "| From path:" << path; + return true; + } } return false; diff --git a/launcher/ui/dialogs/BlockedModsDialog.h b/launcher/ui/dialogs/BlockedModsDialog.h index dac43cba..014f488a 100644 --- a/launcher/ui/dialogs/BlockedModsDialog.h +++ b/launcher/ui/dialogs/BlockedModsDialog.h @@ -1,49 +1,80 @@ +// SPDX-FileCopyrightText: 2022 Sefa Eyeoglu <contact@scrumplex.net> +// SPDX-FileCopyrightText: 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> +// SPDX-FileCopyrightText: 2022 kumquat-ir <66188216+kumquat-ir@users.noreply.github.com> +// +// SPDX-License-Identifier: GPL-3.0-only + +/* + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> + * Copyright (C) 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> + * Copyright (C) 2022 kumquat-ir <66188216+kumquat-ir@users.noreply.github.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/>. + */ + #pragma once #include <QDialog> -#include <QString> #include <QList> +#include <QString> #include <QFileSystemWatcher> -#include "modplatform/helpers/HashUtils.h" - #include "tasks/ConcurrentTask.h" +class QPushButton; + struct BlockedMod { QString name; QString websiteUrl; QString hash; bool matched; QString localPath; - + QString targetFolder; }; QT_BEGIN_NAMESPACE -namespace Ui { class BlockedModsDialog; } +namespace Ui { +class BlockedModsDialog; +} QT_END_NAMESPACE class BlockedModsDialog : public QDialog { -Q_OBJECT + Q_OBJECT -public: - BlockedModsDialog(QWidget *parent, const QString &title, const QString &text, QList<BlockedMod> &mods); + public: + BlockedModsDialog(QWidget* parent, const QString& title, const QString& text, QList<BlockedMod>& mods); ~BlockedModsDialog() override; -protected: - void dragEnterEvent(QDragEnterEvent *event) override; - void dropEvent(QDropEvent *event) override; + protected: + void dragEnterEvent(QDragEnterEvent* event) override; + void dropEvent(QDropEvent* event) override; + + protected slots: + void done(in |
