diff options
Diffstat (limited to 'launcher/ui')
142 files changed, 7619 insertions, 5163 deletions
diff --git a/launcher/ui/GuiUtil.cpp b/launcher/ui/GuiUtil.cpp index 5a62e4d0..930e088a 100644 --- a/launcher/ui/GuiUtil.cpp +++ b/launcher/ui/GuiUtil.cpp @@ -1,8 +1,9 @@ // SPDX-License-Identifier: GPL-3.0-only /* - * PolyMC - Minecraft Launcher + * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Lenny McLennington <lenny@sneed.church> * 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 @@ -39,6 +40,7 @@ #include <QClipboard> #include <QApplication> #include <QFileDialog> +#include <QStandardPaths> #include "ui/dialogs/ProgressDialog.h" #include "ui/dialogs/CustomMessageBox.h" @@ -49,11 +51,34 @@ #include <DesktopServices.h> #include <BuildConfig.h> -QString GuiUtil::uploadPaste(const QString &text, QWidget *parentWidget) +std::optional<QString> GuiUtil::uploadPaste(const QString &name, const QString &text, QWidget *parentWidget) { ProgressDialog dialog(parentWidget); auto pasteTypeSetting = static_cast<PasteUpload::PasteType>(APPLICATION->settings()->get("PastebinType").toInt()); auto pasteCustomAPIBaseSetting = APPLICATION->settings()->get("PastebinCustomAPIBase").toString(); + + { + QUrl baseUrl; + if (pasteCustomAPIBaseSetting.isEmpty()) + baseUrl = PasteUpload::PasteTypes[pasteTypeSetting].defaultBase; + else + baseUrl = pasteCustomAPIBaseSetting; + + if (baseUrl.isValid()) + { + auto response = CustomMessageBox::selectable(parentWidget, QObject::tr("Confirm Upload"), + QObject::tr("You are about to upload \"%1\" to %2.\n" + "You should double-check for personal information.\n\n" + "Are you sure?") + .arg(name, baseUrl.host()), + QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No, QMessageBox::No) + ->exec(); + + if (response != QMessageBox::Yes) + return {}; + } + } + std::unique_ptr<PasteUpload> paste(new PasteUpload(parentWidget, text, pasteCustomAPIBaseSetting, pasteTypeSetting)); dialog.execWithTask(paste.get()); diff --git a/launcher/ui/GuiUtil.h b/launcher/ui/GuiUtil.h index 5e109383..96ebd9a2 100644 --- a/launcher/ui/GuiUtil.h +++ b/launcher/ui/GuiUtil.h @@ -1,10 +1,11 @@ #pragma once #include <QWidget> +#include <optional> namespace GuiUtil { -QString uploadPaste(const QString &text, QWidget *parentWidget); +std::optional<QString> uploadPaste(const QString &name, const QString &text, QWidget *parentWidget); void setClipboardText(const QString &text); QStringList BrowseForFiles(QString context, QString caption, QString filter, QString defaultPath, QWidget *parentWidget); QString BrowseForFile(QString context, QString caption, QString filter, QString defaultPath, QWidget *parentWidget); diff --git a/launcher/ui/MainWindow.cpp b/launcher/ui/MainWindow.cpp index 3651aa15..72b7db64 100644 --- a/launcher/ui/MainWindow.cpp +++ b/launcher/ui/MainWindow.cpp @@ -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 @@ -42,6 +43,7 @@ #include "FileSystem.h" #include "MainWindow.h" +#include "ui_MainWindow.h" #include <QVariant> #include <QUrl> @@ -85,8 +87,7 @@ #include <net/Download.h> #include <news/NewsChecker.h> #include <tools/BaseProfiler.h> -#include <updater/DownloadTask.h> -#include <updater/UpdateChecker.h> +#include <updater/ExternalUpdater.h> #include <DesktopServices.h> #include "InstanceWindow.h" #include "InstancePageProvider.h" @@ -101,22 +102,20 @@ #include "ui/dialogs/NewsDialog.h" #include "ui/dialogs/ProgressDialog.h" #include "ui/dialogs/AboutDialog.h" -#include "ui/dialogs/VersionSelectDialog.h" #include "ui/dialogs/CustomMessageBox.h" #include "ui/dialogs/IconPickerDialog.h" #include "ui/dialogs/CopyInstanceDialog.h" -#include "ui/dialogs/UpdateDialog.h" #include "ui/dialogs/EditAccountDialog.h" #include "ui/dialogs/ExportInstanceDialog.h" -#include "ui/dialogs/ImportResourcePackDialog.h" +#include "ui/dialogs/ImportResourceDialog.h" #include "ui/themes/ITheme.h" +#include "ui/themes/ThemeManager.h" -#include <minecraft/mod/ResourcePackFolderModel.h> -#include <minecraft/mod/tasks/LocalResourcePackParseTask.h> -#include <minecraft/mod/TexturePackFolderModel.h> -#include <minecraft/mod/tasks/LocalTexturePackParseTask.h> +#include "minecraft/mod/tasks/LocalResourceParse.h" +#include "minecraft/mod/ModFolderModel.h" +#include "minecraft/mod/ShaderPackFolderModel.h" +#include "minecraft/WorldList.h" -#include "UpdateController.h" #include "KonamiCode.h" #include "InstanceImportTask.h" @@ -138,785 +137,112 @@ QString profileInUseFilter(const QString & profile, bool used) } } -// WHY: to hold the pre-translation strings together with the T pointer, so it can be retranslated without a lot of ugly code -template <typename T> -class Translated +MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWindow) { -public: - Translated(){} - Translated(QWidget *parent) - { - m_contained = new T(parent); - } - void setTooltipId(const char * tooltip) - { - m_tooltip = tooltip; - } - void setTextId(const char * text) - { - m_text = text; - } - operator T*() - { - return m_contained; - } - T * operator->() - { - return m_contained; - } - void retranslate() - { - if(m_text) - { - QString result; - result = QApplication::translate("MainWindow", m_text); - if(result.contains("%1")) { - result = result.arg(BuildConfig.LAUNCHER_DISPLAYNAME); - } - m_contained->setText(result); - } - if(m_tooltip) - { - QString result; - result = QApplication::translate("MainWindow", m_tooltip); - if(result.contains("%1")) { - result = result.arg(BuildConfig.LAUNCHER_DISPLAYNAME); - } - m_contained->setToolTip(result); - } - } -private: - T * m_contained = nullptr; - const char * m_text = nullptr; - const char * m_tooltip = nullptr; -}; -using TranslatedAction = Translated<QAction>; -using TranslatedToolButton = Translated<QToolButton>; - -class TranslatedToolbar -{ -public: - TranslatedToolbar(){} - TranslatedToolbar(QWidget *parent) - { - m_contained = new QToolBar(parent); - } - void setWindowTitleId(const char * title) - { - m_title = title; - } - operator QToolBar*() - { - return m_contained; - } - QToolBar * operator->() - { - return m_contained; - } - void retranslate() - { - if(m_title) - { - m_contained->setWindowTitle(QApplication::translate("MainWindow", m_title)); - } - } -private: - QToolBar * m_contained = nullptr; - const char * m_title = nullptr; -}; - -class MainWindow::Ui -{ -public: - TranslatedAction actionAddInstance; - //TranslatedAction actionRefresh; - TranslatedAction actionCheckUpdate; - TranslatedAction actionSettings; - TranslatedAction actionMoreNews; - TranslatedAction actionManageAccounts; - TranslatedAction actionLaunchInstance; - TranslatedAction actionKillInstance; - TranslatedAction actionRenameInstance; - TranslatedAction actionChangeInstGroup; - TranslatedAction actionChangeInstIcon; - TranslatedAction actionEditInstance; - TranslatedAction actionViewSelectedInstFolder; - TranslatedAction actionDeleteInstance; - TranslatedAction actionCAT; - TranslatedAction actionCopyInstance; - TranslatedAction actionLaunchInstanceOffline; - TranslatedAction actionLaunchInstanceDemo; - TranslatedAction actionExportInstance; - TranslatedAction actionCreateInstanceShortcut; - QVector<TranslatedAction *> all_actions; - - LabeledToolButton *renameButton = nullptr; - LabeledToolButton *changeIconButton = nullptr; - - QMenu * foldersMenu = nullptr; - TranslatedToolButton foldersMenuButton; - TranslatedAction actionViewInstanceFolder; - TranslatedAction actionViewCentralModsFolder; - - QMenu * editMenu = nullptr; - TranslatedAction actionUndoTrashInstance; - - QMenu * helpMenu = nullptr; - TranslatedToolButton helpMenuButton; - TranslatedAction actionClearMetadata; - #ifdef Q_OS_MAC - TranslatedAction actionAddToPATH; - #endif - TranslatedAction actionReportBug; - TranslatedAction actionDISCORD; - TranslatedAction actionMATRIX; - TranslatedAction actionREDDIT; - TranslatedAction actionAbout; - - TranslatedAction actionNoAccountsAdded; - TranslatedAction actionNoDefaultAccount; - - TranslatedAction actionLockToolbars; - - TranslatedAction actionChangeTheme; - - QVector<TranslatedToolButton *> all_toolbuttons; - - QWidget *centralWidget = nullptr; - QHBoxLayout *horizontalLayout = nullptr; - QStatusBar *statusBar = nullptr; - - QMenuBar *menuBar = nullptr; - QMenu *fileMenu; - QMenu *viewMenu; - QMenu *profileMenu; - - TranslatedAction actionCloseWindow; - - TranslatedAction actionOpenWiki; - TranslatedAction actionNewsMenuBar; - - TranslatedToolbar mainToolBar; - TranslatedToolbar instanceToolBar; - TranslatedToolbar newsToolBar; - QVector<TranslatedToolbar *> all_toolbars; - - void createMainToolbarActions(MainWindow *MainWindow) - { - actionAddInstance = TranslatedAction(MainWindow); - actionAddInstance->setObjectName(QStringLiteral("actionAddInstance")); - actionAddInstance->setIcon(APPLICATION->getThemedIcon("new")); - actionAddInstance.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Add Instanc&e...")); - actionAddInstance.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Add a new instance.")); - actionAddInstance->setShortcut(QKeySequence::New); - all_actions.append(&actionAddInstance); - - actionViewInstanceFolder = TranslatedAction(MainWindow); - actionViewInstanceFolder->setObjectName(QStringLiteral("actionViewInstanceFolder")); - actionViewInstanceFolder->setIcon(APPLICATION->getThemedIcon("viewfolder")); - actionViewInstanceFolder.setTextId(QT_TRANSLATE_NOOP("MainWindow", "&View Instance Folder")); - actionViewInstanceFolder.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Open the instance folder in a file browser.")); - all_actions.append(&actionViewInstanceFolder); - - actionViewCentralModsFolder = TranslatedAction(MainWindow); - actionViewCentralModsFolder->setObjectName(QStringLiteral("actionViewCentralModsFolder")); - actionViewCentralModsFolder->setIcon(APPLICATION->getThemedIcon("centralmods")); - actionViewCentralModsFolder.setTextId(QT_TRANSLATE_NOOP("MainWindow", "View &Central Mods Folder")); - actionViewCentralModsFolder.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Open the central mods folder in a file browser.")); - all_actions.append(&actionViewCentralModsFolder); - - foldersMenu = new QMenu(MainWindow); - foldersMenu->setTitle(tr("F&olders")); - foldersMenu->setToolTipsVisible(true); - - foldersMenu->addAction(actionViewInstanceFolder); - foldersMenu->addAction(actionViewCentralModsFolder); - - foldersMenuButton = TranslatedToolButton(MainWindow); - foldersMenuButton.setTextId(QT_TRANSLATE_NOOP("MainWindow", "F&olders")); - foldersMenuButton.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Open one of the folders shared between instances.")); - foldersMenuButton->setMenu(foldersMenu); - foldersMenuButton->setPopupMode(QToolButton::InstantPopup); - foldersMenuButton->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); - foldersMenuButton->setIcon(APPLICATION->getThemedIcon("viewfolder")); - foldersMenuButton->setFocusPolicy(Qt::NoFocus); - all_toolbuttons.append(&foldersMenuButton); - - actionSettings = TranslatedAction(MainWindow); - actionSettings->setObjectName(QStringLiteral("actionSettings")); - actionSettings->setIcon(APPLICATION->getThemedIcon("settings")); - actionSettings->setMenuRole(QAction::PreferencesRole); - actionSettings.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Setti&ngs...")); - actionSettings.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Change settings.")); - actionSettings->setShortcut(QKeySequence::Preferences); - all_actions.append(&actionSettings); - - actionUndoTrashInstance = TranslatedAction(MainWindow); - actionUndoTrashInstance->setObjectName(QStringLiteral("actionUndoTrashInstance")); - actionUndoTrashInstance.setTextId(QT_TRANSLATE_NOOP("MainWindow", "&Undo Last Instance Deletion")); - actionUndoTrashInstance->setEnabled(APPLICATION->instances()->trashedSomething()); - actionUndoTrashInstance->setShortcut(QKeySequence::Undo); - all_actions.append(&actionUndoTrashInstance); - - actionClearMetadata = TranslatedAction(MainWindow); - actionClearMetadata->setObjectName(QStringLiteral("actionClearMetadata")); - actionClearMetadata->setIcon(APPLICATION->getThemedIcon("refresh")); - actionClearMetadata.setTextId(QT_TRANSLATE_NOOP("MainWindow", "&Clear Metadata Cache")); - actionClearMetadata.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Clear cached metadata")); - all_actions.append(&actionClearMetadata); - - #ifdef Q_OS_MAC - actionAddToPATH = TranslatedAction(MainWindow); - actionAddToPATH->setObjectName(QStringLiteral("actionAddToPATH")); - actionAddToPATH.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Install to &PATH")); - actionAddToPATH.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Install a prismlauncher symlink to /usr/local/bin")); - all_actions.append(&actionAddToPATH); - #endif - - if (!BuildConfig.BUG_TRACKER_URL.isEmpty()) { - actionReportBug = TranslatedAction(MainWindow); - actionReportBug->setObjectName(QStringLiteral("actionReportBug")); - actionReportBug->setIcon(APPLICATION->getThemedIcon("bug")); - actionReportBug.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Report a &Bug...")); - actionReportBug.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Open the bug tracker to report a bug with %1.")); - all_actions.append(&actionReportBug); - } - - if(!BuildConfig.MATRIX_URL.isEmpty()) { - actionMATRIX = TranslatedAction(MainWindow); - actionMATRIX->setObjectName(QStringLiteral("actionMATRIX")); - actionMATRIX->setIcon(APPLICATION->getThemedIcon("matrix")); - actionMATRIX.setTextId(QT_TRANSLATE_NOOP("MainWindow", "&Matrix Space")); - actionMATRIX.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Open %1 Matrix space")); - all_actions.append(&actionMATRIX); - } - - if (!BuildConfig.DISCORD_URL.isEmpty()) { - actionDISCORD = TranslatedAction(MainWindow); - actionDISCORD->setObjectName(QStringLiteral("actionDISCORD")); - actionDISCORD->setIcon(APPLICATION->getThemedIcon("discord")); - actionDISCORD.setTextId(QT_TRANSLATE_NOOP("MainWindow", "&Discord Guild")); - actionDISCORD.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Open %1 Discord guild.")); - all_actions.append(&actionDISCORD); - } - - if (!BuildConfig.SUBREDDIT_URL.isEmpty()) { - actionREDDIT = TranslatedAction(MainWindow); - actionREDDIT->setObjectName(QStringLiteral("actionREDDIT")); - actionREDDIT->setIcon(APPLICATION->getThemedIcon("reddit-alien")); - actionREDDIT.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Sub&reddit")); - actionREDDIT.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Open %1 subreddit.")); - all_actions.append(&actionREDDIT); - } - - actionAbout = TranslatedAction(MainWindow); - actionAbout->setObjectName(QStringLiteral("actionAbout")); - actionAbout->setIcon(APPLICATION->getThemedIcon("about")); - actionAbout->setMenuRole(QAction::AboutRole); - actionAbout.setTextId(QT_TRANSLATE_NOOP("MainWindow", "&About %1")); - actionAbout.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "View information about %1.")); - all_actions.append(&actionAbout); - - if(BuildConfig.UPDATER_ENABLED) - { - actionCheckUpdate = TranslatedAction(MainWindow); - actionCheckUpdate->setObjectName(QStringLiteral("actionCheckUpdate")); - actionCheckUpdate->setIcon(APPLICATION->getThemedIcon("checkupdate")); - actionCheckUpdate.setTextId(QT_TRANSLATE_NOOP("MainWindow", "&Update...")); - actionCheckUpdate.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Check for new updates for %1.")); - actionCheckUpdate->setMenuRole(QAction::ApplicationSpecificRole); - all_actions.append(&actionCheckUpdate); - } - - actionCAT = TranslatedAction(MainWindow); - actionCAT->setObjectName(QStringLiteral("actionCAT")); - actionCAT->setCheckable(true); - actionCAT->setIcon(APPLICATION->getThemedIcon("cat")); - actionCAT.setTextId(QT_TRANSLATE_NOOP("MainWindow", "&Meow")); - actionCAT.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "It's a fluffy kitty :3")); - actionCAT->setPriority(QAction::LowPriority); - all_actions.append(&actionCAT); - - // profile menu and its actions - actionManageAccounts = TranslatedAction(MainWindow); - actionManageAccounts->setObjectName(QStringLiteral("actionManageAccounts")); - actionManageAccounts.setTextId(QT_TRANSLATE_NOOP("MainWindow", "&Manage Accounts...")); - // FIXME: no tooltip! - actionManageAccounts->setCheckable(false); - actionManageAccounts->setIcon(APPLICATION->getThemedIcon("accounts")); - all_actions.append(&actionManageAccounts); - - actionLockToolbars = TranslatedAction(MainWindow); - actionLockToolbars->setObjectName(QStringLiteral("actionLockToolbars")); - actionLockToolbars.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Lock Toolbars")); - actionLockToolbars->setCheckable(true); - all_actions.append(&actionLockToolbars); - - actionChangeTheme = TranslatedAction(MainWindow); - actionChangeTheme->setObjectName(QStringLiteral("actionChangeTheme")); - actionChangeTheme.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Themes")); - all_actions.append(&actionChangeTheme); - } - - void createMainToolbar(QMainWindow *MainWindow) - { - mainToolBar = TranslatedToolbar(MainWindow); - mainToolBar->setVisible(menuBar->isNativeMenuBar() || !APPLICATION->settings()->get("MenuBarInsteadOfToolBar").toBool()); - mainToolBar->setObjectName(QStringLiteral("mainToolBar")); - mainToolBar->setAllowedAreas(Qt::TopToolBarArea | Qt::BottomToolBarArea); - mainToolBar->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); - mainToolBar->setFloatable(false); - mainToolBar.setWindowTitleId(QT_TRANSLATE_NOOP("MainWindow", "Main Toolbar")); - - mainToolBar->addAction(actionAddInstance); - - mainToolBar->addSeparator(); - - QWidgetAction* foldersButtonAction = new QWidgetAction(MainWindow); - foldersButtonAction->setDefaultWidget(foldersMenuButton); - mainToolBar->addAction(foldersButtonAction); - - mainToolBar->addAction(actionSettings); - - helpMenu = new QMenu(MainWindow); - helpMenu->setToolTipsVisible(true); - - helpMenu->addAction(actionClearMetadata); - - #ifdef Q_OS_MAC - helpMenu->addAction(actionAddToPATH); - #endif - - if (!BuildConfig.BUG_TRACKER_URL.isEmpty()) { - helpMenu->addAction(actionReportBug); - } - - if(!BuildConfig.MATRIX_URL.isEmpty()) { - helpMenu->addAction(actionMATRIX); - } - - if (!BuildConfig.DISCORD_URL.isEmpty()) { - helpMenu->addAction(actionDISCORD); - } - - if (!BuildConfig.SUBREDDIT_URL.isEmpty()) { - helpMenu->addAction(actionREDDIT); - } - - helpMenu->addAction(actionAbout); - - helpMenuButton = TranslatedToolButton(MainWindow); - helpMenuButton.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Help")); - helpMenuButton.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Get help with %1 or Minecraft.")); - helpMenuButton->setMenu(helpMenu); - helpMenuButton->setPopupMode(QToolButton::InstantPopup); - helpMenuButton->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); - helpMenuButton->setIcon(APPLICATION->getThemedIcon("help")); - helpMenuButton->setFocusPolicy(Qt::NoFocus); - all_toolbuttons.append(&helpMenuButton); - QWidgetAction* helpButtonAction = new QWidgetAction(MainWindow); - helpButtonAction->setDefaultWidget(helpMenuButton); - mainToolBar->addAction(helpButtonAction); - - if(BuildConfig.UPDATER_ENABLED) - { - mainToolBar->addAction(actionCheckUpdate); - } - - mainToolBar->addSeparator(); - - mainToolBar->addAction(actionCAT); - - all_toolbars.append(&mainToolBar); - MainWindow->addToolBar(Qt::TopToolBarArea, mainToolBar); - } - - void createMenuBar(QMainWindow *MainWindow) - { - menuBar = new QMenuBar(MainWindow); - menuBar->setVisible(APPLICATION->settings()->get("MenuBarInsteadOfToolBar").toBool()); - - fileMenu = menuBar->addMenu(tr("&File")); - // Workaround for QTBUG-94802 (https://bugreports.qt.io/browse/QTBUG-94802); also present for other menus - fileMenu->setSeparatorsCollapsible(false); - fileMenu->addAction(actionAddInstance); - fileMenu->addAction(actionLaunchInstance); - fileMenu->addAction(actionKillInstance); - fileMenu->addAction(actionCloseWindow); - fileMenu->addSeparator(); - fileMenu->addAction(actionEditInstance); - fileMenu->addAction(actionChangeInstGroup); - fileMenu->addAction(actionViewSelectedInstFolder); - fileMenu->addAction(actionExportInstance); - fileMenu->addAction(actionCopyInstance); - fileMenu->addAction(actionDeleteInstance); - fileMenu->addAction(actionCreateInstanceShortcut); - fileMenu->addSeparator(); - fileMenu->addAction(actionSettings); - - editMenu = menuBar->addMenu(tr("&Edit")); - editMenu->addAction(actionUndoTrashInstance); - - viewMenu = menuBar->addMenu(tr("&View")); - viewMenu->setSeparatorsCollapsible(false); - viewMenu->addAction(actionChangeTheme); - viewMenu->addSeparator(); - viewMenu->addAction(actionCAT); - viewMenu->addSeparator(); - - viewMenu->addAction(actionLockToolbars); - - menuBar->addMenu(foldersMenu); - - profileMenu = menuBar->addMenu(tr("&Accounts")); - profileMenu->setSeparatorsCollapsible(false); - profileMenu->addAction(actionManageAccounts); - - helpMenu = menuBar->addMenu(tr("&Help")); - helpMenu->setSeparatorsCollapsible(false); - helpMenu->addAction(actionClearMetadata); - #ifdef Q_OS_MAC - helpMenu->addAction(actionAddToPATH); - #endif - helpMenu->addSeparator(); - helpMenu->addAction(actionAbout); - helpMenu->addAction(actionOpenWiki); - helpMenu->addAction(actionNewsMenuBar); - helpMenu->addSeparator(); - if (!BuildConfig.BUG_TRACKER_URL.isEmpty()) - helpMenu->addAction(actionReportBug); - if (!BuildConfig.MATRIX_URL.isEmpty()) - helpMenu->addAction(actionMATRIX); - if (!BuildConfig.DISCORD_URL.isEmpty()) - helpMenu->addAction(actionDISCORD); - if (!BuildConfig.SUBREDDIT_URL.isEmpty()) - helpMenu->addAction(actionREDDIT); - if(BuildConfig.UPDATER_ENABLED) - { - helpMenu->addSeparator(); - helpMenu->addAction(actionCheckUpdate); - } - MainWindow->setMenuBar(menuBar); - } + ui->setupUi(this); - void createMenuActions(MainWindow *MainWindow) - { - actionCloseWindow = TranslatedAction(MainWindow); - actionCloseWindow->setObjectName(QStringLiteral("actionCloseWindow")); - actionCloseWindow.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Close &Window")); - actionCloseWindow.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Close the current window")); - actionCloseWindow->setShortcut(QKeySequence::Close); - connect(actionCloseWindow, &QAction::triggered, APPLICATION, &Application::closeCurrentWindow); - all_actions.append(&actionCloseWindow); - - actionOpenWiki = TranslatedAction(MainWindow); - actionOpenWiki->setObjectName(QStringLiteral("actionOpenWiki")); - actionOpenWiki.setTextId(QT_TRANSLATE_NOOP("MainWindow", "%1 &Help")); - actionOpenWiki.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Open the %1 wiki")); - actionOpenWiki->setIcon(APPLICATION->getThemedIcon("help")); - connect(actionOpenWiki, &QAction::triggered, MainWindow, &MainWindow::on_actionOpenWiki_triggered); - all_actions.append(&actionOpenWiki); - - actionNewsMenuBar = TranslatedAction(MainWindow); - actionNewsMenuBar->setObjectName(QStringLiteral("actionNewsMenuBar")); - actionNewsMenuBar.setTextId(QT_TRANSLATE_NOOP("MainWindow", "%1 &News")); - actionNewsMenuBar.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Open the %1 wiki")); - actionNewsMenuBar->setIcon(APPLICATION->getThemedIcon("news")); - connect(actionNewsMenuBar, &QAction::triggered, MainWindow, &MainWindow::on_actionMoreNews_triggered); - all_actions.append(&actionNewsMenuBar); - } - - // "Instance actions" are actions that require an instance to be selected (i.e. "new instance" is not here) - // Actions that also require other conditions (e.g. a running instance) won't be changed. - void setInstanceActionsEnabled(bool enabled) - { - actionEditInstance->setEnabled(enabled); - actionChangeInstGroup->setEnabled(enabled); - actionViewSelectedInstFolder->setEnabled(enabled); - actionExportInstance->setEnabled(enabled); - actionDeleteInstance->setEnabled(enabled); - actionCopyInstance->setEnabled(enabled); - actionCreateInstanceShortcut->setEnabled(enabled); - } + setWindowIcon(APPLICATION->getThemedIcon("logo")); + setWindowTitle(APPLICATION->applicationDisplayName()); +#ifndef QT_NO_ACCESSIBILITY + setAccessibleName(BuildConfig.LAUNCHER_DISPLAYNAME); +#endif - void createStatusBar(QMainWindow *MainWindow) + // instance toolbar stuff { - statusBar = new QStatusBar(MainWindow); - statusBar->setObjectName(QStringLiteral("statusBar")); - MainWindow->setStatusBar(statusBar); - } + // Qt doesn't like vertical moving toolbars, so we have to force them... + // See https://github.com/PolyMC/PolyMC/issues/493 + connect(ui->instanceToolBar, &QToolBar::orientationChanged, + [=](Qt::Orientation) { ui->instanceToolBar->setOrientation(Qt::Vertical); }); - void createNewsToolbar(QMainWindow *MainWindow) - { - newsToolBar = TranslatedToolbar(MainWindow); - newsToolBar->setObjectName(QStringLiteral("newsToolBar")); - newsToolBar->setAllowedAreas(Qt::TopToolBarArea | Qt::BottomToolBarArea); - newsToolBar->setIconSize(QSize(16, 16)); - newsToolBar->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); - newsToolBar->setFloatable(false); - newsToolBar->setWindowTitle(QT_TRANSLATE_NOOP("MainWindow", "News Toolbar")); - - actionMoreNews = TranslatedAction(MainWindow); - actionMoreNews->setObjectName(QStringLiteral("actionMoreNews")); - actionMoreNews->setIcon(APPLICATION->getThemedIcon("news")); - actionMoreNews.setTextId(QT_TRANSLATE_NOOP("MainWindow", "More news...")); - actionMoreNews.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Open the development blog to read more news about %1.")); - all_actions.append(&actionMoreNews); - newsToolBar->addAction(actionMoreNews); - - all_toolbars.append(&newsToolBar); - MainWindow->addToolBar(Qt::BottomToolBarArea, newsToolBar); - } - - void createInstanceActions(QMainWindow *MainWindow) - { - // NOTE: not added to toolbar, but used for instance context menu (right click) - actionChangeInstIcon = TranslatedAction(MainWindow); - actionChangeInstIcon->setObjectName(QStringLiteral("actionChangeInstIcon")); - actionChangeInstIcon->setIcon(QIcon(":/icons/instances/grass")); - actionChangeInstIcon->setIconVisibleInMenu(true); - actionChangeInstIcon.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Change Icon")); - actionChangeInstIcon.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Change the selected instance's icon.")); - all_actions.append(&actionChangeInstIcon); - - changeIconButton = new LabeledToolButton(MainWindow); + // if you try to add a widget to a toolbar in a .ui file + // qt designer will delete it when you save the file >:( + changeIconButton = new LabeledToolButton(this); changeIconButton->setObjectName(QStringLiteral("changeIconButton")); changeIconButton->setIcon(APPLICATION->getThemedIcon("news")); - changeIconButton->setToolTip(actionChangeInstIcon->toolTip()); changeIconButton->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); + connect(changeIconButton, &QToolButton::clicked, this, &MainWindow::on_actionChangeInstIcon_triggered); + ui->instanceToolBar->insertWidgetBefore(ui->actionLaunchInstance, changeIconButton); - // NOTE: not added to toolbar, but used for instance context menu (right click) - actionRenameInstance = TranslatedAction(MainWindow); - actionRenameInstance->setObjectName(QStringLiteral("actionRenameInstance")); - actionRenameInstance.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Rename")); - actionRenameInstance.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Rename the selected instance.")); - actionRenameInstance->setIcon(APPLICATION->getThemedIcon("rename")); - all_actions.append(&actionRenameInstance); - - // the rename label is inside the rename tool button - renameButton = new LabeledToolButton(MainWindow); + renameButton = new LabeledToolButton(this); renameButton->setObjectName(QStringLiteral("renameButton")); - renameButton->setToolTip(actionRenameInstance->toolTip()); renameButton->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); + connect(renameButton, &QToolButton::clicked, this, &MainWindow::on_actionRenameInstance_triggered); + ui->instanceToolBar->insertWidgetBefore(ui->actionLaunchInstance, renameButton); - actionLaunchInstance = TranslatedAction(MainWindow); - actionLaunchInstance->setObjectName(QStringLiteral("actionLaunchInstance")); - actionLaunchInstance.setTextId(QT_TRANSLATE_NOOP("MainWindow", "&Launch")); - actionLaunchInstance.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Launch the selected instance.")); - actionLaunchInstance->setIcon(APPLICATION->getThemedIcon("launch")); - all_actions.append(&actionLaunchInstance); - - actionLaunchInstanceOffline = TranslatedAction(MainWindow); - actionLaunchInstanceOffline->setObjectName(QStringLiteral("actionLaunchInstanceOffline")); - actionLaunchInstanceOffline.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Launch &Offline")); - actionLaunchInstanceOffline.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Launch the selected instance in offline mode.")); - all_actions.append(&actionLaunchInstanceOffline); - - actionLaunchInstanceDemo = TranslatedAction(MainWindow); - actionLaunchInstanceDemo->setObjectName(QStringLiteral("actionLaunchInstanceDemo")); - actionLaunchInstanceDemo.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Launch &Demo")); - actionLaunchInstanceDemo.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Launch the selected instance in demo mode.")); - all_actions.append(&actionLaunchInstanceDemo); - - actionKillInstance = TranslatedAction(MainWindow); - actionKillInstance->setObjectName(QStringLiteral("actionKillInstance")); - actionKillInstance->setDisabled(true); - actionKillInstance.setTextId(QT_TRANSLATE_NOOP("MainWindow", "&Kill")); - actionKillInstance.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Kill the running instance")); - actionKillInstance->setShortcut(QKeySequence(tr("Ctrl+K"))); - actionKillInstance->setIcon(APPLICATION->getThemedIcon("status-bad")); - all_actions.append(&actionKillInstance); - - actionEditInstance = TranslatedAction(MainWindow); - actionEditInstance->setObjectName(QStringLiteral("actionEditInstance")); - actionEditInstance.setTextId(QT_TRANSLATE_NOOP("MainWindow", "&Edit...")); - actionEditInstance.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Change the instance settings, mods and versions.")); - actionEditInstance->setShortcut(QKeySequence(tr("Ctrl+I"))); - actionEditInstance->setIcon(APPLICATION->getThemedIcon("settings-configure")); - all_actions.append(&actionEditInstance); - - actionChangeInstGroup = TranslatedAction(MainWindow); - actionChangeInstGroup->setObjectName(QStringLiteral("actionChangeInstGroup")); - actionChangeInstGroup.setTextId(QT_TRANSLATE_NOOP("MainWindow", "&Change Group...")); - actionChangeInstGroup.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Change the selected instance's group.")); - actionChangeInstGroup->setShortcut(QKeySequence(tr("Ctrl+G"))); - actionChangeInstGroup->setIcon(APPLICATION->getThemedIcon("tag")); - all_actions.append(&actionChangeInstGroup); - - actionViewSelectedInstFolder = TranslatedAction(MainWindow); - actionViewSelectedInstFolder->setObjectName(QStringLiteral("actionViewSelectedInstFolder")); - actionViewSelectedInstFolder.setTextId(QT_TRANSLATE_NOOP("MainWindow", "&Folder")); - actionViewSelectedInstFolder.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Open the selected instance's root folder in a file browser.")); - actionViewSelectedInstFolder->setIcon(APPLICATION->getThemedIcon("viewfolder")); - all_actions.append(&actionViewSelectedInstFolder); - - actionExportInstance = TranslatedAction(MainWindow); - actionExportInstance->setObjectName(QStringLiteral("actionExportInstance")); - actionExportInstance.setTextId(QT_TRANSLATE_NOOP("MainWindow", "E&xport...")); - actionExportInstance.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Export the selected instance as a zip file.")); - actionExportInstance->setShortcut(QKeySequence(tr("Ctrl+E"))); - actionExportInstance->setIcon(APPLICATION->getThemedIcon("export")); - all_actions.append(&actionExportInstance); - - actionDeleteInstance = TranslatedAction(MainWindow); - actionDeleteInstance->setObjectName(QStringLiteral("actionDeleteInstance")); - actionDeleteInstance.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Dele&te")); - actionDeleteInstance.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Delete the selected instance.")); - actionDeleteInstance->setShortcuts({QKeySequence(tr("Backspace")), QKeySequence::Delete}); - actionDeleteInstance->setAutoRepeat(false); - actionDeleteInstance->setIcon(APPLICATION->getThemedIcon("delete")); - all_actions.append(&actionDeleteInstance); - - actionCopyInstance = TranslatedAction(MainWindow); - actionCopyInstance->setObjectName(QStringLiteral("actionCopyInstance")); - actionCopyInstance.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Cop&y...")); - actionCopyInstance.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Copy the selected instance.")); - actionCopyInstance->setShortcut(QKeySequence(tr("Ctrl+D"))); - 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); - } - - void createInstanceToolbar(QMainWindow *MainWindow) - { - instanceToolBar = TranslatedToolbar(MainWindow); - instanceToolBar->setObjectName(QStringLiteral("instanceToolBar")); - // disabled until we have an instance selected - instanceToolBar->setEnabled(false); - // Qt doesn't like vertical moving toolbars, so we have to force them... - // See https://github.com/PolyMC/PolyMC/issues/493 - connect(instanceToolBar, &QToolBar::orientationChanged, [=](Qt::Orientation){ instanceToolBar->setOrientation(Qt::Vertical); }); - instanceToolBar->setAllowedAreas(Qt::LeftToolBarArea | Qt::RightToolBarArea); - instanceToolBar->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); - instanceToolBar->setIconSize(QSize(16, 16)); - - instanceToolBar->setFloatable(false); - instanceToolBar->setWindowTitle(QT_TRANSLATE_NOOP("MainWindow", "Instance Toolbar")); - - instanceToolBar->addWidget(changeIconButton); - instanceToolBar->addWidget(renameButton); - - instanceToolBar->addSeparator(); + ui->instanceToolBar->insertSeparator(ui->actionLaunchInstance); - instanceToolBar->addAction(actionLaunchInstance); - instanceToolBar->addAction(actionKillInstance); - - instanceToolBar->addSeparator(); - - instanceToolBar->addAction(actionEditInstance); - instanceToolBar->addAction(actionChangeInstGroup); - - instanceToolBar->addAction(actionViewSelectedInstFolder); + // restore the instance toolbar settings + auto const setting_name = QString("WideBarVisibility_%1").arg(ui->instanceToolBar->objectName()); + if (!APPLICATION->settings()->contains(setting_name)) + instanceToolbarSetting = APPLICATION->settings()->registerSetting(setting_name); + else + instanceToolbarSetting = APPLICATION->settings()->getSetting(setting_name); - instanceToolBar->addAction(actionExportInstance); - instanceToolBar->addAction(actionCopyInstance); - instanceToolBar->addAction(actionDeleteInstance); + ui->instanceToolBar->setVisibilityState(instanceToolbarSetting->get().toByteArray()); - instanceToolBar->addAction(actionCreateInstanceShortcut); // TODO find better position for this + ui->instanceToolBar->addContextMenuAction(ui->newsToolBar->toggleViewAction()); + ui->instanceToolBar->addContextMenuAction(ui->instanceToolBar->toggleViewAction()); + ui->instanceToolBar->addContextMenuAction(ui->actionLockToolbars); - QLayout * lay = instanceToolBar->layout(); - for(int i = 0; i < lay->count(); i++) - { - QLayoutItem * item = lay->itemAt(i); - if (item->widget()->metaObject()->className() == QString("QToolButton")) - { - item->setAlignment(Qt::AlignLeft); - } - } - - all_toolbars.append(&instanceToolBar); - MainWindow->addToolBar(Qt::RightToolBarArea, instanceToolBar); } - void setupUi(MainWindow *MainWindow) + // set the menu for the folders help, and accounts tool buttons { - if (MainWindow->objectName().isEmpty()) - { - MainWindow->setObjectName(QStringLiteral("MainWindow")); - } - MainWindow->resize(800, 600); - MainWindow->setWindowIcon(APPLICATION->getThemedIcon("logo")); - MainWindow->setWindowTitle(APPLICATION->applicationDisplayName()); -#ifndef QT_NO_ACCESSIBILITY - MainWindow->setAccessibleName(BuildConfig.LAUNCHER_DISPLAYNAME); -#endif - - createMainToolbarActions(MainWindow); - createMenuActions(MainWindow); - createInstanceActions(MainWindow); - - createMenuBar(MainWindow); - - createMainToolbar(MainWindow); - - centralWidget = new QWidget(MainWindow); - centralWidget->setObjectName(QStringLiteral("centralWidget")); - horizontalLayout = new QHBoxLayout(centralWidget); - horizontalLayout->setSpacing(0); - horizontalLayout->setObjectName(QStringLiteral("horizontalLayout")); - horizontalLayout->setSizeConstraint(QLayout::SetDefaultConstraint); - horizontalLayout->setContentsMargins(0, 0, 0, 0); - MainWindow->setCentralWidget(centralWidget); + auto foldersMenuButton = dynamic_cast<QToolButton*>(ui->mainToolBar->widgetForAction(ui->actionFoldersButton)); + ui->actionFoldersButton->setMenu(ui->foldersMenu); + foldersMenuButton->setPopupMode(QToolButton::InstantPopup); - createStatusBar(MainWindow); - createNewsToolbar(MainWindow); - createInstanceToolbar(MainWindow); + helpMenuButton = dynamic_cast<QToolButton*>(ui->mainToolBar->widgetForAction(ui->actionHelpButton)); + ui->actionHelpButton->setMenu(new QMenu(this)); + ui->actionHelpButton->menu()->addActions(ui->helpMenu->actions()); + ui->actionHelpButton->menu()->removeAction(ui->actionCheckUpdate); + helpMenuButton->setPopupMode(QToolButton::InstantPopup); - MainWindow->updateToolsMenu(); - MainWindow->updateThemeMenu(); + auto accountMenuButton = dynamic_cast<QToolButton*>(ui->mainToolBar->widgetForAction(ui->actionAccountsButton)); + ui->actionAccountsButton->setMenu(ui->accountsMenu); + accountMenuButton->setPopupMode(QToolButton::InstantPopup); + } - retranslateUi(MainWindow); + // hide, disable and show stuff + { + ui->actionReportBug->setVisible(!BuildConfig.BUG_TRACKER_URL.isEmpty()); + ui->actionMATRIX->setVisible(!BuildConfig.MATRIX_URL.isEmpty()); + ui->actionDISCORD->setVisible(!BuildConfig.DISCORD_URL.isEmpty()); + ui->actionREDDIT->setVisible(!BuildConfig.SUBREDDIT_URL.isEmpty()); - QMetaObject::connectSlotsByName(MainWindow); - } // setupUi + ui->actionCheckUpdate->setVisible(BuildConfig.UPDATER_ENABLED); - void retranslateUi(MainWindow *MainWindow) - { - // all the actions - for(auto * item: all_actions) - { - item->retranslate(); - } - for(auto * item: all_toolbars) - { - item->retranslate(); - } - for(auto * item: all_toolbuttons) - { - item->retranslate(); - } - // submenu buttons - foldersMenuButton->setText(tr("Folders")); - helpMenuButton->setText(tr("Help")); +#ifndef Q_OS_MAC + ui->actionAddToPATH->setVisible(false); +#endif - // playtime counter - if (MainWindow->m_statusCenter) - { - MainWindow->updateStatusCenter(); - } - } // retranslateUi -}; + // disabled until we have an instance selected + ui->instanceToolBar->setEnabled(false); + setInstanceActionsEnabled(false); + } -MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new MainWindow::Ui) -{ - ui->setupUi(this); + // add the toolbar toggles to the view menu + ui->viewMenu->addAction(ui->instanceToolBar->toggleViewAction()); + ui->viewMenu->addAction(ui->newsToolBar->toggleViewAction()); + updateThemeMenu(); + updateMainToolBar(); // OSX magic. setUnifiedTitleAndToolBarOnMac(true); // Global shortcuts { + // you can't set QKeySequence::StandardKey shortcuts in qt designer >:( + ui->actionAddInstance->setShortcut(QKeySequence::New); + ui->actionSettings->setShortcut(QKeySequence::Preferences); + ui->actionUndoTrashInstance->setShortcut(QKeySequence::Undo); + ui->actionDeleteInstance->setShortcuts({ QKeySequence(tr("Backspace")), QKeySequence::Delete }); + ui->actionCloseWindow->setShortcut(QKeySequence::Close); + connect(ui->actionCloseWindow, &QAction::triggered, APPLICATION, &Application::closeCurrentWindow); + // FIXME: This is kinda weird. and bad. We need some kind of managed shutdown. auto q = new QShortcut(QKeySequence::Quit, this); - connect(q, SIGNAL(activated()), qApp, SLOT(quit())); + connect(q, &QShortcut::activated, APPLICATION, &Application::quit); } // Konami Code @@ -934,6 +260,7 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new MainWindow newsLabel->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); newsLabel->setFocusPolicy(Qt::NoFocus); ui->newsToolBar->insertWidget(ui->actionMoreNews, newsLabel); + QObject::connect(newsLabel, &QAbstractButton::clicked, this, &MainWindow::newsButtonClicked); QObject::connect(m_newsChecker.get(), &NewsChecker::newsLoaded, this, &MainWindow::updateNewsLabel); updateNewsLabel(); @@ -953,7 +280,7 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new MainWindow view->installEventFilter(this); view->setContextMenuPolicy(Qt::CustomContextMenu); connect(view, &QWidget::customContextMenuRequested, this, &MainWindow::showInstanceContextMenu); - connect(view, &InstanceView::droppedURLs, this, &MainWindow::droppedURLs, Qt::QueuedConnection); + connect(view, &InstanceView::droppedURLs, this, &MainWindow::processURLs, Qt::QueuedConnection); proxymodel = new InstanceProxyModel(this); proxymodel->setSourceModel(APPLICATION->instances().get()); @@ -969,10 +296,12 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new MainWindow } // The cat background { + // set the cat action priority here so you can still see the action in qt designer + ui->actionCAT->setPriority(QAction::LowPriority); bool cat_enable = APPLICATION->settings()->get("TheCat").toBool(); ui->actionCAT->setChecked(cat_enable); - // NOTE: calling the operator like that is an ugly hack to appease ancient gcc... - connect(ui->actionCAT.operator->(), SIGNAL(toggled(bool)), SLOT(onCatToggled(bool))); + connect(ui->actionCAT, &QAction::toggled, this, &MainWindow::onCatToggled); + connect(APPLICATION, &Application::currentCatChanged, this, &MainWindow::onCatChanged); setCatBackground(cat_enable); } @@ -1009,25 +338,13 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new MainWindow // Add "manage accounts" button, right align QWidget *spacer = new QWidget(); spacer->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); - ui->mainToolBar->addWidget(spacer); + ui->mainToolBar->insertWidget(ui->actionAccountsButton, spacer); - accountMenu = new QMenu(this); // Use undocumented property... https://stackoverflow.com/questions/7121718/create-a-scrollbar-in-a-submenu-qt - accountMenu->setStyleSheet("QMenu { menu-scrollable: 1; }"); + ui->accountsMenu->setStyleSheet("QMenu { menu-scrollable: 1; }"); repopulateAccountsMenu(); - accountMenuButton = new QToolButton(this); - accountMenuButton->setMenu(accountMenu); - accountMenuButton->setPopupMode(QToolButton::InstantPopup); - accountMenuButton->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); - accountMenuButton->setIcon(APPLICATION->getThemedIcon("noaccount")); - - QWidgetAction *accountMenuButtonAction = new QWidgetAction(this); - accountMenuButtonAction->setDefaultWidget(accountMenuButton); - - ui->mainToolBar->addAction(accountMenuButtonAction); - // Update the menu when the active account changes. // Shouldn't have to use lambdas here like this, but if I don't, the compiler throws a fit. // Template hell sucks... @@ -1059,35 +376,21 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new MainWindow updateNewsLabel(); } - - if(BuildConfig.UPDATER_ENABLED) - { + if (BuildConfig.UPDATER_ENABLED) { bool updatesAllowed = APPLICATION->updatesAreAllowed(); updatesAllowedChanged(updatesAllowed); - // NOTE: calling the operator like that is an ugly hack to appease ancient gcc... - connect(ui->actionCheckUpdate.operator->(), &QAction::triggered, this, &MainWindow::checkForUpdates); + connect(ui->actionCheckUpdate, &QAction::triggered, this, &MainWindow::checkForUpdates); // set up the updater object. - auto updater = APPLICATION->updateChecker(); - connect(updater.get(), &UpdateChecker::updateAvailable, this, &MainWindow::updateAvailable); - connect(updater.get(), &UpdateChecker::noUpdateFound, this, &MainWindow::updateNotAvailable); - // if automatic update checks are allowed, start one. - if (APPLICATION->settings()->get("AutoUpdate").toBool() && updatesAllowed) - { - updater->checkForUpdate(APPLICATION->settings()->get("UpdateChannel").toString(), false); - } + auto updater = APPLICATION->updater(); - if (APPLICATION->updateChecker()->getExternalUpdater()) - { - connect(APPLICATION->updateChecker()->getExternalUpdater(), - &ExternalUpdater::canCheckForUpdatesChanged, - this, - &MainWindow::updatesAllowedChanged); + if (updater) { + connect(updater.get(), &ExternalUpdater::canCheckForUpdatesChanged, this, &MainWindow::updatesAllowedChanged); } } - connect(ui->actionUndoTrashInstance.operator->(), &QAction::triggered, this, &MainWindow::undoTrashInstance); + connect(ui->actionUndoTrashInstance, &QAction::triggered, this, &MainWindow::undoTrashInstance); setSelectedInstanceById(APPLICATION->settings()->get("SelectedInstance").toString()); @@ -1115,10 +418,10 @@ void MainWindow::retranslateUi() MinecraftAccountPtr defaultAccount = accounts->defaultAccount(); if(defaultAccount) { auto profileLabel = profileInUseFilter(defaultAccount->profileName(), defaultAccount->isInUse()); - accountMenuButton->setText(profileLabel); + ui->actionAccountsButton->setText(profileLabel); } else { - accountMenuButton->setText(tr("Accounts")); + ui->actionAccountsButton->setText(tr("Accounts")); } if (m_selectedInstance) { @@ -1128,6 +431,20 @@ void MainWindow::retranslateUi() } ui->retranslateUi(this); + + changeIconButton->setToolTip(ui->actionChangeInstIcon->toolTip()); + renameButton->setToolTip(ui->actionRenameInstance->toolTip()); + + // replace the %1 with the launcher display name in some actions + if (helpMenuButton->toolTip().contains("%1")) + helpMenuButton->setToolTip(helpMenuButton->toolTip().arg(BuildConfig.LAUNCHER_DISPLAYNAME)); + + for (auto action : ui->helpMenu->actions()) { + if (action->text().contains("%1")) + action->setText(action->text().arg(BuildConfig.LAUNCHER_DISPLAYNAME)); + if (action->toolTip().contains("%1")) + action->setToolTip(action->toolTip().arg(BuildConfig.LAUNCHER_DISPLAYNAME)); + } } MainWindow::~MainWindow() @@ -1167,13 +484,16 @@ void MainWindow::showInstanceContextMenu(const QPoint &pos) bool onInstance = view->indexAt(pos).isValid(); if (onInstance) { - actions = ui->instanceToolBar->actions(); + // reuse the file menu actions + actions = ui->fileMenu->actions(); - // replace the change icon widget with an actual action - actions.replace(0, ui->actionChangeInstIcon); + // remove the add instance action, launcher settings action and close action + actions.removeFirst(); + actions.removeLast(); + actions.removeLast(); - // replace the rename widget with an actual action - actions.replace(1, ui->actionRenameInstance); + actions.prepend(ui->actionChangeInstIcon); + actions.prepend(ui->actionRenameInstance); // add header actions.prepend(actionSep); @@ -1229,8 +549,6 @@ void MainWindow::updateMainToolBar() void MainWindow::updateToolsMenu() { - QToolButton *launchButton = dynamic_cast<QToolButton*>(ui->instanceToolBar->widgetForAction(ui->actionLaunchInstance)); - bool currentInstanceRunning = m_selectedInstance && m_selectedInstance->isRunning(); ui->actionLaunchInstance->setDisabled(!m_selectedInstance || currentInstanceRunning); @@ -1238,7 +556,6 @@ void MainWindow::updateToolsMenu() ui->actionLaunchInstanceDemo->setDisabled(!m_selectedInstance || currentInstanceRunning); QMenu *launchMenu = ui->actionLaunchInstance->menu(); - launchButton->setPopupMode(QToolButton::MenuButtonPopup); if (launchMenu) { launchMenu->clear(); @@ -1247,7 +564,6 @@ void MainWindow::updateToolsMenu() { launchMenu = new QMenu(this); } - QAction *normalLaunch = launchMenu->addAction(tr("Launch")); normalLaunch->setShortcut(QKeySequence::Open); QAction *normalLaunchOffline = launchMenu->addAction(tr("Launch Offline")); @@ -1345,7 +661,7 @@ void MainWindow::updateThemeMenu() themeAction->setActionGroup(themesGroup); connect(themeAction, &QAction::triggered, [theme]() { - APPLICATION->setApplicationTheme(theme->id(),false); + APPLICATION->setApplicationTheme(theme->id()); APPLICATION->settings()->set("ApplicationTheme", theme->id()); }); } @@ -1355,8 +671,7 @@ void MainWindow::updateThemeMenu() void MainWindow::repopulateAccountsMenu() { - accountMenu->clear(); - ui->profileMenu->clear(); + ui->accountsMenu->clear(); auto accounts = APPLICATION->accounts(); MinecraftAccountPtr defaultAccount = accounts->defaultAccount(); @@ -1365,23 +680,17 @@ void MainWindow::repopulateAccountsMenu() if (defaultAccount) { // this can be called before accountMenuButton exists - if (accountMenuButton) + if (ui->actionAccountsButton) { auto profileLabel = profileInUseFilter(defaultAccount->profileName(), defaultAccount->isInUse()); - accountMenuButton->setText(profileLabel); + ui->actionAccountsButton->setText(profileLabel); } } if (accounts->count() <= 0) { - ui->all_actions.removeAll(&ui->actionNoAccountsAdded); - ui->actionNoAccountsAdded = TranslatedAction(this); - ui->actionNoAccountsAdded->setObjectName(QStringLiteral("actionNoAccountsAdded")); - ui->actionNoAccountsAdded.setTextId(QT_TRANSLATE_NOOP("MainWindow", "No accounts added!")); ui->actionNoAccountsAdded->setEnabled(false); - accountMenu->addAction(ui->actionNoAccountsAdded); - ui->profileMenu->addAction(ui->actionNoAccountsAdded); - ui->all_actions.append(&ui->actionNoAccountsAdded); + ui->accountsMenu->addAction(ui->actionNoAccountsAdded); } else { @@ -1412,37 +721,22 @@ void MainWindow::repopulateAccountsMenu() action->setShortcut(QKeySequence(tr("Ctrl+%1").arg(i + 1))); } - accountMenu->addAction(action); - ui->profileMenu->addAction(action); + ui->accountsMenu->addAction(action); connect(action, SIGNAL(triggered(bool)), SLOT(changeActiveAccount())); } } - accountMenu->addSeparator(); - ui->profileMenu->addSeparator(); + ui->accountsMenu->addSeparator(); - ui->all_actions.removeAll(&ui->actionNoDefaultAccount); - ui->actionNoDefaultAccount = TranslatedAction(this); - ui->actionNoDefaultAccount->setObjectName(QStringLiteral("actionNoDefaultAccount")); - ui->actionNoDefaultAccount.setTextId(QT_TRANSLATE_NOOP("MainWindow", "No Default Account")); - ui->actionNoDefaultAccount->setCheckable(true); - ui->actionNoDefaultAccount->setIcon(APPLICATION->getThemedIcon("noaccount")); ui->actionNoDefaultAccount->setData(-1); - ui->actionNoDefaultAccount->setShortcut(QKeySequence(tr("Ctrl+0"))); - if (!defaultAccount) { - ui->actionNoDefaultAccount->setChecked(true); - } + ui->actionNoDefaultAccount->setChecked(!defaultAccount); + + ui->accountsMenu->addAction(ui->actionNoDefaultAccount); - accountMenu->addAction(ui->actionNoDefaultAccount); - ui->profileMenu->addAction(ui->actionNoDefaultAccount); connect(ui->actionNoDefaultAccount, SIGNAL(triggered(bool)), SLOT(changeActiveAccount())); - ui->all_actions.append(&ui->actionNoDefaultAccount); - ui->actionNoDefaultAccount.retranslate(); - accountMenu->addSeparator(); - ui->profileMenu->addSeparator(); - accountMenu->addAction(ui->actionManageAccounts); - ui->profileMenu->addAction(ui->actionManageAccounts); + ui->accountsMenu->addSeparator(); + ui->accountsMenu->addAction(ui->actionManageAccounts); } void MainWindow::updatesAllowedChanged(bool allowed) @@ -1486,20 +780,20 @@ void MainWindow::defaultAccountChanged() if (account && account->profileName() != "") { auto profileLabel = profileInUseFilter(account->profileName(), account->isInUse()); - accountMenuButton->setText(profileLabel); + ui->actionAccountsButton->setText(profileLabel); auto face = account->getFace(); if(face.isNull()) { - accountMenuButton->setIcon(APPLICATION->getThemedIcon("noaccount")); + ui->actionAccountsButton->setIcon(APPLICATION->getThemedIcon("noaccount")); } else { - accountMenuButton->setIcon(face); + ui->actionAccountsButton->setIcon(face); } return; } // Set the icon to the "no account" icon. - accountMenuButton->setIcon(APPLICATION->getThemedIcon("noaccount")); - accountMenuButton->setText(tr("Accounts")); + ui->actionAccountsButton->setIcon(APPLICATION->getThemedIcon("noaccount")); + ui->actionAccountsButton->setText(tr("Accounts")); } bool MainWindow::eventFilter(QObject *obj, QEvent *ev) @@ -1561,32 +855,6 @@ void MainWindow::updateNewsLabel() } } -void MainWindow::updateAvailable(GoUpdate::Status status) -{ - if(!APPLICATION->updatesAreAllowed()) - { - updateNotAvailable(); - return; - } - UpdateDialog dlg(true, this); - UpdateAction action = (UpdateAction)dlg.exec(); - switch (action) - { - case UPDATE_LATER: - qDebug() << "Update will be installed later."; - break; - case UPDATE_NOW: - downloadUpdates(status); - break; - } -} - -void MainWindow::updateNotAvailable() -{ - UpdateDialog dlg(false, this); - dlg.exec(); -} - QList<int> stringToIntList(const QString &string) { #if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) @@ -1611,72 +879,15 @@ QString intListToString(const QList<int> &list) return slist.join(','); } -void MainWindow::downloadUpdates(GoUpdate::Status status) -{ - if(!APPLICATION->updatesAreAllowed()) - { - return; - } - qDebug() << "Downloading updates."; - ProgressDialog updateDlg(this); - status.rootPath = APPLICATION->root(); - - auto dlPath = FS::PathCombine(APPLICATION->root(), "update", "XXXXXX"); - if (!FS::ensureFilePathExists(dlPath)) - { - CustomMessageBox::selectable(this, tr("Error"), tr("Couldn't create folder for update downloads:\n%1").arg(dlPath), QMessageBox::Warning)->show(); - } - GoUpdate::DownloadTask updateTask(APPLICATION->network(), status, dlPath, &updateDlg); - // If the task succeeds, install the updates. - if (updateDlg.execWithTask(&updateTask)) - { - /** - * NOTE: This disables launching instances until the update either succeeds (and this process exits) - * or the update fails (and the control leaves this scope). - */ - APPLICATION->updateIsRunning(true); - UpdateController update(this, APPLICATION->root(), updateTask.updateFilesDir(), updateTask.operations()); - update.installUpdates(); - APPLICATION->updateIsRunning(false); - } - else - { - CustomMessageBox::selectable(this, tr("Error"), updateTask.failReason(), QMessageBox::Warning)->show(); - } -} - void MainWindow::onCatToggled(bool state) { setCatBackground(state); APPLICATION->settings()->set("TheCat", state); } -namespace { -template <typename T> -T non_stupid_abs(T in) -{ - if (in < 0) - return -in; - return in; -} -} - void MainWindow::setCatBackground(bool enabled) { - if (enabled) - { - QDateTime now = QDateTime::currentDateTime(); - QDateTime birthday(QDate(now.date().year(), 11, 30), QTime(0, 0)); - QDateTime xmas(QDate(now.date().year(), 12, 25), QTime(0, 0)); - QDateTime halloween(QDate(now.date().year(), 10, 31), QTime(0, 0)); - QString cat = APPLICATION->settings()->get("BackgroundCat").toString(); - if (non_stupid_abs(now.daysTo(xmas)) <= 4) { - cat += "-xmas"; - } else if (non_stupid_abs(now.daysTo(halloween)) <= 4) { - cat += "-spooky"; - } else if (non_stupid_abs(now.daysTo(birthday)) <= 12) { - cat += "-bday"; - } + if (enabled) { view->setStyleSheet(QString(R"( InstanceView { @@ -1687,10 +898,8 @@ InstanceView background-repeat: none; background-color:palette(base); })") - .arg(cat)); - } - else - { + .arg(ThemeManager::getCatImage())); + } else { view->setStyleSheet(QString()); } } @@ -1812,10 +1021,12 @@ void MainWindow::on_actionAddInstance_triggered() addInstance(); } -void MainWindow::droppedURLs(QList<QUrl> urls) +void MainWindow::processURLs(QList<QUrl> urls) { // NOTE: This loop only processes one dropped file! for (auto& url : urls) { + qDebug() << "Processing" << url; + // The isLocalFile() check below doesn't work as intended without an explicit scheme. if (url.scheme().isEmpty()) url.setScheme("file"); @@ -1825,31 +1036,50 @@ void MainWindow::droppedURLs(QList<QUrl> urls) break; } - auto localFileName = url.toLocalFile(); + auto localFileName = QDir::toNativeSeparators(url.toLocalFile()) ; QFileInfo localFileInfo(localFileName); - bool isResourcePack = ResourcePackUtils::validate(localFileInfo); - bool isTexturePack = TexturePackUtils::validate(localFileInfo); + auto type = ResourceUtils::identify(localFileInfo); - if (!isResourcePack && !isTexturePack) { // probably instance/modpack + if (ResourceUtils::ValidResourceTypes.count(type) == 0) { // probably instance/modpack addInstance(localFileName); - break; + continue; } - ImportResourcePackDialog dlg(this); + ImportResourceDialog dlg(localFileName, type, this); if (dlg.exec() != QDialog::Accepted) - break; + continue; - qDebug() << "Adding resource/texture pack" << localFileName << "to" << dlg.selectedInstanceKey; + qDebug() << "Adding resource" << 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; + + switch (type) { + case PackedResourceType::ResourcePack: + minecraftInst->resourcePackList()->installResource(localFileName); + break; + case PackedResourceType::TexturePack: + minecraftInst->texturePackList()->installResource(localFileName); + break; + case PackedResourceType::DataPack: + qWarning() << "Importing of Data Packs not supported at this time. Ignoring" << localFileName; + break; + case PackedResourceType::Mod: + minecraftInst->loaderModList()->installMod(localFileName); + break; + case PackedResourceType::ShaderPack: + minecraftInst->shaderPackList()->installResource(localFileName); + break; + case PackedResourceType::WorldSave: + minecraftInst->worldList()->installWorld(localFileInfo); + break; + case PackedResourceType::UNKNOWN: + default: + qDebug() << "Can't Identify" << localFileName << "Ignoring it."; + break; + } } } @@ -1880,7 +1110,7 @@ void MainWindow::on_actionChangeInstIcon_triggered() m_selectedInstance->setIconKey(dlg.selectedIconKey); auto icon = APPLICATION->icons()->getIcon(dlg.selectedIconKey); ui->actionChangeInstIcon->setIcon(icon); - ui->changeIconButton->setIcon(icon); + changeIconButton->setIcon(icon); } } @@ -1890,7 +1120,7 @@ void MainWindow::iconUpdated(QString icon) { auto icon = APPLICATION->icons()->getIcon(m_currentInstIcon); ui->actionChangeInstIcon->setIcon(icon); - ui->changeIconButton->setIcon(icon); + changeIconButton->setIcon(icon); } } @@ -1899,7 +1129,7 @@ void MainWindow::updateInstanceToolIcon(QString new_icon) m_currentInstIcon = new_icon; auto icon = APPLICATION->icons()->getIcon(m_currentInstIcon); ui->actionChangeInstIcon->setIcon(icon); - ui->changeIconButton->setIcon(icon); + changeIconButton->setIcon(icon); } void MainWindow::setSelectedInstanceById(const QString &id) @@ -1985,8 +1215,7 @@ void MainWindow::checkForUpdates() { if(BuildConfig.UPDATER_ENABLED) { - auto updater = APPLICATION->updateChecker(); - updater->checkForUpdate(APPLICATION->settings()->get("UpdateChannel").toString(), true); + APPLICATION->triggerUpdateCheck(); } else { @@ -2079,6 +1308,10 @@ void MainWindow::newsButtonClicked() news_dialog.exec(); } +void MainWindow::onCatChanged(int) { + setCatBackground(APPLICATION->settings()->get("TheCat").toBool()); +} + void MainWindow::on_actionAbout_triggered() { AboutDialog dialog(this); @@ -2093,21 +1326,37 @@ void MainWindow::on_actionDeleteInstance_triggered() auto id = m_selectedInstance->id(); - 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(); + auto response = CustomMessageBox::selectable(this, tr("Confirm Deletion"), + tr("You are about to delete \"%1\".\n" + "This may be permanent and will completely delete the instance.\n\n" + "Are 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()); + if (response != QMessageBox::Yes) + return; + + auto linkedInstances = APPLICATION->instances()->getLinkedInstancesById(id); + if (!linkedInstances.empty()) { + response = CustomMessageBox::selectable( + this, tr("There are linked instances"), + tr("The following instance(s) might reference files in this instance:\n\n" + "%1\n\n" + "Deleting it could break the other instance(s), \n\n" + "Do you wish to proceed?", nullptr, linkedInstances.count()).arg(linkedInstances.join("\n")), + QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No, QMessageBox::No + )->exec(); + if (response != QMessageBox::Yes) return; - } + } - APPLICATION->instances()->deleteInstance(id); + if (APPLICATION->instances()->trashInstance(id)) { + ui->actionUndoTrashInstance->setEnabled(APPLICATION->instances()->trashedSomething()); + return; } + + APPLICATION->instances()->deleteInstance(id); } void MainWindow::on_actionExportInstance_triggered() @@ -2141,6 +1390,7 @@ void MainWindow::closeEvent(QCloseEvent *event) // Save the window state and geometry. APPLICATION->settings()->set("MainWindowState", saveState().toBase64()); APPLICATION->settings()->set("MainWindowGeometry", saveGeometry().toBase64()); + instanceToolbarSetting->set(ui->instanceToolBar->getVisibilityState()); event->accept(); emit isClosing(); } @@ -2252,7 +1502,7 @@ void MainWindow::on_actionCreateInstanceShortcut_triggered() } QString iconPath = FS::PathCombine(m_selectedInstance->instanceRoot(), "icon.png"); - + QFile iconFile(iconPath); if (!iconFile.open(QFile::WriteOnly)) { @@ -2261,7 +1511,7 @@ void MainWindow::on_actionCreateInstanceShortcut_triggered() } bool success = icon->icon().pixmap(64, 64).save(&iconFile, "PNG"); iconFile.close(); - + if (!success) { iconFile.remove(); @@ -2302,7 +1552,7 @@ void MainWindow::on_actionCreateInstanceShortcut_triggered() } 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"); @@ -2325,7 +1575,7 @@ void MainWindow::on_actionCreateInstanceShortcut_triggered() 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)) { @@ -2374,7 +1624,7 @@ void MainWindow::instanceChanged(const QModelIndex ¤t, const QModelIndex & if (m_selectedInstance) { ui->instanceToolBar->setEnabled(true); - ui->setInstanceActionsEnabled(true); + setInstanceActionsEnabled(true); ui->actionLaunchInstance->setEnabled(m_selectedInstance->canLaunch()); ui->actionLaunchInstanceOffline->setEnabled(m_selectedInstance->canLaunch()); ui->actionLaunchInstanceDemo->setEnabled(m_selectedInstance->canLaunch()); @@ -2387,7 +1637,7 @@ void MainWindow::instanceChanged(const QModelIndex ¤t, const QModelIndex & ui->actionKillInstance->setEnabled(m_selectedInstance->isRunning()); ui->actionExportInstance->setEnabled(m_selectedInstance->canExport()); - ui->renameButton->setText(m_selectedInstance->name()); + renameButton->setText(m_selectedInstance->name()); m_statusLeft->setText(m_selectedInstance->getStatusbarDescription()); updateStatusCenter(); updateInstanceToolIcon(m_selectedInstance->iconKey()); @@ -2401,7 +1651,7 @@ void MainWindow::instanceChanged(const QModelIndex ¤t, const QModelIndex & else { ui->instanceToolBar->setEnabled(false); - ui->setInstanceActionsEnabled(false); + setInstanceActionsEnabled(false); ui->actionLaunchInstance->setEnabled(false); ui->actionLaunchInstanceOffline->setEnabled(false); ui->actionLaunchInstanceDemo->setEnabled(false); @@ -2434,9 +1684,9 @@ void MainWindow::selectionBad() statusBar()->clearMessage(); ui->instanceToolBar->setEnabled(false); - ui->setInstanceActionsEnabled(false); + setInstanceActionsEnabled(false); updateToolsMenu(); - ui->renameButton->setText(tr("Rename Instance")); + renameButton->setText(tr("Rename Instance")); updateInstanceToolIcon("grass"); // ...and then see if we can enable the previously selected instance @@ -2491,6 +1741,18 @@ void MainWindow::updateStatusCenter() m_statusCenter->setText(tr("Total playtime: %1").arg(Time::prettifyDuration(timePlayed))); } } +// "Instance actions" are actions that require an instance to be selected (i.e. "new instance" is not here) +// Actions that also require other conditions (e.g. a running instance) won't be changed. +void MainWindow::setInstanceActionsEnabled(bool enabled) +{ + ui->actionEditInstance->setEnabled(enabled); + ui->actionChangeInstGroup->setEnabled(enabled); + ui->actionViewSelectedInstFolder->setEnabled(enabled); + ui->actionExportInstance->setEnabled(enabled); + ui->actionDeleteInstance->setEnabled(enabled); + ui->actionCopyInstance->setEnabled(enabled); + ui->actionCreateInstanceShortcut->setEnabled(enabled); +} void MainWindow::refreshCurrentInstance(bool running) { diff --git a/launcher/ui/MainWindow.h b/launcher/ui/MainWindow.h index f96f641d..3a42c34e 100644 --- a/launcher/ui/MainWindow.h +++ b/launcher/ui/MainWindow.h @@ -48,7 +48,6 @@ #include "BaseInstance.h" #include "minecraft/auth/MinecraftAccount.h" #include "net/NetJob.h" -#include "updater/GoUpdate.h" class LaunchController; class NewsChecker; @@ -61,13 +60,16 @@ class BaseProfilerFactory; class InstanceView; class KonamiCode; class InstanceTask; +class LabeledToolButton; +namespace Ui +{ +class MainWindow; +} class MainWindow : public QMainWindow { Q_OBJECT - class Ui; - public: explicit MainWindow(QWidget *parent = 0); ~MainWindow(); @@ -80,7 +82,7 @@ public: void updatesAllowedChanged(bool allowed); - void droppedURLs(QList<QUrl> urls); + void processURLs(QList<QUrl> urls); signals: void isClosing(); @@ -90,6 +92,8 @@ protected: private slots: void onCatToggled(bool); + void onCatChanged(int); + void on_actionAbout_triggered(); void on_actionAddInstance_triggered(); @@ -105,10 +109,6 @@ private slots: void on_actionChangeInstGroup_triggered(); void on_actionChangeInstIcon_triggered(); - void on_changeIconButton_clicked(bool) - { - on_actionChangeInstIcon_triggered(); - } void on_actionViewInstanceFolder_triggered(); @@ -154,10 +154,6 @@ private slots: void on_actionExportInstance_triggered(); void on_actionRenameInstance_triggered(); - void on_renameButton_clicked(bool) - { - on_actionRenameInstance_triggered(); - } void on_actionEditInstance_triggered(); @@ -190,10 +186,6 @@ private slots: void startTask(Task *task); - void updateAvailable(GoUpdate::Status status); - - void updateNotAvailable(); - void defaultAccountChanged(); void changeActiveAccount(); @@ -202,10 +194,6 @@ private slots: void updateNewsLabel(); - /*! - * Runs the DownloadTask and installs updates. - */ - void downloadUpdates(GoUpdate::Status status); void konamiTriggered(); @@ -228,24 +216,27 @@ private: void updateInstanceToolIcon(QString new_icon); void setSelectedInstanceById(const QString &id); void updateStatusCenter(); + void setInstanceActionsEnabled(bool enabled); void runModalTask(Task *task); void instanceFromInstanceTask(InstanceTask *task); void finalizeInstance(InstancePtr inst); private: - std::unique_ptr<Ui> ui; - + Ui::MainWindow *ui; // these are managed by Qt's memory management model! InstanceView *view = nullptr; InstanceProxyModel *proxymodel = nullptr; QToolButton *newsLabel = nullptr; QLabel *m_statusLeft = nullptr; QLabel *m_statusCenter = nullptr; - QMenu *accountMenu = nullptr; - QToolButton *accountMenuButton = nullptr; + LabeledToolButton *changeIconButton = nullptr; + LabeledToolButton *renameButton = nullptr; + QToolButton *helpMenuButton = nullptr; KonamiCode * secretEventFilter = nullptr; + std::shared_ptr<Setting> instanceToolbarSetting = nullptr; + unique_qobject_ptr<NewsChecker> m_newsChecker; InstancePtr m_selectedInstance; @@ -254,4 +245,3 @@ private: // managed by the application object Task *m_versionLoadTask = nullptr; }; - diff --git a/launcher/ui/MainWindow.ui b/launcher/ui/MainWindow.ui new file mode 100644 index 00000000..2b6a10b1 --- /dev/null +++ b/launcher/ui/MainWindow.ui @@ -0,0 +1,690 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>MainWindow</class> + <widget class="QMainWindow" name="MainWindow"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>800</width> + <height>600</height> + </rect> + </property> + <widget class="QWidget" name="centralWidget"> + <layout class="QHBoxLayout" name="horizontalLayout"> + <property name="spacing"> + <number>0</number> + </property> + <property name="leftMargin"> + <number>0</number> + </property> + <property name="topMargin"> + <number>0</number> + </property> + <property name="rightMargin"> + <number>0</number> + </property> + <property name="bottomMargin"> + <number>0</number> + </property> + </layout> + </widget> + <widget class="QStatusBar" name="statusBar"/> + <widget class="QToolBar" name="mainToolBar"> + <property name="windowTitle"> + <string>Main Toolbar</string> + </property> + <property name="allowedAreas"> + <set>Qt::BottomToolBarArea|Qt::TopToolBarArea</set> + </property> + <property name="toolButtonStyle"> + <enum>Qt::ToolButtonTextBesideIcon</enum> + </property> + <property name="floatable"> + <bool>false</bool> + </property> + <attribute name="toolBarArea"> + <enum>TopToolBarArea</enum> + </attribute> + <attribute name="toolBarBreak"> + <bool>false</bool> + </attribute> + <addaction name="actionAddInstance"/> + <addaction name="separator"/> + <addaction name="actionFoldersButton"/> + <addaction name="actionSettings"/> + <addaction name="actionHelpButton"/> + <addaction name="actionCheckUpdate"/> + <addaction name="separator"/> + <addaction name="actionCAT"/> + <addaction name="actionAccountsButton"/> + </widget> + <widget class="QToolBar" name="newsToolBar"> + <property name="windowTitle"> + <string>News Toolbar</string> + </property> + <property name="allowedAreas"> + <set>Qt::BottomToolBarArea|Qt::TopToolBarArea</set> + </property> + <property name="iconSize"> + <size> + <width>16</width> + <height>16</height> + </size> + </property> + <property name="toolButtonStyle"> + <enum>Qt::ToolButtonTextBesideIcon</enum> + </property> + <property name="floatable"> + <bool>false</bool> + </property> + <attribute name="toolBarArea"> + <enum>BottomToolBarArea</enum> + </attribute> + <attribute name="toolBarBreak"> + <bool>false</bool> + </attribute> + <addaction name="actionMoreNews"/> + </widget> + <widget class="WideBar" name="instanceToolBar"> + <property name="windowTitle"> + <string>Instance Toolbar</string> + </property> + <property name="allowedAreas"> + <set>Qt::LeftToolBarArea|Qt::RightToolBarArea</set> + </property> + <property name="iconSize"> + <size> + <width>16</width> + <height>16</height> + </size> + </property> + <property name="toolButtonStyle"> + <enum>Qt::ToolButtonTextBesideIcon</enum> + </property> + <property name="floatable"> + <bool>false</bool> + </property> + <property name="useDefaultAction" stdset="0"> + <bool>true</bool> + </property> + <attribute name="toolBarArea"> + <enum>RightToolBarArea</enum> + </attribute> + <attribute name="toolBarBreak"> + <bool>false</bool> + </attribute> + <addaction name="actionLaunchInstance"/> + <addaction name="actionKillInstance"/> + <addaction name="separator"/> + <addaction name="actionEditInstance"/> + <addaction name="actionChangeInstGroup"/> + <addaction name="actionViewSelectedInstFolder"/> + <addaction name="actionExportInstance"/> + <addaction name="actionCopyInstance"/> + <addaction name="actionDeleteInstance"/> + <addaction name="actionCreateInstanceShortcut"/> + </widget> + <widget class="QMenuBar" name="menuBar"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>800</width> + <height>20</height> + </rect> + </property> + <widget class="QMenu" name="fileMenu"> + <property name="title"> + <string>&File</string> + </property> + <property name="toolTipsVisible"> + <bool>true</bool> + </property> + <addaction name="actionAddInstance"/> + <addaction name="separator"/> + <addaction name="actionLaunchInstance"/> + <addaction name="actionKillInstance"/> + <addaction name="separator"/> + <addaction name="actionEditInstance"/> + <addaction name="actionChangeInstGroup"/> + <addaction name="actionViewSelectedInstFolder"/> + <addaction name="actionExportInstance"/> + <addaction name="actionCopyInstance"/> + <addaction name="actionDeleteInstance"/> + <addaction name="actionCreateInstanceShortcut"/> + <addaction name="separator"/> + <addaction name="actionSettings"/> + <addaction name="actionCloseWindow"/> + </widget> + <widget class="QMenu" name="editMenu"> + <property name="title"> + <string>&Edit</string> + </property> + <property name="toolTipsVisible"> + <bool>true</bool> + </property> + <addaction name="actionUndoTrashInstance"/> + </widget> + <widget class="QMenu" name="viewMenu"> + <property name="title"> + <string>&View</string> + </property> + <property name="toolTipsVisible"> + <bool>true</bool> + </property> + <addaction name="actionChangeTheme"/> + <addaction name="separator"/> + <addaction name="actionCAT"/> + <addaction name="actionLockToolbars"/> + <addaction name="separator"/> + </widget> + <widget class="QMenu" name="foldersMenu"> + <property name="title"> + <string>F&olders</string> + </property> + <property name="toolTipsVisible"> + <bool>true</bool> + </property> + <addaction name="actionViewInstanceFolder"/> + <addaction name="actionViewCentralModsFolder"/> + </widget> + <widget class="QMenu" name="accountsMenu"> + <property name="title"> + <string>&Accounts</string> + </property> + </widget> + <widget class="QMenu" name="helpMenu"> + <property name="title"> + <string>&Help</string> + </property> + <property name="toolTipsVisible"> + <bool>true</bool> + </property> + <addaction name="actionClearMetadata"/> + <addaction name="actionReportBug"/> + <addaction name="actionAddToPATH"/> + <addaction name="separator"/> + <addaction name="actionMATRIX"/> + <addaction name="actionDISCORD"/> + <addaction name="actionREDDIT"/> + <addaction name="separator"/> + <addaction name="actionMoreNews"/> + <addaction name="actionCheckUpdate"/> + <addaction name="actionOpenWiki"/> + <addaction name="actionAbout"/> + </widget> + <addaction name="fileMenu"/> + <addaction name="editMenu"/> + <addaction name="viewMenu"/> + <addaction name="foldersMenu"/> + <addaction name="accountsMenu"/> + <addaction name="helpMenu"/> + </widget> + <action name="actionMoreNews"> + <property name="icon"> + <iconset theme="news"> + <normaloff>.</normaloff>.</iconset> + </property> + <property name="text"> + <string>More news...</string> + </property> + <property name="toolTip"> + <string>Open the development blog to read more news about %1.</string> + </property> + </action> + <action name="actionCAT"> + <property name="checkable"> + <bool>true</bool> + </property> + <property name="icon"> + <iconset theme="cat"> + <normaloff>.</normaloff>.</iconset> + </property> + <property name="text"> + <string>&Meow</string> + </property> + <property name="toolTip"> + <string>It's a fluffy kitty :3</string> + </property> + </action> + <action name="actionLockToolbars"> + <property name="checkable"> + <bool>true</bool> + </property> + <property name="text"> + <string>Lock Toolbars</string> + </property> + </action> + <action name="actionUndoTrashInstance"> + <property name="enabled"> + <bool>false</bool> + </property> + <property name="text"> + <string>&Undo Last Instance Deletion</string> + </property> + </action> + <action name="actionAddInstance"> + <property name="icon"> + <iconset theme="new"> + <normaloff>.</normaloff>.</iconset> + </property> + <property name="text"> + <string>Add Instanc&e...</string> + </property> + <property name="toolTip"> + <string>Add a new instance.</string> + </property> + </action> + <action name="actionCheckUpdate"> + <property name="icon"> + <iconset theme="checkupdate"> + <normaloff>.</normaloff>.</iconset> + </property> + <property name="text"> + <string>&Update...</string> + </property> + <property name="toolTip"> + <string>Check for new updates for %1.</string> + </property> + <property name="menuRole"> + <enum>QAction::ApplicationSpecificRole</enum> + </property> + </action> + <action name="actionSettings"> + <property name="icon"> + <iconset theme="settings"> + <normaloff>.</normaloff>.</iconset> + </property> + <property name="text"> + <string>Setti&ngs...</string> + </property> + <property name="toolTip"> + <string>Change settings.</string> + </property> + <property name="menuRole"> + <enum>QAction::PreferencesRole</enum> + </property> + </action> + <action name="actionManageAccounts"> + <property name="icon"> + <iconset theme="accounts"> + <normaloff>.</normaloff>.</iconset> + </property> + <property name="text"> + <string>&Manage Accounts...</string> + </property> + </action> + <action name="actionLaunchInstance"> + <property name="icon"> + <iconset theme="launch"> + <normaloff>.</normaloff>.</iconset> + </property> + <property name="text"> + <string>&Launch</string> + </property> + <property name="toolTip"> + <string>Launch the selected instance.</string> + </property> + </action> + <action name="actionKillInstance"> + <property name="icon"> + <iconset theme="status-bad"> + <normaloff>.</normaloff>.</iconset> + </property> + <property name="text"> + <string>&Kill</string> + </property> + <property name="toolTip"> + <string>Kill the running instance.</string> + </property> + <property name="shortcut"> + <string>Ctrl+K</string> + </property> + </action> + <action name="actionRenameInstance"> + <property name="icon"> + <iconset theme="rename"> + <normaloff>.</normaloff>.</iconset> + </property> + <property name="text"> + <string>Rename</string> + </property> + <property name="toolTip"> + <string>Rename the selected instance.</string> + </property> + </action> + <action name="actionChangeInstGroup"> + <property name="icon"> + <iconset theme="tag"> + <normaloff>.</normaloff>.</iconset> + </property> + <property name="text"> + <string>&Change Group...</string> + </property> + <property name="toolTip"> + <string>Change the selected instance's group.</string> + </property> + <property name="shortcut"> + <string>Ctrl+G</string> + </property> + </action> + <action name="actionChangeInstIcon"> + <property name="text"> + <string>Change Icon</string> + </property> + <property name="toolTip"> + <string>Change the selected instance's icon.</string> + </property> + </action> + <action name="actionEditInstance"> + <property name="icon"> + <iconset theme="settings"> + <normaloff>.</normaloff>.</iconset> + </property> + <property name="text"> + <string>&Edit...</string> + </property> + <property name="toolTip"> + <string>Change the instance settings, mods and versions.</string> + </property> + <property name="shortcut"> + <string>Ctrl+I</string> + </property> + </action> + <action name="actionViewSelectedInstFolder"> + <property name="icon"> + <iconset theme="viewfolder"> + <normaloff>.</normaloff>.</iconset> + </property> + <property name="text"> + <string>&Folder</string> + </property> + <property name="toolTip"> + <string>Open the selected instance's root folder in a file browser.</string> + </property> + </action> + <action name="actionDeleteInstance"> + <property name="icon"> + <iconset theme="delete"> + <normaloff>.</normaloff>.</iconset> + </property> + <property name="text"> + <string>Dele&te</string> + </property> + <property name="toolTip"> + <string>Delete the selected instance.</string> + </property> + <property name="autoRepeat"> + <bool>false</bool> + </property> + </action> + <action name="actionCopyInstance"> + <property name="icon"> + <iconset theme="copy"> + <normaloff>.</normaloff>.</iconset> + </property> + <property name="text"> + <string>Cop&y...</string> + </property> + <property name="toolTip"> + <string>Copy the selected instance.</string> + </property> + <property name="shortcut"> + <string>Ctrl+D</string> + </property> + </action> + <action name="actionLaunchInstanceOffline"> + <property name="text"> + <string>Launch &Offline</string> + </property> + <property name="toolTip"> + <string>Launch the selected instance in offline mode.</string> + </property> + </action> + <action name="actionLaunchInstanceDemo"> + <property name="text"> + <string>Launch &Demo</string> + </property> + <property name="toolTip"> + <string>Launch the selected instance in demo mode.</string> + </property> + </action> + <action name="actionExportInstance"> + <property name="icon"> + <iconset theme="export"> + <normaloff>.</normaloff>.</iconset> + </property> + <property name="text"> + <string>E&xport...</string> + </property> + <property name="toolTip"> + <string>Export the selected instance as a zip file.</string> + </property> + <property name="shortcut"> + <string>Ctrl+E</string> + </property> + </action> + <action name="actionCreateInstanceShortcut"> + <property name="icon"> + <iconset theme="shortcut"> + <normaloff>.</normaloff>.</iconset> + </property> + <property name="text"> + <string>Create Shortcut</string> + </property> + <property name="toolTip"> + <string>Creates a shortcut on your desktop to launch the selected instance.</string> + </property> + </action> + <action name="actionNoAccountsAdded"> + <property name="icon"> + <iconset theme="noaccount"> + <normaloff>.</normaloff>.</iconset> + </property> + <property name="text"> + <string>No accounts added!</string> + </property> + </action> + <action name="actionNoDefaultAccount"> + <property name="checkable"> + <bool>true</bool> + </property> + <property name="icon"> + <iconset theme="noaccount"> + <normaloff>.</normaloff>.</iconset> + </property> + <property name="text"> + <string>No Default Account</string> + </property> + <property name="shortcut"> + <string>Ctrl+0</string> + </property> + </action> + <action name="actionCloseWindow"> + <property name="icon"> + <iconset theme="status-bad"> + <normaloff>.</normaloff>.</iconset> + </property> + <property name="text"> + <string>Close &Window</string> + </property> + <property name="toolTip"> + <string>Close the current window</string> + </property> + <property name="menuRole"> + <enum>QAction::QuitRole</enum> + </property> + </action> + <action name="actionViewInstanceFolder"> + <property name="icon"> + <iconset theme="viewfolder"> + <normaloff>.</normaloff>.</iconset> + </property> + <property name="text"> + <string>&View Instance Folder</string> + </property> + <property name="toolTip"> + <string>Open the instance folder in a file browser.</string> + </property> + </action> + <action name="actionViewCentralModsFolder"> + <property name="icon"> + <iconset theme="centralmods"> + <normaloff>.</normaloff>.</iconset> + </property> + <property name="text"> + <string>View &Central Mods Folder</string> + </property> + <property name="toolTip"> + <string>Open the central mods folder in a file browser.</string> + </property> + </action> + <action name="actionChangeTheme"> + <property name="text"> + <string>Themes</string> + </property> + </action> + <action name="actionReportBug"> + <property name="icon"> + <iconset theme="bug"> + <normaloff>.</normaloff>.</iconset> + </property> + <property name="text"> + <string>Report a &Bug...</string> + </property> + <property name="toolTip"> + <string>Open the bug tracker to report a bug with %1.</string> + </property> + </action> + <action name="actionDISCORD"> + <property name="icon"> + <iconset theme="discord"> + <normaloff>.</normaloff>.</iconset> + </property> + <property name="text"> + <string>&Discord Guild</string> + </property> + <property name="toolTip"> + <string>Open %1 Discord guild.</string> + </property> + </action> + <action name="actionMATRIX"> + <property name="icon"> + <iconset theme="matrix"> + <normaloff>.</normaloff>.</iconset> + </property> + <property name="text"> + <string>&Matrix Space</string> + </property> + <property name="toolTip"> + <string>Open %1 Matrix space.</string> + </property> + </action> + <action name="actionREDDIT"> + <property name="icon"> + <iconset theme="reddit-alien"> + <normaloff>.</normaloff>.</iconset> + </property> + <property name="text"> + <string>Sub&reddit</string> + </property> + <property name="toolTip"> + <string>Open %1 subreddit.</string> + </property> + </action> + <action name="actionAbout"> + <property name="icon"> + <iconset theme="about"> + <normaloff>.</normaloff>.</iconset> + </property> + <property name="text"> + <string>&About %1</string> + </property> + <property name="toolTip"> + <string>View information about %1.</string> + </property> + <property name="menuRole"> + <enum>QAction::AboutRole</enum> + </property> + </action> + <action name="actionClearMetadata"> + <property name="icon"> + <iconset theme="refresh"> + <normaloff>.</normaloff>.</iconset> + </property> + <property name="text"> + <string>&Clear Metadata Cache</string> + </property> + <property name="toolTip"> + <string>Clear cached metadata</string> + </property> + </action> + <action name="actionAddToPATH"> + <property name="icon"> + <iconset theme="custom-commands"> + <normaloff>.</normaloff>.</iconset> + </property> + <property name="text"> + <string>Install to &PATH</string> + </property> + <property name="toolTip"> + <string>Install a %1 symlink to /usr/local/bin</string> + </property> + </action> + <action name="actionFoldersButton"> + <property name="icon"> + <iconset theme="viewfolder"> + <normaloff>.</normaloff>.</iconset> + </property> + <property name="text"> + <string>Folders</string> + </property> + <property name="toolTip"> + <string>Open one of the folders shared between instances.</string> + </property> + </action> + <action name="actionHelpButton"> + <property name="icon"> + <iconset theme="help"> + <normaloff>.</normaloff>.</iconset> + </property> + <property name="text"> + <string>Help</string> + </property> + <property name="toolTip"> + <string>Get help with %1 or Minecraft.</string> + </property> + </action> + <action name="actionAccountsButton"> + <property name="icon"> + <iconset theme="noaccount"> + <normaloff>.</normaloff>.</iconset> + </property> + <property name="text"> + <string>Accounts</string> + </property> + </action> + <action name="actionOpenWiki"> + <property name="icon"> + <iconset theme="help"> + <normaloff>.</normaloff>.</iconset> + </property> + <property name="text"> + <string>%1 &Help</string> + </property> + <property name="toolTip"> + <string>Open the %1 wiki</string> + </property> + </action> + </widget> + <customwidgets> + <customwidget> + <class>WideBar</class> + <extends>QToolBar</extends> + <header>ui/widgets/WideBar.h</header> + </customwidget> + </customwidgets> + <resources/> + <connections/> +</ui> diff --git a/launcher/ui/WinDarkmode.cpp b/launcher/ui/WinDarkmode.cpp deleted file mode 100644 index eac68e4f..00000000 --- a/launcher/ui/WinDarkmode.cpp +++ /dev/null @@ -1,32 +0,0 @@ -#include <QWidget> - -#include "WinDarkmode.h" - -namespace WinDarkmode { - -/* See https://github.com/statiolake/neovim-qt/commit/da8eaba7f0e38b6b51f3bacd02a8cc2d1f7a34d8 */ -void setDarkWinTitlebar(WId winid, bool darkmode) -{ - HWND hwnd = reinterpret_cast<HWND>(winid); - BOOL dark = (BOOL) darkmode; - - HMODULE hUxtheme = LoadLibraryExW(L"uxtheme.dll", NULL, LOAD_LIBRARY_SEARCH_SYSTEM32); - HMODULE hUser32 = GetModuleHandleW(L"user32.dll"); - fnAllowDarkModeForWindow AllowDarkModeForWindow - = reinterpret_cast<fnAllowDarkModeForWindow>(GetProcAddress(hUxtheme, MAKEINTRESOURCEA(133))); - fnSetPreferredAppMode SetPreferredAppMode - = reinterpret_cast<fnSetPreferredAppMode>(GetProcAddress(hUxtheme, MAKEINTRESOURCEA(135))); - fnSetWindowCompositionAttribute SetWindowCompositionAttribute - = reinterpret_cast<fnSetWindowCompositionAttribute>(GetProcAddress(hUser32, "SetWindowCompositionAttribute")); - - SetPreferredAppMode(AllowDark); - AllowDarkModeForWindow(hwnd, dark); - WINDOWCOMPOSITIONATTRIBDATA data = { - WCA_USEDARKMODECOLORS, - &dark, - sizeof(dark) - }; - SetWindowCompositionAttribute(hwnd, &data); -} - -} diff --git a/launcher/ui/WinDarkmode.h b/launcher/ui/WinDarkmode.h deleted file mode 100644 index 5b567c6b..00000000 --- a/launcher/ui/WinDarkmode.h +++ /dev/null @@ -1,60 +0,0 @@ -#pragma once - -#include <windows.h> -#include <dwmapi.h> - - -namespace WinDarkmode { - -void setDarkWinTitlebar(WId winid, bool darkmode); - -enum PreferredAppMode { - Default, - AllowDark, - ForceDark, - ForceLight, - Max -}; - -enum WINDOWCOMPOSITIONATTRIB { - WCA_UNDEFINED = 0, - WCA_NCRENDERING_ENABLED = 1, - WCA_NCRENDERING_POLICY = 2, - WCA_TRANSITIONS_FORCEDISABLED = 3, - WCA_ALLOW_NCPAINT = 4, - WCA_CAPTION_BUTTON_BOUNDS = 5, - WCA_NONCLIENT_RTL_LAYOUT = 6, - WCA_FORCE_ICONIC_REPRESENTATION = 7, - WCA_EXTENDED_FRAME_BOUNDS = 8, - WCA_HAS_ICONIC_BITMAP = 9, - WCA_THEME_ATTRIBUTES = 10, - WCA_NCRENDERING_EXILED = 11, - WCA_NCADORNMENTINFO = 12, - WCA_EXCLUDED_FROM_LIVEPREVIEW = 13, - WCA_VIDEO_OVERLAY_ACTIVE = 14, - WCA_FORCE_ACTIVEWINDOW_APPEARANCE = 15, - WCA_DISALLOW_PEEK = 16, - WCA_CLOAK = 17, - WCA_CLOAKED = 18, - WCA_ACCENT_POLICY = 19, - WCA_FREEZE_REPRESENTATION = 20, - WCA_EVER_UNCLOAKED = 21, - WCA_VISUAL_OWNER = 22, - WCA_HOLOGRAPHIC = 23, - WCA_EXCLUDED_FROM_DDA = 24, - WCA_PASSIVEUPDATEMODE = 25, - WCA_USEDARKMODECOLORS = 26, - WCA_LAST = 27 -}; - -struct WINDOWCOMPOSITIONATTRIBDATA { - WINDOWCOMPOSITIONATTRIB Attrib; - PVOID pvData; - SIZE_T cbData; -}; - -using fnAllowDarkModeForWindow = BOOL (WINAPI *)(HWND hWnd, BOOL allow); -using fnSetPreferredAppMode = PreferredAppMode (WINAPI *)(PreferredAppMode appMode); -using fnSetWindowCompositionAttribute = BOOL (WINAPI *)(HWND hwnd, WINDOWCOMPOSITIONATTRIBDATA *); - -} diff --git a/launcher/ui/dialogs/AboutDialog.cpp b/launcher/ui/dialogs/AboutDialog.cpp index a36e4a3d..76e3d8ed 100644 --- a/launcher/ui/dialogs/AboutDialog.cpp +++ b/launcher/ui/dialogs/AboutDialog.cpp @@ -39,12 +39,11 @@ #include <QIcon> #include "Application.h" #include "BuildConfig.h" +#include "Markdown.h" #include <net/NetJob.h> #include <qobject.h> -#include "HoeDown.h" - namespace { QString getLink(QString link, QString name) { return QString("<<a href='%1'>%2</a>>").arg(link).arg(name); @@ -114,10 +113,9 @@ QString getCreditsHtml() QString getLicenseHtml() { - HoeDown hoedown; QFile dataFile(":/documents/COPYING.md"); dataFile.open(QIODevice::ReadOnly); - QString output = hoedown.process(dataFile.readAll()); + QString output = markdownToHTML(dataFile.readAll()); return output; } diff --git a/launcher/ui/dialogs/BlockedModsDialog.cpp b/launcher/ui/dialogs/BlockedModsDialog.cpp index 8b49bd1a..fdfae597 100644 --- a/launcher/ui/dialogs/BlockedModsDialog.cpp +++ b/launcher/ui/dialogs/BlockedModsDialog.cpp @@ -32,9 +32,12 @@ #include <QDebug> #include <QDesktopServices> #include <QDialogButtonBox> +#include <QDir> +#include <QDirIterator> #include <QDragEnterEvent> #include <QFileDialog> #include <QFileInfo> +#include <QMimeData> #include <QPushButton> #include <QStandardPaths> @@ -85,11 +88,11 @@ void BlockedModsDialog::dragEnterEvent(QDragEnterEvent* e) void BlockedModsDialog::dropEvent(QDropEvent* e) { for (QUrl& url : e->mimeData()->urls()) { - if (url.scheme().isEmpty()) { // ensure isLocalFile() works correctly + if (url.scheme().isEmpty()) { // ensure isLocalFile() works correctly url.setScheme("file"); } - - if (!url.isLocalFile()) { // can't drop external files here. + + if (!url.isLocalFile()) { // can't drop external files here. continue; } @@ -168,7 +171,7 @@ void BlockedModsDialog::update() } } -/// @brief Signal fired when a watched direcotry has changed +/// @brief Signal fired when a watched directory has changed /// @param path the path to the changed directory void BlockedModsDialog::directoryChanged(QString path) { @@ -180,10 +183,31 @@ void BlockedModsDialog::directoryChanged(QString path) /// @brief add the user downloads folder and the global mods folder to the filesystem watcher void BlockedModsDialog::setupWatch() { - const QString downloadsFolder = QStandardPaths::writableLocation(QStandardPaths::DownloadLocation); + const QString downloadsFolder = APPLICATION->settings()->get("DownloadsDir").toString(); const QString modsFolder = APPLICATION->settings()->get("CentralModsDir").toString(); - m_watcher.addPath(downloadsFolder); - m_watcher.addPath(modsFolder); + const bool downloadsFolderWatchRecursive = APPLICATION->settings()->get("DownloadsDirWatchRecursive").toBool(); + watchPath(downloadsFolder, downloadsFolderWatchRecursive); + watchPath(modsFolder, true); +} + +void BlockedModsDialog::watchPath(QString path, bool watch_recursive) +{ + auto to_watch = QFileInfo(path); + auto to_watch_path = to_watch.canonicalFilePath(); + if (m_watcher.directories().contains(to_watch_path)) + return; // don't watch the same path twice (no loops!) + + qDebug() << "[Blocked Mods Dialog] Adding Watch Path:" << path; + m_watcher.addPath(to_watch_path); + + if (!to_watch.isDir() || !watch_recursive) + return; + + QDirIterator it(to_watch_path, QDir::Filter::Dirs | QDir::Filter::NoDotAndDotDot, QDirIterator::NoIteratorFlags); + while (it.hasNext()) { + QString watch_dir = QDir(it.next()).canonicalPath(); // resolve symlinks and relative paths + watchPath(watch_dir, watch_recursive); + } } /// @brief scan all watched folder @@ -217,7 +241,7 @@ void BlockedModsDialog::scanPath(QString path, bool start_task) } } -/// @brief add a hashing task for the file located at path, add the path to the pending set if the hasing task is already running +/// @brief add a hashing task for the file located at path, add the path to the pending set if the hashing task is already running /// @param path the path to the local file being hashed void BlockedModsDialog::addHashTask(QString path) { @@ -230,7 +254,7 @@ void BlockedModsDialog::addHashTask(QString path) /// @param path the path to the local file being hashed void BlockedModsDialog::buildHashTask(QString path) { - auto hash_task = Hashing::createBlockedModHasher(path, ModPlatform::Provider::FLAME, "sha1"); + auto hash_task = Hashing::createBlockedModHasher(path, ModPlatform::ResourceProvider::FLAME, "sha1"); qDebug() << "[Blocked Mods Dialog] Creating Hash task for path: " << path; @@ -277,11 +301,35 @@ bool BlockedModsDialog::checkValidPath(QString path) { 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; + auto compare = [](QString fsFilename, QString metadataFilename) { + return metadataFilename.compare(fsFilename, Qt::CaseInsensitive) == 0; + }; + + // super lax compare (but not fuzzy) + // convert to lowercase + // convert all speratores to whitespace + // simplify sequence of internal whitespace to a single space + // efectivly compare two strings ignoring all separators and case + auto laxCompare = [](QString fsfilename, QString metadataFilename) { + // allowed character seperators + QList<QChar> allowedSeperators = { '-', '+', '.' , '_'}; + + // copy in lowercase + auto fsName = fsfilename.toLower(); + auto metaName = metadataFilename.toLower(); + + // replace all potential allowed seperatores with whitespace + for (auto sep : allowedSeperators) { + fsName = fsName.replace(sep, ' '); + metaName = metaName.replace(sep, ' '); + } + + // remove extraneous whitespace + fsName = fsName.simplified(); + metaName = metaName.simplified(); + + return fsName.compare(metaName) == 0; }; for (auto& mod : m_mods) { @@ -289,7 +337,7 @@ bool BlockedModsDialog::checkValidPath(QString path) qDebug() << "[Blocked Mods Dialog] Name match found:" << mod.name << "| From path:" << path; return true; } - if (compare(laxFilename, mod.name)) { + if (laxCompare(filename, mod.name)) { qDebug() << "[Blocked Mods Dialog] Lax name match found:" << mod.name << "| From path:" << path; return true; } @@ -324,7 +372,7 @@ void BlockedModsDialog::validateMatchedMods() } } -/// @brief run hash task or mark a pending run if it is already runing +/// @brief run hash task or mark a pending run if it is already running void BlockedModsDialog::runHashTask() { if (!m_hashing_task->isRunning()) { diff --git a/launcher/ui/dialogs/BlockedModsDialog.h b/launcher/ui/dialogs/BlockedModsDialog.h index 014f488a..e3b7c975 100644 --- a/launcher/ui/dialogs/BlockedModsDialog.h +++ b/launcher/ui/dialogs/BlockedModsDialog.h @@ -79,6 +79,7 @@ class BlockedModsDialog : public QDialog { void update(); void directoryChanged(QString path); void setupWatch(); + void watchPath(QString path, bool watch_recursive = false); void scanPaths(); void scanPath(QString path, bool start_task); void addHashTask(QString path); diff --git a/launcher/ui/dialogs/ChooseProviderDialog.cpp b/launcher/ui/dialogs/ChooseProviderDialog.cpp index 89935d9a..83748e1e 100644 --- a/launcher/ui/dialogs/ChooseProviderDialog.cpp +++ b/launcher/ui/dialogs/ChooseProviderDialog.cpp @@ -67,9 +67,9 @@ void ChooseProviderDialog::confirmAll() accept(); } -auto ChooseProviderDialog::getSelectedProvider() const -> ModPlatform::Provider +auto ChooseProviderDialog::getSelectedProvider() const -> ModPlatform::ResourceProvider { - return ModPlatform::Provider(m_providers.checkedId()); + return ModPlatform::ResourceProvider(m_providers.checkedId()); } void ChooseProviderDialog::addProviders() @@ -77,7 +77,7 @@ void ChooseProviderDialog::addProviders() int btn_index = 0; QRadioButton* btn; - for (auto& provider : { ModPlatform::Provider::MODRINTH, ModPlatform::Provider::FLAME }) { + for (auto& provider : { ModPlatform::ResourceProvider::MODRINTH, ModPlatform::ResourceProvider::FLAME }) { btn = new QRadioButton(ProviderCaps.readableName(provider), this); m_providers.addButton(btn, btn_index++); ui->providersLayout->addWidget(btn); diff --git a/launcher/ui/dialogs/ChooseProviderDialog.h b/launcher/ui/dialogs/ChooseProviderDialog.h index 4a3b9f29..be9735b5 100644 --- a/launcher/ui/dialogs/ChooseProviderDialog.h +++ b/launcher/ui/dialogs/ChooseProviderDialog.h @@ -8,7 +8,7 @@ class ChooseProviderDialog; } namespace ModPlatform { -enum class Provider; +enum class ResourceProvider; } class Mod; @@ -24,7 +24,7 @@ class ChooseProviderDialog : public QDialog { bool try_others = false; - ModPlatform::Provider chosen; + ModPlatform::ResourceProvider chosen; }; public: @@ -45,7 +45,7 @@ class ChooseProviderDialog : public QDialog { void addProviders(); void disableInput(); - auto getSelectedProvider() const -> ModPlatform::Provider; + auto getSelectedProvider() const -> ModPlatform::ResourceProvider; private: Ui::ChooseProviderDialog* ui; diff --git a/launcher/ui/dialogs/CopyInstanceDialog.cpp b/launcher/ui/dialogs/CopyInstanceDialog.cpp index 3f5122f6..d75bb5fe 100644 --- a/launcher/ui/dialogs/CopyInstanceDialog.cpp +++ b/launcher/ui/dialogs/CopyInstanceDialog.cpp @@ -37,18 +37,21 @@ #include <QPushButton> #include "Application.h" +#include "BuildConfig.h" #include "CopyInstanceDialog.h" #include "ui_CopyInstanceDialog.h" #include "ui/dialogs/IconPickerDialog.h" -#include "BaseVersion.h" -#include "icons/IconList.h" #include "BaseInstance.h" +#include "BaseVersion.h" +#include "DesktopServices.h" +#include "FileSystem.h" #include "InstanceList.h" +#include "icons/IconList.h" -CopyInstanceDialog::CopyInstanceDialog(InstancePtr original, QWidget *parent) - :QDialog(parent), ui(new Ui::CopyInstanceDialog), m_original(original) +CopyInstanceDialog::CopyInstanceDialog(InstancePtr original, QWidget* parent) + : QDialog(parent), ui(new Ui::CopyInstanceDialog), m_original(original) { ui->setupUi(this); resize(minimumSizeHint()); @@ -71,8 +74,7 @@ CopyInstanceDialog::CopyInstanceDialog(InstancePtr original, QWidget *parent) groupList.push_front(""); ui->groupBox->addItems(groupList); int index = groupList.indexOf(APPLICATION->instances()->getInstanceGroup(m_original->id())); - if(index == -1) - { + if (index == -1) { index = 0; } ui->groupBox->setCurrentIndex(index); @@ -85,6 +87,35 @@ CopyInstanceDialog::CopyInstanceDialog(InstancePtr original, QWidget *parent) ui->copyServersCheckbox->setChecked(m_selectedOptions.isCopyServersEnabled()); ui->copyModsCheckbox->setChecked(m_selectedOptions.isCopyModsEnabled()); ui->copyScreenshotsCheckbox->setChecked(m_selectedOptions.isCopyScreenshotsEnabled()); + + ui->symbolicLinksCheckbox->setChecked(m_selectedOptions.isUseSymLinksEnabled()); + ui->hardLinksCheckbox->setChecked(m_selectedOptions.isUseHardLinksEnabled()); + + ui->recursiveLinkCheckbox->setChecked(m_selectedOptions.isLinkRecursivelyEnabled()); + ui->dontLinkSavesCheckbox->setChecked(m_selectedOptions.isDontLinkSavesEnabled()); + + auto detectedFS = FS::statFS(m_original->instanceRoot()).fsType; + + m_cloneSupported = FS::canCloneOnFS(detectedFS); + m_linkSupported = FS::canLinkOnFS(detectedFS); + + if (m_cloneSupported) { + ui->cloneSupportedLabel->setText(tr("Reflinks are supported on %1").arg(FS::getFilesystemTypeName(detectedFS))); + } else { + ui->cloneSupportedLabel->setText(tr("Reflinks aren't supported on %1").arg(FS::getFilesystemTypeName(detectedFS))); + } + +#if defined(Q_OS_WIN) + ui->symbolicLinksCheckbox->setIcon(style()->standardIcon(QStyle::SP_VistaShield)); + ui->symbolicLinksCheckbox->setToolTip(tr("Use symbolic links instead of copying files.") + + "\n" + tr("On Windows, symbolic links may require admin permission to create.")); +#endif + + updateLinkOptions(); + updateUseCloneCheckbox(); + + auto HelpButton = ui->buttonBox->button(QDialogButtonBox::Help); + connect(HelpButton, &QPushButton::clicked, this, &CopyInstanceDialog::help); } CopyInstanceDialog::~CopyInstanceDialog() @@ -96,8 +127,7 @@ void CopyInstanceDialog::updateDialogState() { auto allowOK = !instName().isEmpty(); auto OkButton = ui->buttonBox->button(QDialogButtonBox::Ok); - if(OkButton->isEnabled() != allowOK) - { + if (OkButton->isEnabled() != allowOK) { OkButton->setEnabled(allowOK); } } @@ -105,8 +135,7 @@ void CopyInstanceDialog::updateDialogState() QString CopyInstanceDialog::instName() const { auto result = ui->instNameTextBox->text().trimmed(); - if(result.size()) - { + if (result.size()) { return result; } return QString(); @@ -127,6 +156,11 @@ const InstanceCopyPrefs& CopyInstanceDialog::getChosenOptions() const return m_selectedOptions; } +void CopyInstanceDialog::help() +{ + DesktopServices::openUrl(QUrl(BuildConfig.HELP_URL.arg("instance-copy"))); +} + void CopyInstanceDialog::checkAllCheckboxes(const bool& b) { ui->keepPlaytimeCheckbox->setChecked(b); @@ -147,20 +181,46 @@ void CopyInstanceDialog::updateSelectAllCheckbox() ui->selectAllCheckbox->blockSignals(false); } +void CopyInstanceDialog::updateUseCloneCheckbox() +{ + ui->useCloneCheckbox->setEnabled(m_cloneSupported && !ui->symbolicLinksCheckbox->isChecked() && !ui->hardLinksCheckbox->isChecked()); + ui->useCloneCheckbox->setChecked(m_cloneSupported && m_selectedOptions.isUseCloneEnabled() && !ui->symbolicLinksCheckbox->isChecked() && + !ui->hardLinksCheckbox->isChecked()); +} + +void CopyInstanceDialog::updateLinkOptions() +{ + ui->symbolicLinksCheckbox->setEnabled(m_linkSupported && !ui->hardLinksCheckbox->isChecked() && !ui->useCloneCheckbox->isChecked()); + ui->hardLinksCheckbox->setEnabled(m_linkSupported && !ui->symbolicLinksCheckbox->isChecked() && !ui->useCloneCheckbox->isChecked()); + + ui->symbolicLinksCheckbox->setChecked(m_linkSupported && m_selectedOptions.isUseSymLinksEnabled() && + !ui->useCloneCheckbox->isChecked()); + ui->hardLinksCheckbox->setChecked(m_linkSupported && m_selectedOptions.isUseHardLinksEnabled() && !ui->useCloneCheckbox->isChecked()); + + bool linksInUse = (ui->symbolicLinksCheckbox->isChecked() || ui->hardLinksCheckbox->isChecked()); + ui->recursiveLinkCheckbox->setEnabled(m_linkSupported && linksInUse && !ui->hardLinksCheckbox->isChecked()); + ui->dontLinkSavesCheckbox->setEnabled(m_linkSupported && linksInUse); + ui->recursiveLinkCheckbox->setChecked(m_linkSupported && linksInUse && m_selectedOptions.isLinkRecursivelyEnabled()); + ui->dontLinkSavesCheckbox->setChecked(m_linkSupported && linksInUse && m_selectedOptions.isDontLinkSavesEnabled()); + +#if defined(Q_OS_WIN) + auto OkButton = ui->buttonBox->button(QDialogButtonBox::Ok); + OkButton->setIcon(m_selectedOptions.isUseSymLinksEnabled() ? style()->standardIcon(QStyle::SP_VistaShield) : QIcon()); +#endif +} + void CopyInstanceDialog::on_iconButton_clicked() { IconPickerDialog dlg(this); dlg.execWithSelection(InstIconKey); - if (dlg.result() == QDialog::Accepted) - { + if (dlg.result() == QDialog::Accepted) { InstIconKey = dlg.selectedIconKey; ui->iconButton->setIcon(APPLICATION->icons()->getIcon(InstIconKey)); } } - -void CopyInstanceDialog::on_instNameTextBox_textChanged(const QString &arg1) +void CopyInstanceDialog::on_instNameTextBox_textChanged(const QString& arg1) { updateDialogState(); } @@ -175,10 +235,10 @@ void CopyInstanceDialog::on_selectAllCheckbox_stateChanged(int state) void CopyInstanceDialog::on_copySavesCheckbox_stateChanged(int state) { m_selectedOptions.enableCopySaves(state == Qt::Checked); + ui->dontLinkSavesCheckbox->setChecked((state == Qt::Checked) && ui->dontLinkSavesCheckbox->isChecked()); updateSelectAllCheckbox(); } - void CopyInstanceDialog::on_keepPlaytimeCheckbox_stateChanged(int state) { m_selectedOptions.enableKeepPlaytime(state == Qt::Checked); @@ -220,3 +280,38 @@ void CopyInstanceDialog::on_copyScreenshotsCheckbox_stateChanged(int state) m_selectedOptions.enableCopyScreenshots(state == Qt::Checked); updateSelectAllCheckbox(); } + +void CopyInstanceDialog::on_symbolicLinksCheckbox_stateChanged(int state) +{ + m_selectedOptions.enableUseSymLinks(state == Qt::Checked); + updateUseCloneCheckbox(); + updateLinkOptions(); +} + +void CopyInstanceDialog::on_hardLinksCheckbox_stateChanged(int state) +{ + m_selectedOptions.enableUseHardLinks(state == Qt::Checked); + if (state == Qt::Checked && !ui->recursiveLinkCheckbox->isChecked()) { + ui->recursiveLinkCheckbox->setChecked(true); + } + updateUseCloneCheckbox(); + updateLinkOptions(); +} + +void CopyInstanceDialog::on_recursiveLinkCheckbox_stateChanged(int state) +{ + m_selectedOptions.enableLinkRecursively(state == Qt::Checked); + updateLinkOptions(); +} + +void CopyInstanceDialog::on_dontLinkSavesCheckbox_stateChanged(int state) +{ + m_selectedOptions.enableDontLinkSaves(state == Qt::Checked); +} + +void CopyInstanceDialog::on_useCloneCheckbox_stateChanged(int state) +{ + m_selectedOptions.enableUseClone(m_cloneSupported && (state == Qt::Checked)); + updateUseCloneCheckbox(); + updateLinkOptions(); +} diff --git a/launcher/ui/dialogs/CopyInstanceDialog.h b/launcher/ui/dialogs/CopyInstanceDialog.h index 884501d1..698c6e93 100644 --- a/launcher/ui/dialogs/CopyInstanceDialog.h +++ b/launcher/ui/dialogs/CopyInstanceDialog.h @@ -16,22 +16,21 @@ #pragma once #include <QDialog> +#include "BaseInstance.h" #include "BaseVersion.h" #include "InstanceCopyPrefs.h" class BaseInstance; -namespace Ui -{ +namespace Ui { class CopyInstanceDialog; } -class CopyInstanceDialog : public QDialog -{ +class CopyInstanceDialog : public QDialog { Q_OBJECT -public: - explicit CopyInstanceDialog(InstancePtr original, QWidget *parent = 0); + public: + explicit CopyInstanceDialog(InstancePtr original, QWidget* parent = 0); ~CopyInstanceDialog(); void updateDialogState(); @@ -41,10 +40,12 @@ public: QString iconKey() const; const InstanceCopyPrefs& getChosenOptions() const; -private -slots: + public slots: + void help(); + + private slots: void on_iconButton_clicked(); - void on_instNameTextBox_textChanged(const QString &arg1); + void on_instNameTextBox_textChanged(const QString& arg1); // Checkboxes void on_selectAllCheckbox_stateChanged(int state); void on_copySavesCheckbox_stateChanged(int state); @@ -55,13 +56,23 @@ slots: void on_copyServersCheckbox_stateChanged(int state); void on_copyModsCheckbox_stateChanged(int state); void on_copyScreenshotsCheckbox_stateChanged(int state); + void on_symbolicLinksCheckbox_stateChanged(int state); + void on_hardLinksCheckbox_stateChanged(int state); + void on_recursiveLinkCheckbox_stateChanged(int state); + void on_dontLinkSavesCheckbox_stateChanged(int state); + void on_useCloneCheckbox_stateChanged(int state); -private: + private: void checkAllCheckboxes(const bool& b); void updateSelectAllCheckbox(); + void updateUseCloneCheckbox(); + void updateLinkOptions(); + /* data */ - Ui::CopyInstanceDialog *ui; + Ui::CopyInstanceDialog* ui; QString InstIconKey; InstancePtr m_original; InstanceCopyPrefs m_selectedOptions; + bool m_cloneSupported = false; + bool m_linkSupported = false; }; diff --git a/launcher/ui/dialogs/CopyInstanceDialog.ui b/launcher/ui/dialogs/CopyInstanceDialog.ui index b7828fe3..5060debc 100644 --- a/launcher/ui/dialogs/CopyInstanceDialog.ui +++ b/launcher/ui/dialogs/CopyInstanceDialog.ui @@ -9,8 +9,8 @@ <rect> <x>0</x> <y>0</y> - <width>341</width> - <height>399</height> + <width>575</width> + <height>695</height> </rect> </property> <property name="windowTitle"> @@ -113,93 +113,268 @@ </layout> </item> <item> - <layout class="QHBoxLayout" name="selectAllButtonLayout"> - <item> - <widget class="QCheckBox" name="selectAllCheckbox"> - <property name="sizePolicy"> - <sizepolicy hsizetype="Fixed" vsizetype="Fixed"> - <horstretch>0</horstretch> - <verstretch>0</verstretch> - </sizepolicy> - </property> - <property name="layoutDirection"> - <enum>Qt::LeftToRight</enum> - </property> - <property name="text"> - <string>Select all</string> - </property> - <property name="checked"> - <bool>false</bool> - </property> - </widget> - </item> - </layout> + <widget class="QGroupBox" name="copyOptionsGroup"> + <property name="title"> + <string>Instance Copy Options</string> + </property> + <layout class="QGridLayout" name="copyOptionsLayout"> + <item row="1" column="0"> + <widget class="QCheckBox" name="keepPlaytimeCheckbox"> + <property name="text"> + <string>Keep play time</string> + </property> + </widget> + </item> + <item row="6" column="1"> + <widget class="QCheckBox" name="copyModsCheckbox"> + <property name="toolTip"> + <string>Disabling this will still keep the mod loader (ex: Fabric, Quilt, etc.) but erase the mods folder and their configs.</string> + </property> + <property name="text"> + <string>Copy mods</string> + </property> + </widget> + </item> + <item row="6" column="0"> + <widget class="QCheckBox" name="copyResPacksCheckbox"> + <property name="enabled"> + <bool>true</bool> + </property> + <property name="text"> + <string>Copy resource packs</string> + </property> + </widget> + </item> + <item row="5" column="0"> + <widget class="QCheckBox" name="copyGameOptionsCheckbox"> + <property name="toolTip"> + <string>Copy the in-game options like FOV, max framerate, etc.</string> + </property> + <property name="text"> + <string>Copy game options</string> + </property> + </widget> + </item> + <item row="3" column="1"> + <widget class="QCheckBox" name="copyShaderPacksCheckbox"> + <property name="text"> + <string>Copy shader packs</string> + </property> + </widget> + </item> + <item row="5" column="1"> + <widget class="QCheckBox" name="copyServersCheckbox"> + <property name="text"> + <string>Copy servers</string> + </property> + </widget> + </item> + <item row="3" column="0"> + <widget class="QCheckBox" name="copySavesCheckbox"> + <property name="text"> + <string>Copy saves</string> + </property> + </widget> + </item> + <item row="1" column="1"> + <widget class="QCheckBox" name="copyScreenshotsCheckbox"> + <property name="text"> + <string>Copy screenshots</string> + </property> + </widget> + </item> + <item row="7" column="1"> + <widget class="QCheckBox" name="selectAllCheckbox"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Minimum" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="layoutDirection"> + <enum>Qt::LeftToRight</enum> + </property> + <property name="text"> + <string>Select all</string> + </property> + <property name="checked"> + <bool>false</bool> + </property> + </widget> + </item> + </layout> + </widget> </item> <item> - <layout class="QGridLayout" name="copyOptionsLayout"> - <item row="6" column="1"> - <widget class="QCheckBox" name="copyModsCheckbox"> - <property name="toolTip"> - <string>Disabling this will still keep the mod loader (ex: Fabric, Quilt, etc.) but erase the mods folder and their configs.</string> - </property> - <property name="text"> - <string>Copy mods</string> - </property> - </widget> - </item> - <item row="5" column="0"> - <widget class="QCheckBox" name="copyGameOptionsCheckbox"> + <widget class="Line" name="line_2"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + </widget> + </item> + <item> + <widget class="QLabel" name="advancedOptionsLabel"> + <property name="text"> + <string>Advanced Copy Options</string> + </property> + <property name="alignment"> + <set>Qt::AlignCenter</set> + </property> + </widget> + </item> + <item> + <layout class="QVBoxLayout" name="copyModeLayout"> + <item> + <widget class="QGroupBox" name="linkFilesGroup"> <property name="toolTip"> - <string>Copy the in-game options like FOV, max framerate, etc.</string> + <string>Use symbolic or hard links instead of copying files.</string> </property> - <property name="text"> - <string>Copy game options</string> - </property> - </widget> - </item> - <item row="3" column="0"> - <widget class="QCheckBox" name="copySavesCheckbox"> - <property name="text"> - <string>Copy saves</string> - </property> - </widget> - </item> - <item row="3" column="1"> - <widget class="QCheckBox" name="copyShaderPacksCheckbox"> - <property name="text"> - <string>Copy shader packs</string> - </property> - </widget> - </item> - <item row="5" column="1"> - <widget class="QCheckBox" name="copyServersCheckbox"> - <property name="text"> - <string>Copy servers</string> + <property name="title"> + <string>Symbolic and Hard Link Options</string> </property> - </widget> - </item> - <item row="6" column="0"> - <widget class="QCheckBox" name="copyResPacksCheckbox"> - <property name="enabled"> - <bool>true</bool> + <property name="flat"> + <bool>false</bool> </property> - <property name="text"> - <string>Copy resource packs</string> + <property name="checkable"> + <bool>false</bool> </property> - </widget> - </item> - <item row="1" column="0"> - <widget class="QCheckBox" name="keepPlaytimeCheckbox"> - <property name="text"> - <string>Keep play time</string> + <property name="checked"> + <bool>false</bool> </property> + <layout class="QVBoxLayout" name="linkOptionsLayout"> + <item> + <widget class="QLabel" name="linkOptionsLabel"> + <property name="text"> + <string>Links are supported on most filesystems except FAT</string> + </property> + <property name="alignment"> + <set>Qt::AlignCenter</set> + </property> + </widget> + </item> + <item> + <layout class="QGridLayout" name="linkOptionsGridLayout" rowstretch="0,0,0,0" columnstretch="0,0" rowminimumheight="0,0,0,0" columnminimumwidth="0,0"> + <property name="leftMargin"> + <number>6</number> + </property> + <property name="topMargin"> + <number>6</number> + </property> + <property name="rightMargin"> + <number>6</number> + </property> + <property name="bottomMargin"> + <number>6</number> + </property> + <item row="2" column="1"> + <widget class="QCheckBox" name="recursiveLinkCheckbox"> + <property name="enabled"> + <bool>false</bool> + </property> + <property name="toolTip"> + <string>Link each resource individually instead of linking whole folders at once</string> + </property> + <property name="text"> + <string>Link files recursively</string> + </property> + </widget> + </item> + <item row="3" column="1"> + <widget class="QCheckBox" name="dontLinkSavesCheckbox"> + <property name="enabled"> + <bool>false</bool> + </property> + <property name="toolTip"> + <string>If "copy saves" is selected world save data will be copied instead of linked and thus not shared between instances.</string> + </property> + <property name="text"> + <string>Don't link saves</string> + </property> + <property name="checked"> + <bool>false</bool> + </property> + </widget> + </item> + <item row="3" column="0"> + <widget class="QCheckBox" name="hardLinksCheckbox"> + <property name="enabled"> + <bool>true</bool> + </property> + <property name="toolTip"> + <string>Use hard links instead of copying files.</string> + </property> + <property name="text"> + <string>Use hard links</string> + </property> + </widget> + </item> + <item row="2" column="0"> + <widget class="QCheckBox" name="symbolicLinksCheckbox"> + <property name="toolTip"> + <string>Use symbolic links instead of copying files.</string> + </property> + <property name="text"> + <string>Use symbolic links</string> + </property> + </widget> + </item> + </layout> + </item> + </layout> </widget> </item> - <item row="1" column="1"> - <widget class="QCheckBox" name="copyScreenshotsCheckbox"> - <property name="text"> - <string>Copy screenshots</string> + <item> + <widget class="QGroupBox" name="horizontalGroupBox"> + <property name="title"> + <string>CoW (Copy-on-Write) Options</string> </property> + <layout class="QHBoxLayout" name="useCloneLayout"> + <item> + <widget class="QCheckBox" name="useCloneCheckbox"> + <property name="enabled"> + <bool>false</bool> + </property> + <property name="toolTip"> + <string>Files cloned with reflinks take up no extra space until they are modified.</string> + </property> + <property name="text"> + <string>Clone instead of copying</string> + </property> + </widget> + </item> + <item> + <spacer name="CoWSpacer"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item> + <widget class="QLabel" name="cloneSupportedLabel"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Preferred" vsizetype="Preferred"> + <horstretch>1</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string>Your filesystem and/or OS doesn't support reflinks</string> + </property> + <property name="alignment"> + <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set> + </property> + <property name="margin"> + <number>4</number> + </property> + </widget> + </item> + </layout> </widget> </item> </layout> @@ -210,7 +385,7 @@ <enum>Qt::Horizontal</enum> </property> <property name="standardButtons"> - <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set> + <set>QDialogButtonBox::Cancel|QDialogButtonBox::Help|QDialogButtonBox::Ok</set> </property> </widget> </item> @@ -220,10 +395,21 @@ <tabstop>iconButton</tabstop> <tabstop>instNameTextBox</tabstop> <tabstop>groupBox</tabstop> + <tabstop>keepPlaytimeCheckbox</tabstop> + <tabstop>copyScreenshotsCheckbox</tabstop> + <tabstop>copySavesCheckbox</tabstop> + <tabstop>copyShaderPacksCheckbox</tabstop> + <tabstop>copyGameOptionsCheckbox</tabstop> + <tabstop>copyServersCheckbox</tabstop> + <tabstop>copyResPacksCheckbox</tabstop> + <tabstop>copyModsCheckbox</tabstop> + <tabstop>symbolicLinksCheckbox</tabstop> + <tabstop>recursiveLinkCheckbox</tabstop> + <tabstop>hardLinksCheckbox</tabstop> + <tabstop>dontLinkSavesCheckbox</tabstop> + <tabstop>useCloneCheckbox</tabstop> </tabstops> - <resources> - <include location="../../graphics.qrc"/> - </resources> + <resources/> <connections> <connection> <sender>buttonBox</sender> @@ -232,8 +418,8 @@ <slot>accept()</slot> <hints> <hint type="sourcelabel"> - <x>254</x> - <y>316</y> + <x>269</x> + <y>692</y> </hint> <hint type="destinationlabel"> <x>157</x> @@ -248,8 +434,8 @@ <slot>reject()</slot> <hints> <hint type="sourcelabel"> - <x>322</x> - <y>316</y> + <x>337</x> + <y>692</y> </hint> <hint type="destinationlabel"> <x>286</x> diff --git a/launcher/ui/dialogs/ExportInstanceDialog.cpp b/launcher/ui/dialogs/ExportInstanceDialog.cpp index 88552b23..07ec3c70 100644 --- a/launcher/ui/dialogs/ExportInstanceDialog.cpp +++ b/launcher/ui/dialogs/ExportInstanceDialog.cpp @@ -44,6 +44,9 @@ #include <QSortFilterProxyModel> #include <QDebug> #include <QSaveFile> +#include <QStack> +#include <QFileInfo> + #include "StringUtils.h" #include "SeparatorPrefixTree.h" #include "Application.h" @@ -428,7 +431,8 @@ bool ExportInstanceDialog::doExport() QMessageBox::warning(this, tr("Error"), tr("Unable to export instance")); return false; } - if (!MMCZip::compressDirFiles(output, m_instance->instanceRoot(), files)) + + if (!MMCZip::compressDirFiles(output, m_instance->instanceRoot(), files, true)) { QMessageBox::warning(this, tr("Error"), tr("Unable to export instance")); return false; diff --git a/launcher/ui/dialogs/ImportResourcePackDialog.cpp b/launcher/ui/dialogs/ImportResourceDialog.cpp index e8902656..84b69273 100644 --- a/launcher/ui/dialogs/ImportResourcePackDialog.cpp +++ b/launcher/ui/dialogs/ImportResourceDialog.cpp @@ -1,5 +1,5 @@ -#include "ImportResourcePackDialog.h" -#include "ui_ImportResourcePackDialog.h" +#include "ImportResourceDialog.h" +#include "ui_ImportResourceDialog.h" #include <QFileDialog> #include <QPushButton> @@ -8,10 +8,11 @@ #include "InstanceList.h" #include <InstanceList.h> -#include "ui/instanceview/InstanceProxyModel.h" #include "ui/instanceview/InstanceDelegate.h" +#include "ui/instanceview/InstanceProxyModel.h" -ImportResourcePackDialog::ImportResourcePackDialog(QWidget* parent) : QDialog(parent), ui(new Ui::ImportResourcePackDialog) +ImportResourceDialog::ImportResourceDialog(QString file_path, PackedResourceType type, QWidget* parent) + : QDialog(parent), ui(new Ui::ImportResourceDialog), m_resource_type(type), m_file_path(file_path) { ui->setupUi(this); setWindowModality(Qt::WindowModal); @@ -40,15 +41,19 @@ ImportResourcePackDialog::ImportResourcePackDialog(QWidget* parent) : QDialog(pa connect(contentsWidget, SIGNAL(doubleClicked(QModelIndex)), SLOT(activated(QModelIndex))); connect(contentsWidget->selectionModel(), SIGNAL(selectionChanged(QItemSelection, QItemSelection)), SLOT(selectionChanged(QItemSelection, QItemSelection))); + + ui->label->setText( + tr("Choose the instance you would like to import this %1 to.").arg(ResourceUtils::getPackedTypeName(m_resource_type))); + ui->label_file_path->setText(tr("File: %1").arg(m_file_path)); } -void ImportResourcePackDialog::activated(QModelIndex index) +void ImportResourceDialog::activated(QModelIndex index) { selectedInstanceKey = index.data(InstanceList::InstanceIDRole).toString(); accept(); } -void ImportResourcePackDialog::selectionChanged(QItemSelection selected, QItemSelection deselected) +void ImportResourceDialog::selectionChanged(QItemSelection selected, QItemSelection deselected) { if (selected.empty()) return; @@ -59,7 +64,7 @@ void ImportResourcePackDialog::selectionChanged(QItemSelection selected, QItemSe } } -ImportResourcePackDialog::~ImportResourcePackDialog() +ImportResourceDialog::~ImportResourceDialog() { delete ui; } diff --git a/launcher/ui/dialogs/ImportResourceDialog.h b/launcher/ui/dialogs/ImportResourceDialog.h new file mode 100644 index 00000000..5f2f7a92 --- /dev/null +++ b/launcher/ui/dialogs/ImportResourceDialog.h @@ -0,0 +1,30 @@ +#pragma once + +#include <QDialog> +#include <QItemSelection> + +#include "minecraft/mod/tasks/LocalResourceParse.h" +#include "ui/instanceview/InstanceProxyModel.h" + +namespace Ui { +class ImportResourceDialog; +} + +class ImportResourceDialog : public QDialog { + Q_OBJECT + + public: + explicit ImportResourceDialog(QString file_path, PackedResourceType type, QWidget* parent = nullptr); + ~ImportResourceDialog() override; + QString selectedInstanceKey; + + private: + Ui::ImportResourceDialog* ui; + PackedResourceType m_resource_type; + QString m_file_path; + InstanceProxyModel* proxyModel; + + private slots: + void selectionChanged(QItemSelection, QItemSelection); + void activated(QModelIndex); +}; diff --git a/launcher/ui/dialogs/ImportResourcePackDialog.ui b/launcher/ui/dialogs/ImportResourceDialog.ui index 20cb9177..cc3f4ec1 100644 --- a/launcher/ui/dialogs/ImportResourcePackDialog.ui +++ b/launcher/ui/dialogs/ImportResourceDialog.ui @@ -1,7 +1,7 @@ <?xml version="1.0" encoding="UTF-8"?> <ui version="4.0"> - <class>ImportResourcePackDialog</class> - <widget class="QDialog" name="ImportResourcePackDialog"> + <class>ImportResourceDialog</class> + <widget class="QDialog" name="ImportResourceDialog"> <property name="geometry"> <rect> <x>0</x> @@ -11,7 +11,7 @@ </rect> </property> <property name="windowTitle"> - <string>Choose instance to import</string> + <string>Choose instance to import to</string> </property> <layout class="QVBoxLayout" name="verticalLayout"> <item> @@ -22,6 +22,13 @@ </widget> </item> <item> + <widget class="QLabel" name="label_file_path"> + <property name="text"> + <string/> + </property> + </widget> + </item> + <item> <widget class="QListView" name="instanceView"/> </item> <item> @@ -41,7 +48,7 @@ <connection> <sender>buttonBox</sender> <signal>accepted()</signal> - <receiver>ImportResourcePackDialog</receiver> + <receiver>ImportResourceDialog</receiver> <slot>accept()</slot> <hints> <hint type="sourcelabel"> @@ -57,7 +64,7 @@ <connection> <sender>buttonBox</sender> <signal>rejected()</signal> - <receiver>ImportResourcePackDialog</receiver> + <receiver>ImportResourceDialog</receiver> <slot>reject()</slot> <hints> <hint type="sourcelabel"> diff --git a/launcher/ui/dialogs/ImportResourcePackDialog.h b/launcher/ui/dialogs/ImportResourcePackDialog.h deleted file mode 100644 index 8356f204..00000000 --- a/launcher/ui/dialogs/ImportResourcePackDialog.h +++ /dev/null @@ -1,27 +0,0 @@ -#pragma once - -#include <QDialog> -#include <QItemSelection> - -#include "ui/instanceview/InstanceProxyModel.h" - -namespace Ui { -class ImportResourcePackDialog; -} - -class ImportResourcePackDialog : public QDialog { - Q_OBJECT - - public: - explicit ImportResourcePackDialog(QWidget* parent = 0); - ~ImportResourcePackDialog(); - InstanceProxyModel* proxyModel; - QString selectedInstanceKey; - - private: - Ui::ImportResourcePackDialog* ui; - - private slots: - void selectionChanged(QItemSelection, QItemSelection); - void activated(QModelIndex); -}; diff --git a/launcher/ui/dialogs/ModDownloadDialog.cpp b/launcher/ui/dialogs/ModDownloadDialog.cpp deleted file mode 100644 index 24d23ba9..00000000 --- a/launcher/ui/dialogs/ModDownloadDialog.cpp +++ /dev/null @@ -1,202 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-only -/* - * 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 - * 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 "ModDownloadDialog.h" - -#include <BaseVersion.h> -#include <InstanceList.h> -#include <icons/IconList.h> - -#include "Application.h" -#include "ReviewMessageBox.h" - -#include <QDialogButtonBox> -#include <QLayout> -#include <QPushButton> -#include <QValidator> - -#include "ModDownloadTask.h" -#include "ui/pages/modplatform/flame/FlameModPage.h" -#include "ui/pages/modplatform/modrinth/ModrinthModPage.h" -#include "ui/widgets/PageContainer.h" - -ModDownloadDialog::ModDownloadDialog(const std::shared_ptr<ModFolderModel>& mods, QWidget* parent, BaseInstance* instance) - : QDialog(parent), mods(mods), m_verticalLayout(new QVBoxLayout(this)), m_instance(instance) -{ - setObjectName(QStringLiteral("ModDownloadDialog")); - m_verticalLayout->setObjectName(QStringLiteral("verticalLayout")); - - resize(std::max(0.5 * parent->width(), 400.0), std::max(0.75 * parent->height(), 400.0)); - - setWindowIcon(APPLICATION->getThemedIcon("new")); - // NOTE: m_buttons must be initialized before PageContainer, because it indirectly accesses m_buttons through setSuggestedPack! Do not - // move this below. - m_buttons = new QDialogButtonBox(QDialogButtonBox::Help | QDialogButtonBox::Ok | QDialogButtonBox::Cancel); - - m_container = new PageContainer(this); - m_container->setSizePolicy(QSizePolicy::Policy::Preferred, QSizePolicy::Policy::Expanding); - m_container->layout()->setContentsMargins(0, 0, 0, 0); - m_verticalLayout->addWidget(m_container); - - m_container->addButtons(m_buttons); - - connect(m_container, &PageContainer::selectedPageChanged, this, &ModDownloadDialog::selectedPageChanged); - - // Bonk Qt over its stupid head and make sure it understands which button is the default one... - // See: https://stackoverflow.com/questions/24556831/qbuttonbox-set-default-button - auto OkButton = m_buttons->button(QDialogButtonBox::Ok); - OkButton->setEnabled(false); - OkButton->setDefault(true); - OkButton->setAutoDefault(true); - OkButton->setText(tr("Review and confirm")); - OkButton->setShortcut(tr("Ctrl+Return")); - OkButton->setToolTip(tr("Opens a new popup to review your selected mods and confirm your selection. Shortcut: Ctrl+Return")); - connect(OkButton, &QPushButton::clicked, this, &ModDownloadDialog::confirm); - - auto CancelButton = m_buttons->button(QDialogButtonBox::Cancel); - CancelButton->setDefault(false); - CancelButton->setAutoDefault(false); - connect(CancelButton, &QPushButton::clicked, this, &ModDownloadDialog::reject); - - auto HelpButton = m_buttons->button(QDialogButtonBox::Help); - HelpButton->setDefault(false); - HelpButton->setAutoDefault(false); - connect(HelpButton, &QPushButton::clicked, m_container, &PageContainer::help); - - QMetaObject::connectSlotsByName(this); - setWindowModality(Qt::WindowModal); - setWindowTitle(dialogTitle()); - - restoreGeometry(QByteArray::fromBase64(APPLICATION->settings()->get("ModDownloadGeometry").toByteArray())); -} - -QString ModDownloadDialog::dialogTitle() -{ - return tr("Download mods"); -} - -void ModDownloadDialog::reject() -{ - APPLICATION->settings()->set("ModDownloadGeometry", saveGeometry().toBase64()); - QDialog::reject(); -} - -void ModDownloadDialog::confirm() -{ - auto keys = modTask.keys(); - keys.sort(Qt::CaseInsensitive); - - auto confirm_dialog = ReviewMessageBox::create(this, tr("Confirm mods to download")); - - for (auto& task : keys) { - confirm_dialog->appendMod({ task, modTask.find(task).value()->getFilename() }); - } - - if (confirm_dialog->exec()) { - auto deselected = confirm_dialog->deselectedMods(); - for (auto name : deselected) { - modTask.remove(name); - } - - this->accept(); - } -} - -void ModDownloadDialog::accept() -{ - APPLICATION->settings()->set("ModDownloadGeometry", saveGeometry().toBase64()); - QDialog::accept(); -} - -QList<BasePage*> ModDownloadDialog::getPages() -{ - QList<BasePage*> pages; - - pages.append(ModrinthModPage::create(this, m_instance)); - if (APPLICATION->capabilities() & Application::SupportsFlame) - pages.append(FlameModPage::create(this, m_instance)); - - m_selectedPage = dynamic_cast<ModPage*>(pages[0]); - - return pages; -} - -void ModDownloadDialog::addSelectedMod(QString name, ModDownloadTask* task) -{ - removeSelectedMod(name); - modTask.insert(name, task); - - m_buttons->button(QDialogButtonBox::Ok)->setEnabled(!modTask.isEmpty()); -} - -void ModDownloadDialog::removeSelectedMod(QString name) -{ - if (modTask.contains(name)) - delete modTask.find(name).value(); - modTask.remove(name); - - m_buttons->button(QDialogButtonBox::Ok)->setEnabled(!modTask.isEmpty()); -} - -bool ModDownloadDialog::isModSelected(QString name, QString filename) const -{ - // FIXME: Is there a way to check for versions without checking the filename - // as a heuristic, other than adding such info to ModDownloadTask itself? - auto iter = modTask.find(name); - return iter != modTask.end() && (iter.value()->getFilename() == filename); -} - -bool ModDownloadDialog::isModSelected(QString name) const -{ - auto iter = modTask.find(name); - return iter != modTask.end(); -} - -const QList<ModDownloadTask*> ModDownloadDialog::getTasks() -{ - return modTask.values(); -} - -void ModDownloadDialog::selectedPageChanged(BasePage* previous, BasePage* selected) -{ - auto* prev_page = dynamic_cast<ModPage*>(previous); - if (!prev_page) { - qCritical() << "Page '" << previous->displayName() << "' in ModDownloadDialog is not a ModPage!"; - return; - } - - m_selectedPage = dynamic_cast<ModPage*>(selected); - if (!m_selectedPage) { - qCritical() << "Page '" << selected->displayName() << "' in ModDownloadDialog is not a ModPage!"; - return; - } - - // Same effect as having a global search bar - m_selectedPage->setSearchTerm(prev_page->getSearchTerm()); -} - -bool ModDownloadDialog::selectPage(QString pageId) -{ - return m_container->selectPage(pageId); -} - -ModPage* ModDownloadDialog::getSelectedPage() -{ - return m_selectedPage; -} diff --git a/launcher/ui/dialogs/ModDownloadDialog.h b/launcher/ui/dialogs/ModDownloadDialog.h deleted file mode 100644 index fcf6f4fc..00000000 --- a/launcher/ui/dialogs/ModDownloadDialog.h +++ /dev/null @@ -1,78 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-only -/* - * 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 - * 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 <QVBoxLayout> - -#include "ModDownloadTask.h" -#include "minecraft/mod/ModFolderModel.h" -#include "ui/pages/BasePageProvider.h" - -namespace Ui -{ -class ModDownloadDialog; -} - -class PageContainer; -class QDialogButtonBox; -class ModPage; -class ModrinthModPage; - -class ModDownloadDialog final : public QDialog, public BasePageProvider -{ - Q_OBJECT - - public: - explicit ModDownloadDialog(const std::shared_ptr<ModFolderModel>& mods, QWidget* parent, BaseInstance* instance); - ~ModDownloadDialog() override = default; - - QString dialogTitle() override; - QList<BasePage*> getPages() override; - - void addSelectedMod(QString name = QString(), ModDownloadTask* task = nullptr); - void removeSelectedMod(QString name = QString()); - bool isModSelected(QString name, QString filename) const; - bool isModSelected(QString name) const; - - const QList<ModDownloadTask*> getTasks(); - const std::shared_ptr<ModFolderModel>& mods; - - bool selectPage(QString pageId); - ModPage* getSelectedPage(); - - public slots: - void confirm(); - void accept() override; - void reject() override; - - private slots: - void selectedPageChanged(BasePage* previous, BasePage* selected); - - private: - Ui::ModDownloadDialog* ui = nullptr; - PageContainer* m_container = nullptr; - QDialogButtonBox* m_buttons = nullptr; - QVBoxLayout* m_verticalLayout = nullptr; - ModPage* m_selectedPage = nullptr; - - QHash<QString, ModDownloadTask*> modTask; - BaseInstance* m_instance; -}; diff --git a/launcher/ui/dialogs/ModUpdateDialog.cpp b/launcher/ui/dialogs/ModUpdateDialog.cpp index cedd4a96..8618b924 100644 --- a/launcher/ui/dialogs/ModUpdateDialog.cpp +++ b/launcher/ui/dialogs/ModUpdateDialog.cpp @@ -7,6 +7,7 @@ #include "FileSystem.h" #include "Json.h" +#include "Markdown.h" #include "tasks/ConcurrentTask.h" @@ -17,10 +18,11 @@ #include "modplatform/flame/FlameCheckUpdate.h" #include "modplatform/modrinth/ModrinthCheckUpdate.h" -#include <HoeDown.h> #include <QTextBrowser> #include <QTreeWidgetItem> +#include <optional> + static ModPlatform::ProviderCapabilities ProviderCaps; static std::list<Version> mcVersions(BaseInstance* inst) @@ -28,7 +30,7 @@ static std::list<Version> mcVersions(BaseInstance* inst) return { static_cast<MinecraftInstance*>(inst)->getPackProfile()->getComponent("net.minecraft")->getVersion() }; } -static ModAPI::ModLoaderTypes mcLoaders(BaseInstance* inst) +static std::optional<ResourceAPI::ModLoaderTypes> mcLoaders(BaseInstance* inst) { return { static_cast<MinecraftInstance*>(inst)->getPackProfile()->getModLoaders() }; } @@ -86,15 +88,15 @@ void ModUpdateDialog::checkCandidates() SequentialTask check_task(m_parent, tr("Checking for updates")); if (!m_modrinth_to_update.empty()) { - m_modrinth_check_task = new ModrinthCheckUpdate(m_modrinth_to_update, versions, loaders, m_mod_model); - connect(m_modrinth_check_task, &CheckUpdateTask::checkFailed, this, + m_modrinth_check_task.reset(new ModrinthCheckUpdate(m_modrinth_to_update, versions, loaders, m_mod_model)); + connect(m_modrinth_check_task.get(), &CheckUpdateTask::checkFailed, this, [this](Mod* mod, QString reason, QUrl recover_url) { m_failed_check_update.append({mod, reason, recover_url}); }); check_task.addTask(m_modrinth_check_task); } if (!m_flame_to_update.empty()) { - m_flame_check_task = new FlameCheckUpdate(m_flame_to_update, versions, loaders, m_mod_model); - connect(m_flame_check_task, &CheckUpdateTask::checkFailed, this, + m_flame_check_task.reset(new FlameCheckUpdate(m_flame_to_update, versions, loaders, m_mod_model)); + connect(m_flame_check_task.get(), &CheckUpdateTask::checkFailed, this, [this](Mod* mod, QString reason, QUrl recover_url) { m_failed_check_update.append({mod, reason, recover_url}); }); check_task.addTask(m_flame_check_task); } @@ -212,14 +214,14 @@ auto ModUpdateDialog::ensureMetadata() -> bool bool confirm_rest = false; bool try_others_rest = false; bool skip_rest = false; - ModPlatform::Provider provider_rest = ModPlatform::Provider::MODRINTH; + ModPlatform::ResourceProvider provider_rest = ModPlatform::ResourceProvider::MODRINTH; - auto addToTmp = [&](Mod* m, ModPlatform::Provider p) { + auto addToTmp = [&](Mod* m, ModPlatform::ResourceProvider p) { switch (p) { - case ModPlatform::Provider::MODRINTH: + case ModPlatform::ResourceProvider::MODRINTH: modrinth_tmp.push_back(m); break; - case ModPlatform::Provider::FLAME: + case ModPlatform::ResourceProvider::FLAME: flame_tmp.push_back(m); break; } @@ -264,10 +266,10 @@ auto ModUpdateDialog::ensureMetadata() -> bool } if (!modrinth_tmp.empty()) { - auto* modrinth_task = new EnsureMetadataTask(modrinth_tmp, index_dir, ModPlatform::Provider::MODRINTH); - connect(modrinth_task, &EnsureMetadataTask::metadataReady, [this](Mod* candidate) { onMetadataEnsured(candidate); }); - connect(modrinth_task, &EnsureMetadataTask::metadataFailed, [this, &should_try_others](Mod* candidate) { - onMetadataFailed(candidate, should_try_others.find(candidate->internal_id()).value(), ModPlatform::Provider::MODRINTH); + auto modrinth_task = makeShared<EnsureMetadataTask>(modrinth_tmp, index_dir, ModPlatform::ResourceProvider::MODRINTH); + connect(modrinth_task.get(), &EnsureMetadataTask::metadataReady, [this](Mod* candidate) { onMetadataEnsured(candidate); }); + connect(modrinth_task.get(), &EnsureMetadataTask::metadataFailed, [this, &should_try_others](Mod* candidate) { + onMetadataFailed(candidate, should_try_others.find(candidate->internal_id()).value(), ModPlatform::ResourceProvider::MODRINTH); }); if (modrinth_task->getHashingTask()) @@ -277,10 +279,10 @@ auto ModUpdateDialog::ensureMetadata() -> bool } if (!flame_tmp.empty()) { - auto* flame_task = new EnsureMetadataTask(flame_tmp, index_dir, ModPlatform::Provider::FLAME); - connect(flame_task, &EnsureMetadataTask::metadataReady, [this](Mod* candidate) { onMetadataEnsured(candidate); }); - connect(flame_task, &EnsureMetadataTask::metadataFailed, [this, &should_try_others](Mod* candidate) { - onMetadataFailed(candidate, should_try_others.find(candidate->internal_id()).value(), ModPlatform::Provider::FLAME); + auto flame_task = makeShared<EnsureMetadataTask>(flame_tmp, index_dir, ModPlatform::ResourceProvider::FLAME); + connect(flame_task.get(), &EnsureMetadataTask::metadataReady, [this](Mod* candidate) { onMetadataEnsured(candidate); }); + connect(flame_task.get(), &EnsureMetadataTask::metadataFailed, [this, &should_try_others](Mod* candidate) { + onMetadataFailed(candidate, should_try_others.find(candidate->internal_id()).value(), ModPlatform::ResourceProvider::FLAME); }); if (flame_task->getHashingTask()) @@ -306,35 +308,35 @@ void ModUpdateDialog::onMetadataEnsured(Mod* mod) return; switch (mod->metadata()->provider) { - case ModPlatform::Provider::MODRINTH: + case ModPlatform::ResourceProvider::MODRINTH: m_modrinth_to_update.push_back(mod); break; - case ModPlatform::Provider::FLAME: + case ModPlatform::ResourceProvider::FLAME: m_flame_to_update.push_back(mod); break; } } -ModPlatform::Provider next(ModPlatform::Provider p) +ModPlatform::ResourceProvider next(ModPlatform::ResourceProvider p) { switch (p) { - case ModPlatform::Provider::MODRINTH: - return ModPlatform::Provider::FLAME; - case ModPlatform::Provider::FLAME: - return ModPlatform::Provider::MODRINTH; + case ModPlatform::ResourceProvider::MODRINTH: + return ModPlatform::ResourceProvider::FLAME; + case ModPlatform::ResourceProvider::FLAME: + return ModPlatform::ResourceProvider::MODRINTH; } - return ModPlatform::Provider::FLAME; + return ModPlatform::ResourceProvider::FLAME; } -void ModUpdateDialog::onMetadataFailed(Mod* mod, bool try_others, ModPlatform::Provider first_choice) +void ModUpdateDialog::onMetadataFailed(Mod* mod, bool try_others, ModPlatform::ResourceProvider first_choice) { if (try_others) { auto index_dir = indexDir(); - auto* task = new EnsureMetadataTask(mod, index_dir, next(first_choice)); - connect(task, &EnsureMetadataTask::metadataReady, [this](Mod* candidate) { onMetadataEnsured(candidate); }); - connect(task, &EnsureMetadataTask::metadataFailed, [this](Mod* candidate) { onMetadataFailed(candidate, false); }); + auto task = makeShared<EnsureMetadataTask>(mod, index_dir, next(first_choice)); + connect(task.get(), &EnsureMetadataTask::metadataReady, [this](Mod* candidate) { onMetadataEnsured(candidate); }); + connect(task.get(), &EnsureMetadataTask::metadataFailed, [this](Mod* candidate) { onMetadataFailed(candidate, false); }); m_second_try_metadata->addTask(task); } else { @@ -368,15 +370,8 @@ void ModUpdateDialog::appendMod(CheckUpdateTask::UpdatableMod const& info) QString text = info.changelog; switch (info.provider) { - case ModPlatform::Provider::MODRINTH: { - HoeDown h; - // HoeDown bug?: \n aren't converted to <br> - text = h.process(info.changelog.toUtf8()); - - // Don't convert if there's an HTML tag right after (Qt rendering weirdness) - text.remove(QRegularExpression("(\n+)(?=<)")); - text.replace('\n', "<br>"); - + case ModPlatform::ResourceProvider::MODRINTH: { + text = markdownToHTML(info.changelog.toUtf8()); break; } default: @@ -393,9 +388,9 @@ void ModUpdateDialog::appendMod(CheckUpdateTask::UpdatableMod const& info) ui->modTreeWidget->addTopLevelItem(item_top); } -auto ModUpdateDialog::getTasks() -> const QList<ModDownloadTask*> +auto ModUpdateDialog::getTasks() -> const QList<ResourceDownloadTask::Ptr> { - QList<ModDownloadTask*> list; + QList<ResourceDownloadTask::Ptr> list; auto* item = ui->modTreeWidget->topLevelItem(0); diff --git a/launcher/ui/dialogs/ModUpdateDialog.h b/launcher/ui/dialogs/ModUpdateDialog.h index bd486f0d..1a92f613 100644 --- a/launcher/ui/dialogs/ModUpdateDialog.h +++ b/launcher/ui/dialogs/ModUpdateDialog.h @@ -1,7 +1,7 @@ #pragma once #include "BaseInstance.h" -#include "ModDownloadTask.h" +#include "ResourceDownloadTask.h" #include "ReviewMessageBox.h" #include "minecraft/mod/ModFolderModel.h" @@ -25,7 +25,7 @@ class ModUpdateDialog final : public ReviewMessageBox { void appendMod(const CheckUpdateTask::UpdatableMod& info); - const QList<ModDownloadTask*> getTasks(); + const QList<ResourceDownloadTask::Ptr> getTasks(); auto indexDir() const -> QDir { return m_mod_model->indexDir(); } auto noUpdates() const -> bool { return m_no_updates; }; @@ -36,13 +36,13 @@ class ModUpdateDialog final : public ReviewMessageBox { private slots: void onMetadataEnsured(Mod*); - void onMetadataFailed(Mod*, bool try_others = false, ModPlatform::Provider first_choice = ModPlatform::Provider::MODRINTH); + void onMetadataFailed(Mod*, bool try_others = false, ModPlatform::ResourceProvider first_choice = ModPlatform::ResourceProvider::MODRINTH); private: QWidget* m_parent; - ModrinthCheckUpdate* m_modrinth_check_task = nullptr; - FlameCheckUpdate* m_flame_check_task = nullptr; + shared_qobject_ptr<ModrinthCheckUpdate> m_modrinth_check_task; + shared_qobject_ptr<FlameCheckUpdate> m_flame_check_task; const std::shared_ptr<ModFolderModel> m_mod_model; @@ -50,11 +50,11 @@ class ModUpdateDialog final : public ReviewMessageBox { QList<Mod*> m_modrinth_to_update; QList<Mod*> m_flame_to_update; - ConcurrentTask* m_second_try_metadata; + ConcurrentTask::Ptr m_second_try_metadata; QList<std::tuple<Mod*, QString>> m_failed_metadata; QList<std::tuple<Mod*, QString, QUrl>> m_failed_check_update; - QHash<QString, ModDownloadTask*> m_tasks; + QHash<QString, ResourceDownloadTask::Ptr> m_tasks; BaseInstance* m_instance; bool m_no_updates = false; diff --git a/launcher/ui/dialogs/NewInstanceDialog.cpp b/launcher/ui/dialogs/NewInstanceDialog.cpp index df182f09..aafaf220 100644 --- a/launcher/ui/dialogs/NewInstanceDialog.cpp +++ b/launcher/ui/dialogs/NewInstanceDialog.cpp @@ -56,7 +56,6 @@ #include "ui/widgets/PageContainer.h" #include "ui/pages/modplatform/VanillaPage.h" #include "ui/pages/modplatform/atlauncher/AtlPage.h" -#include "ui/pages/modplatform/ftb/FtbPage.h" #include "ui/pages/modplatform/legacy_ftb/Page.h" #include "ui/pages/modplatform/flame/FlamePage.h" #include "ui/pages/modplatform/ImportPage.h" @@ -100,7 +99,7 @@ NewInstanceDialog::NewInstanceDialog(const QString & initialGroup, const QString // NOTE: m_buttons must be initialized before PageContainer, because it indirectly accesses m_buttons through setSuggestedPack! Do not move this below. m_buttons = new QDialogButtonBox(QDialogButtonBox::Help | QDialogButtonBox::Ok | QDialogButtonBox::Cancel); - m_container = new PageContainer(this); + m_container = new PageContainer(this, {}, this); m_container->setSizePolicy(QSizePolicy::Policy::Preferred, QSizePolicy::Policy::Expanding); m_container->layout()->setContentsMargins(0, 0, 0, 0); ui->verticalLayout->insertWidget(2, m_container); @@ -168,7 +167,6 @@ QList<BasePage *> NewInstanceDialog::getPages() pages.append(new AtlPage(this)); if (APPLICATION->capabilities() & Application::SupportsFlame) pages.append(new FlamePage(this)); - pages.append(new FtbPage(this)); pages.append(new LegacyFTB::Page(this)); pages.append(new ModrinthPage(this)); pages.append(new TechnicPage(this)); diff --git a/launcher/ui/dialogs/ProgressDialog.cpp b/launcher/ui/dialogs/ProgressDialog.cpp index da73a449..246a0fd4 100644 --- a/launcher/ui/dialogs/ProgressDialog.cpp +++ b/launcher/ui/dialogs/ProgressDialog.cpp @@ -1,29 +1,69 @@ -/* Copyright 2013-2021 MultiMC Contributors +/// SPDX-License-Identifier: GPL-3.0-only +/* + * PrismLaucher - Minecraft Launcher + * Copyright (C) 2023 Rachel Powers <508861+Ryex@users.noreply.github.com> * - * 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 + * 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. * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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. * - * 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. + * 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 "ProgressDialog.h" #include "ui_ProgressDialog.h" +#include <limits> #include <QDebug> #include <QKeyEvent> #include "tasks/Task.h" +#include "ui/widgets/SubTaskProgressBar.h" + + +// map a value in a numeric range of an arbitrary type to between 0 and INT_MAX +// for getting the best precision out of the qt progress bar +template<typename T, std::enable_if_t<std::is_arithmetic_v<T>, bool> = true> +std::tuple<int, int> map_int_zero_max(T current, T range_max, T range_min) +{ + int int_max = std::numeric_limits<int>::max(); + + auto type_range = range_max - range_min; + double percentage = static_cast<double>(current - range_min) / static_cast<double>(type_range); + int mapped_current = percentage * int_max; + + return {mapped_current, int_max}; +} + + ProgressDialog::ProgressDialog(QWidget* parent) : QDialog(parent), ui(new Ui::ProgressDialog) { ui->setupUi(this); + ui->taskProgressScrollArea->setHidden(true); this->setWindowFlags(this->windowFlags() & ~Qt::WindowContextHelpButtonHint); setAttribute(Qt::WidgetAttribute::WA_QuitOnClose, true); setSkipButton(false); @@ -54,10 +94,24 @@ ProgressDialog::~ProgressDialog() } void ProgressDialog::updateSize() -{ +{ + QSize lastSize = this->size(); QSize qSize = QSize(480, minimumSizeHint().height()); - resize(qSize); - setFixedSize(qSize); + + // if the current window is too small + if ((lastSize != qSize) && (lastSize.height() < qSize.height())) + { + resize(qSize); + + // keep the dialog in the center after a resize + this->move( + this->parentWidget()->x() + (this->parentWidget()->width() - this->width()) / 2, + this->parentWidget()->y() + (this->parentWidget()->height() - this->height()) / 2 + ); + } + + setMinimumSize(qSize); + } int ProgressDialog::execWithTask(Task* task) @@ -79,17 +133,15 @@ int ProgressDialog::execWithTask(Task* task) connect(task, &Task::failed, this, &ProgressDialog::onTaskFailed); connect(task, &Task::succeeded, this, &ProgressDialog::onTaskSucceeded); connect(task, &Task::status, this, &ProgressDialog::changeStatus); - connect(task, &Task::stepStatus, this, &ProgressDialog::changeStatus); + connect(task, &Task::details, this, &ProgressDialog::changeStatus); + connect(task, &Task::stepProgress, this, &ProgressDialog::changeStepProgress); connect(task, &Task::progress, this, &ProgressDialog::changeProgress); - connect(task, &Task::aborted, this, &ProgressDialog::hide); connect(task, &Task::abortStatusChanged, ui->skipButton, &QPushButton::setEnabled); m_is_multi_step = task->isMultiStep(); - if (!m_is_multi_step) { - ui->globalStatusLabel->setHidden(true); - ui->globalProgressBar->setHidden(true); - } + ui->taskProgressScrollArea->setHidden(!m_is_multi_step); + updateSize(); // It's a good idea to start the task after we entered the dialog's event loop :^) if (!task->isRunning()) { @@ -149,23 +201,53 @@ void ProgressDialog::onTaskSucceeded() void ProgressDialog::changeStatus(const QString& status) { ui->globalStatusLabel->setText(task->getStatus()); - ui->statusLabel->setText(task->getStepStatus()); + ui->globalStatusDetailsLabel->setText(task->getDetails()); updateSize(); } +void ProgressDialog::addTaskProgress(TaskStepProgress const& progress) +{ + SubTaskProgressBar* task_bar = new SubTaskProgressBar(this); + taskProgress.insert(progress.uid, task_bar); + ui->taskProgressLayout->addWidget(task_bar); +} + +void ProgressDialog::changeStepProgress(TaskStepProgress const& task_progress) +{ + m_is_multi_step = true; + if(ui->taskProgressScrollArea->isHidden()) { + ui->taskProgressScrollArea->setHidden(false); + updateSize(); + } + + if (!taskProgress.contains(task_progress.uid)) + addTaskProgress(task_progress); + auto task_bar = taskProgress.value(task_progress.uid); + + + auto const [mapped_current, mapped_total] = map_int_zero_max<qint64>(task_progress.current, task_progress.total, 0); + if (task_progress.total <= 0) { + task_bar->setRange(0, 0); + } else { + task_bar->setRange(0, mapped_total); + } + + task_bar->setValue(mapped_current); + task_bar->setStatus(task_progress.status); + task_bar->setDetails(task_progress.details); + + if (task_progress.isDone()) { + task_bar->setVisible(false); + } + +} + void ProgressDialog::changeProgress(qint64 current, qint64 total) { ui->globalProgressBar->setMaximum(total); ui->globalProgressBar->setValue(current); - if (!m_is_multi_step) { - ui->taskProgressBar->setMaximum(total); - ui->taskProgressBar->setValue(current); - } else { - ui->taskProgressBar->setMaximum(task->getStepProgress()); - ui->taskProgressBar->setValue(task->getStepTotalProgress()); - } } void ProgressDialog::keyPressEvent(QKeyEvent* e) diff --git a/launcher/ui/dialogs/ProgressDialog.h b/launcher/ui/dialogs/ProgressDialog.h index 0b4b78a4..fc9a0fbc 100644 --- a/launcher/ui/dialogs/ProgressDialog.h +++ b/launcher/ui/dialogs/ProgressDialog.h @@ -1,22 +1,50 @@ -/* Copyright 2013-2021 MultiMC Contributors +/// SPDX-License-Identifier: GPL-3.0-only +/* + * PrismLaucher - Minecraft Launcher + * Copyright (C) 2023 Rachel Powers <508861+Ryex@users.noreply.github.com> * - * 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 + * 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. * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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. * - * 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. + * 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 <QDialog> #include <memory> +#include <QHash> +#include <QUuid> + +#include "QObjectPtr.h" +#include "tasks/Task.h" + +#include "ui/widgets/SubTaskProgressBar.h" class Task; class SequentialTask; @@ -52,6 +80,7 @@ slots: void changeStatus(const QString &status); void changeProgress(qint64 current, qint64 total); + void changeStepProgress(TaskStepProgress const& task_progress); private @@ -64,6 +93,7 @@ protected: private: bool handleImmediateResult(QDialog::DialogCode &result); + void addTaskProgress(TaskStepProgress const& progress); private: Ui::ProgressDialog *ui; @@ -71,4 +101,8 @@ private: Task *task; bool m_is_multi_step = false; + QHash<QUuid, SubTaskProgressBar*> taskProgress; + + }; + diff --git a/launcher/ui/dialogs/ProgressDialog.ui b/launcher/ui/dialogs/ProgressDialog.ui index 34ab71e3..a4d08124 100644 --- a/launcher/ui/dialogs/ProgressDialog.ui +++ b/launcher/ui/dialogs/ProgressDialog.ui @@ -2,75 +2,135 @@ <ui version="4.0"> <class>ProgressDialog</class> <widget class="QDialog" name="ProgressDialog"> - <property name="minimumSize"> - <size> - <width>400</width> - <height>0</height> - </size> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>480</width> + <height>210</height> + </rect> </property> - <property name="maximumSize"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Preferred" vsizetype="Preferred"> + <horstretch>1</horstretch> + <verstretch>1</verstretch> + </sizepolicy> + </property> + <property name="minimumSize"> <size> - <width>600</width> - <height>16777215</height> + <width>480</width> + <height>210</height> </size> </property> <property name="windowTitle"> <string>Please wait...</string> </property> - <layout class="QGridLayout" name="gridLayout"> - <item row="4" column="0"> - <widget class="QPushButton" name="skipButton"> - <property name="sizePolicy"> - <sizepolicy hsizetype="Expanding" vsizetype="Expanding"> - <horstretch>0</horstretch> - <verstretch>0</verstretch> - </sizepolicy> + <property name="sizeGripEnabled"> + <bool>true</bool> + </property> + <layout class="QVBoxLayout" name="verticalLayout" stretch="0,0,0,0"> + <item> + <layout class="QHBoxLayout" name="horizontalLayout" stretch="1,0"> + <item> + <widget class="QLabel" name="globalStatusLabel"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Preferred" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="minimumSize"> + <size> + <width>0</width> + <height>15</height> + </size> + </property> + <property name="text"> + <string>Global Task Status...</string> + </property> + </widget> + </item> + <item> + <widget class="QLabel" name="globalStatusDetailsLabel"> + <property name="text"> + <string>Global Status Details...</string> + </property> + <property name="alignment"> + <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set> + </property> + </widget> + </item> + </layout> + </item> + <item> + <widget class="QProgressBar" name="globalProgressBar"> + <property name="enabled"> + <bool>true</bool> </property> - <property name="text"> - <string>Skip</string> + <property name="minimumSize"> + <size> + <width>0</width> + <height>24</height> + </size> </property> - </widget> - </item> - <item row="0" column="0"> - <widget class="QLabel" name="globalStatusLabel"> - <property name="text"> - <string>Global Task Status...</string> + <property name="value"> + <number>24</number> </property> </widget> </item> - <item row="2" column="0"> - <widget class="QLabel" name="statusLabel"> + <item> + <widget class="QScrollArea" name="taskProgressScrollArea"> <property name="sizePolicy"> - <sizepolicy hsizetype="Preferred" vsizetype="MinimumExpanding"> + <sizepolicy hsizetype="Expanding" vsizetype="MinimumExpanding"> <horstretch>0</horstretch> <verstretch>0</verstretch> </sizepolicy> </property> - <property name="text"> - <string>Task Status...</string> + <property name="minimumSize"> + <size> + <width>0</width> + <height>100</height> + </size> </property> - <property name="wordWrap"> - <bool>true</bool> + <property name="frameShape"> + <enum>QFrame::StyledPanel</enum> </property> - </widget> - </item> - <item row="3" column="0"> - <widget class="QProgressBar" name="taskProgressBar"> - <property name="value"> - <number>24</number> + <property name="horizontalScrollBarPolicy"> + <enum>Qt::ScrollBarAsNeeded</enum> + </property> + <property name="sizeAdjustPolicy"> + <enum>QAbstractScrollArea::AdjustToContents</enum> </property> - <property name="textVisible"> - <bool>false</bool> + <property name="widgetResizable"> + <bool>true</bool> </property> + <widget class="QWidget" name="taskProgressContainer"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>464</width> + <height>96</height> + </rect> + </property> + <layout class="QVBoxLayout" name="taskProgressLayout"> + <property name="spacing"> + <number>2</number> + </property> + </layout> + </widget> </widget> </item> - <item row="1" column="0"> - <widget class="QProgressBar" name="globalProgressBar"> - <property name="enabled"> - <bool>true</bool> + <item> + <widget class="QPushButton" name="skipButton"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> </property> - <property name="value"> - <number>24</number> + <property name="text"> + <string>Skip</string> </property> </widget> </item> diff --git a/launcher/ui/dialogs/ResourceDownloadDialog.cpp b/launcher/ui/dialogs/ResourceDownloadDialog.cpp new file mode 100644 index 00000000..d2a8d33e --- /dev/null +++ b/launcher/ui/dialogs/ResourceDownloadDialog.cpp @@ -0,0 +1,311 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * 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 + * 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 "ResourceDownloadDialog.h" + +#include <QPushButton> + +#include "Application.h" +#include "ResourceDownloadTask.h" + +#include "minecraft/mod/ModFolderModel.h" +#include "minecraft/mod/ResourcePackFolderModel.h" +#include "minecraft/mod/TexturePackFolderModel.h" +#include "minecraft/mod/ShaderPackFolderModel.h" + +#include "ui/dialogs/ReviewMessageBox.h" + +#include "ui/pages/modplatform/ResourcePage.h" + +#include "ui/pages/modplatform/flame/FlameResourcePages.h" +#include "ui/pages/modplatform/modrinth/ModrinthResourcePages.h" + +#include "ui/widgets/PageContainer.h" + +namespace ResourceDownload { + +ResourceDownloadDialog::ResourceDownloadDialog(QWidget* parent, const std::shared_ptr<ResourceFolderModel> base_model) + : QDialog(parent), m_base_model(base_model), m_buttons(QDialogButtonBox::Help | QDialogButtonBox::Ok | QDialogButtonBox::Cancel), m_vertical_layout(this) +{ + setObjectName(QStringLiteral("ResourceDownloadDialog")); + + resize(std::max(0.5 * parent->width(), 400.0), std::max(0.75 * parent->height(), 400.0)); + + setWindowIcon(APPLICATION->getThemedIcon("new")); + + // Bonk Qt over its stupid head and make sure it understands which button is the default one... + // See: https://stackoverflow.com/questions/24556831/qbuttonbox-set-default-button + auto OkButton = m_buttons.button(QDialogButtonBox::Ok); + OkButton->setEnabled(false); + OkButton->setDefault(true); + OkButton->setAutoDefault(true); + OkButton->setText(tr("Review and confirm")); + OkButton->setShortcut(tr("Ctrl+Return")); + + auto CancelButton = m_buttons.button(QDialogButtonBox::Cancel); + CancelButton->setDefault(false); + CancelButton->setAutoDefault(false); + + auto HelpButton = m_buttons.button(QDialogButtonBox::Help); + HelpButton->setDefault(false); + HelpButton->setAutoDefault(false); + + setWindowModality(Qt::WindowModal); +} + +void ResourceDownloadDialog::accept() +{ + if (!geometrySaveKey().isEmpty()) + APPLICATION->settings()->set(geometrySaveKey(), saveGeometry().toBase64()); + + QDialog::accept(); +} + +void ResourceDownloadDialog::reject() +{ + if (!geometrySaveKey().isEmpty()) + APPLICATION->settings()->set(geometrySaveKey(), saveGeometry().toBase64()); + + QDialog::reject(); +} + +// NOTE: We can't have this in the ctor because PageContainer calls a virtual function, and so +// won't work with subclasses if we put it in this ctor. +void ResourceDownloadDialog::initializeContainer() +{ + m_container = new PageContainer(this, {}, this); + m_container->setSizePolicy(QSizePolicy::Policy::Preferred, QSizePolicy::Policy::Expanding); + m_container->layout()->setContentsMargins(0, 0, 0, 0); + m_vertical_layout.addWidget(m_container); + + m_container->addButtons(&m_buttons); + + connect(m_container, &PageContainer::selectedPageChanged, this, &ResourceDownloadDialog::selectedPageChanged); +} + +void ResourceDownloadDialog::connectButtons() +{ + auto OkButton = m_buttons.button(QDialogButtonBox::Ok); + OkButton->setToolTip(tr("Opens a new popup to review your selected %1 and confirm your selection. Shortcut: Ctrl+Return").arg(resourcesString())); + connect(OkButton, &QPushButton::clicked, this, &ResourceDownloadDialog::confirm); + + auto CancelButton = m_buttons.button(QDialogButtonBox::Cancel); + connect(CancelButton, &QPushButton::clicked, this, &ResourceDownloadDialog::reject); + + auto HelpButton = m_buttons.button(QDialogButtonBox::Help); + connect(HelpButton, &QPushButton::clicked, m_container, &PageContainer::help); +} + +void ResourceDownloadDialog::confirm() +{ + auto keys = m_selected.keys(); + keys.sort(Qt::CaseInsensitive); + + auto confirm_dialog = ReviewMessageBox::create(this, tr("Confirm %1 to download").arg(resourcesString())); + confirm_dialog->retranslateUi(resourcesString()); + + for (auto& task : keys) { + auto selected = m_selected.constFind(task).value(); + confirm_dialog->appendResource({ task, selected->getFilename(), selected->getCustomPath() }); + } + + if (confirm_dialog->exec()) { + auto deselected = confirm_dialog->deselectedResources(); + for (auto name : deselected) { + m_selected.remove(name); + } + + this->accept(); + } +} + +bool ResourceDownloadDialog::selectPage(QString pageId) +{ + return m_container->selectPage(pageId); +} + +ResourcePage* ResourceDownloadDialog::getSelectedPage() +{ + return m_selectedPage; +} + +void ResourceDownloadDialog::addResource(ModPlatform::IndexedPack& pack, ModPlatform::IndexedVersion& ver, bool is_indexed) +{ + removeResource(pack, ver); + + ver.is_currently_selected = true; + m_selected.insert(pack.name, makeShared<ResourceDownloadTask>(pack, ver, getBaseModel(), is_indexed)); + + m_buttons.button(QDialogButtonBox::Ok)->setEnabled(!m_selected.isEmpty()); +} + +static ModPlatform::IndexedVersion& getVersionWithID(ModPlatform::IndexedPack& pack, QVariant id) +{ + Q_ASSERT(pack.versionsLoaded); + auto it = std::find_if(pack.versions.begin(), pack.versions.end(), [id](auto const& v) { return v.fileId == id; }); + Q_ASSERT(it != pack.versions.end()); + return *it; +} + +void ResourceDownloadDialog::removeResource(ModPlatform::IndexedPack& pack, ModPlatform::IndexedVersion& ver) +{ + if (auto selected_task_it = m_selected.find(pack.name); selected_task_it != m_selected.end()) { + auto selected_task = *selected_task_it; + auto old_version_id = selected_task->getVersionID(); + + // If the new and old version IDs don't match, search for the old one and deselect it. + if (ver.fileId != old_version_id) + getVersionWithID(pack, old_version_id).is_currently_selected = false; + } + + // Deselect the new version too, since all versions of that pack got removed. + ver.is_currently_selected = false; + + m_selected.remove(pack.name); + + m_buttons.button(QDialogButtonBox::Ok)->setEnabled(!m_selected.isEmpty()); +} + +const QList<ResourceDownloadDialog::DownloadTaskPtr> ResourceDownloadDialog::getTasks() +{ + return m_selected.values(); +} + +void ResourceDownloadDialog::selectedPageChanged(BasePage* previous, BasePage* selected) +{ + auto* prev_page = dynamic_cast<ResourcePage*>(previous); + if (!prev_page) { + qCritical() << "Page '" << previous->displayName() << "' in ResourceDownloadDialog is not a ResourcePage!"; + return; + } + + m_selectedPage = dynamic_cast<ResourcePage*>(selected); + if (!m_selectedPage) { + qCritical() << "Page '" << selected->displayName() << "' in ResourceDownloadDialog is not a ResourcePage!"; + return; + } + + // Same effect as having a global search bar + m_selectedPage->setSearchTerm(prev_page->getSearchTerm()); +} + + + +ModDownloadDialog::ModDownloadDialog(QWidget* parent, const std::shared_ptr<ModFolderModel>& mods, BaseInstance* instance) + : ResourceDownloadDialog(parent, mods), m_instance(instance) +{ + setWindowTitle(dialogTitle()); + + initializeContainer(); + connectButtons(); + + if (!geometrySaveKey().isEmpty()) + restoreGeometry(QByteArray::fromBase64(APPLICATION->settings()->get(geometrySaveKey()).toByteArray())); +} + +QList<BasePage*> ModDownloadDialog::getPages() +{ + QList<BasePage*> pages; + + pages.append(ModrinthModPage::create(this, *m_instance)); + if (APPLICATION->capabilities() & Application::SupportsFlame) + pages.append(FlameModPage::create(this, *m_instance)); + + m_selectedPage = dynamic_cast<ModPage*>(pages[0]); + + return pages; +} + + +ResourcePackDownloadDialog::ResourcePackDownloadDialog(QWidget* parent, + const std::shared_ptr<ResourcePackFolderModel>& resource_packs, + BaseInstance* instance) + : ResourceDownloadDialog(parent, resource_packs), m_instance(instance) +{ + setWindowTitle(dialogTitle()); + + initializeContainer(); + connectButtons(); + + if (!geometrySaveKey().isEmpty()) + restoreGeometry(QByteArray::fromBase64(APPLICATION->settings()->get(geometrySaveKey()).toByteArray())); +} + +QList<BasePage*> ResourcePackDownloadDialog::getPages() +{ + QList<BasePage*> pages; + + pages.append(ModrinthResourcePackPage::create(this, *m_instance)); + if (APPLICATION->capabilities() & Application::SupportsFlame) + pages.append(FlameResourcePackPage::create(this, *m_instance)); + + return pages; +} + + +TexturePackDownloadDialog::TexturePackDownloadDialog(QWidget* parent, + const std::shared_ptr<TexturePackFolderModel>& resource_packs, + BaseInstance* instance) + : ResourceDownloadDialog(parent, resource_packs), m_instance(instance) +{ + setWindowTitle(dialogTitle()); + + initializeContainer(); + connectButtons(); + + if (!geometrySaveKey().isEmpty()) + restoreGeometry(QByteArray::fromBase64(APPLICATION->settings()->get(geometrySaveKey()).toByteArray())); +} + +QList<BasePage*> TexturePackDownloadDialog::getPages() +{ + QList<BasePage*> pages; + + pages.append(ModrinthTexturePackPage::create(this, *m_instance)); + if (APPLICATION->capabilities() & Application::SupportsFlame) + pages.append(FlameTexturePackPage::create(this, *m_instance)); + + return pages; +} + + +ShaderPackDownloadDialog::ShaderPackDownloadDialog(QWidget* parent, + const std::shared_ptr<ShaderPackFolderModel>& shaders, + BaseInstance* instance) + : ResourceDownloadDialog(parent, shaders), m_instance(instance) +{ + setWindowTitle(dialogTitle()); + + initializeContainer(); + connectButtons(); + + if (!geometrySaveKey().isEmpty()) + restoreGeometry(QByteArray::fromBase64(APPLICATION->settings()->get(geometrySaveKey()).toByteArray())); +} + +QList<BasePage*> ShaderPackDownloadDialog::getPages() +{ + QList<BasePage*> pages; + + pages.append(ModrinthShaderPackPage::create(this, *m_instance)); + + return pages; +} + +} // namespace ResourceDownload diff --git a/launcher/ui/dialogs/ResourceDownloadDialog.h b/launcher/ui/dialogs/ResourceDownloadDialog.h new file mode 100644 index 00000000..5678dc8b --- /dev/null +++ b/launcher/ui/dialogs/ResourceDownloadDialog.h @@ -0,0 +1,171 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * 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 + * 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 <QDialogButtonBox> +#include <QHash> +#include <QLayout> + +#include "QObjectPtr.h" +#include "modplatform/ModIndex.h" +#include "ui/pages/BasePageProvider.h" + +class BaseInstance; +class ModFolderModel; +class PageContainer; +class QVBoxLayout; +class QDialogButtonBox; +class ResourceDownloadTask; +class ResourceFolderModel; +class ResourcePackFolderModel; +class TexturePackFolderModel; +class ShaderPackFolderModel; + +namespace ResourceDownload { + +class ResourcePage; + +class ResourceDownloadDialog : public QDialog, public BasePageProvider { + Q_OBJECT + + public: + using DownloadTaskPtr = shared_qobject_ptr<ResourceDownloadTask>; + + ResourceDownloadDialog(QWidget* parent, const std::shared_ptr<ResourceFolderModel> base_model); + + void initializeContainer(); + void connectButtons(); + + //: String that gets appended to the download dialog title ("Download " + resourcesString()) + [[nodiscard]] virtual QString resourcesString() const { return tr("resources"); } + + QString dialogTitle() override { return tr("Download %1").arg(resourcesString()); }; + + bool selectPage(QString pageId); + ResourcePage* getSelectedPage(); + + void addResource(ModPlatform::IndexedPack&, ModPlatform::IndexedVersion&, bool is_indexed = false); + void removeResource(ModPlatform::IndexedPack&, ModPlatform::IndexedVersion&); + + const QList<DownloadTaskPtr> getTasks(); + [[nodiscard]] const std::shared_ptr<ResourceFolderModel> getBaseModel() const { return m_base_model; } + + public slots: + void accept() override; + void reject() override; + + protected slots: + void selectedPageChanged(BasePage* previous, BasePage* selected); + + virtual void confirm(); + + protected: + [[nodiscard]] virtual QString geometrySaveKey() const { return ""; } + + protected: + const std::shared_ptr<ResourceFolderModel> m_base_model; + + PageContainer* m_container = nullptr; + ResourcePage* m_selectedPage = nullptr; + + QDialogButtonBox m_buttons; + QVBoxLayout m_vertical_layout; + + QHash<QString, DownloadTaskPtr> m_selected; +}; + + + +class ModDownloadDialog final : public ResourceDownloadDialog { + Q_OBJECT + + public: + explicit ModDownloadDialog(QWidget* parent, const std::shared_ptr<ModFolderModel>& mods, BaseInstance* instance); + ~ModDownloadDialog() override = default; + + //: String that gets appended to the mod download dialog title ("Download " + resourcesString()) + [[nodiscard]] QString resourcesString() const override { return tr("mods"); } + [[nodiscard]] QString geometrySaveKey() const override { return "ModDownloadGeometry"; } + + QList<BasePage*> getPages() override; + + private: + BaseInstance* m_instance; +}; + +class ResourcePackDownloadDialog final : public ResourceDownloadDialog { + Q_OBJECT + + public: + explicit ResourcePackDownloadDialog(QWidget* parent, + const std::shared_ptr<ResourcePackFolderModel>& resource_packs, + BaseInstance* instance); + ~ResourcePackDownloadDialog() override = default; + + //: String that gets appended to the resource pack download dialog title ("Download " + resourcesString()) + [[nodiscard]] QString resourcesString() const override { return tr("resource packs"); } + [[nodiscard]] QString geometrySaveKey() const override { return "RPDownloadGeometry"; } + + QList<BasePage*> getPages() override; + + private: + BaseInstance* m_instance; +}; + +class TexturePackDownloadDialog final : public ResourceDownloadDialog { + Q_OBJECT + + public: + explicit TexturePackDownloadDialog(QWidget* parent, + const std::shared_ptr<TexturePackFolderModel>& resource_packs, + BaseInstance* instance); + ~TexturePackDownloadDialog() override = default; + + //: String that gets appended to the texture pack download dialog title ("Download " + resourcesString()) + [[nodiscard]] QString resourcesString() const override { return tr("texture packs"); } + [[nodiscard]] QString geometrySaveKey() const override { return "TPDownloadGeometry"; } + + QList<BasePage*> getPages() override; + + private: + BaseInstance* m_instance; +}; + +class ShaderPackDownloadDialog final : public ResourceDownloadDialog { + Q_OBJECT + + public: + explicit ShaderPackDownloadDialog(QWidget* parent, + const std::shared_ptr<ShaderPackFolderModel>& shader_packs, + BaseInstance* instance); + ~ShaderPackDownloadDialog() override = default; + + //: String that gets appended to the shader pack download dialog title ("Download " + resourcesString()) + [[nodiscard]] QString resourcesString() const override { return tr("shader packs"); } + [[nodiscard]] QString geometrySaveKey() const override { return "ShaderDownloadGeometry"; } + + QList<BasePage*> getPages() override; + + private: + BaseInstance* m_instance; +}; + +} // namespace ResourceDownload diff --git a/launcher/ui/dialogs/ReviewMessageBox.cpp b/launcher/ui/dialogs/ReviewMessageBox.cpp index 7c25c91c..7b2df278 100644 --- a/launcher/ui/dialogs/ReviewMessageBox.cpp +++ b/launcher/ui/dialogs/ReviewMessageBox.cpp @@ -1,6 +1,8 @@ #include "ReviewMessageBox.h" #include "ui_ReviewMessageBox.h" +#include "Application.h" + #include <QPushButton> ReviewMessageBox::ReviewMessageBox(QWidget* parent, QString const& title, QString const& icon) @@ -11,6 +13,10 @@ ReviewMessageBox::ReviewMessageBox(QWidget* parent, QString const& title, QStrin auto back_button = ui->buttonBox->button(QDialogButtonBox::Cancel); back_button->setText(tr("Back")); + ui->modTreeWidget->header()->setSectionResizeMode(0, QHeaderView::Stretch); + ui->modTreeWidget->header()->setStretchLastSection(false); + ui->modTreeWidget->header()->setSectionResizeMode(1, QHeaderView::ResizeToContents); + connect(ui->buttonBox, &QDialogButtonBox::accepted, this, &ReviewMessageBox::accept); connect(ui->buttonBox, &QDialogButtonBox::rejected, this, &ReviewMessageBox::reject); } @@ -25,7 +31,7 @@ auto ReviewMessageBox::create(QWidget* parent, QString&& title, QString&& icon) return new ReviewMessageBox(parent, title, icon); } -void ReviewMessageBox::appendMod(ModInformation&& info) +void ReviewMessageBox::appendResource(ResourceInformation&& info) { auto itemTop = new QTreeWidgetItem(ui->modTreeWidget); itemTop->setCheckState(0, Qt::CheckState::Checked); @@ -36,10 +42,20 @@ void ReviewMessageBox::appendMod(ModInformation&& info) itemTop->insertChildren(0, { filenameItem }); + if (!info.custom_file_path.isEmpty()) { + auto customPathItem = new QTreeWidgetItem(itemTop); + customPathItem->setText(0, tr("This download will be placed in: %1").arg(info.custom_file_path)); + + itemTop->insertChildren(1, { customPathItem }); + + itemTop->setIcon(1, QIcon(APPLICATION->getThemedIcon("status-yellow"))); + itemTop->setToolTip(1, tr("This file will be downloaded to a folder location different from the default, possibly due to its loader requiring it.")); + } + ui->modTreeWidget->addTopLevelItem(itemTop); } -auto ReviewMessageBox::deselectedMods() -> QStringList +auto ReviewMessageBox::deselectedResources() -> QStringList { QStringList list; @@ -55,3 +71,11 @@ auto ReviewMessageBox::deselectedMods() -> QStringList return list; } + +void ReviewMessageBox::retranslateUi(QString resources_name) +{ + setWindowTitle(tr("Confirm %1 selection").arg(resources_name)); + + ui->explainLabel->setText(tr("You're about to download the following %1:").arg(resources_name)); + ui->onlyCheckedLabel->setText(tr("Only %1 with a check will be downloaded!").arg(resources_name)); +} diff --git a/launcher/ui/dialogs/ReviewMessageBox.h b/launcher/ui/dialogs/ReviewMessageBox.h index 9cfa679a..5ec2bc23 100644 --- a/launcher/ui/dialogs/ReviewMessageBox.h +++ b/launcher/ui/dialogs/ReviewMessageBox.h @@ -12,15 +12,18 @@ class ReviewMessageBox : public QDialog { public: static auto create(QWidget* parent, QString&& title, QString&& icon = "") -> ReviewMessageBox*; - using ModInformation = struct { + using ResourceInformation = struct res_info { QString name; QString filename; + QString custom_file_path {}; }; - void appendMod(ModInformation&& info); - auto deselectedMods() -> QStringList; + void appendResource(ResourceInformation&& info); + auto deselectedResources() -> QStringList; - ~ReviewMessageBox(); + void retranslateUi(QString resources_name); + + ~ReviewMessageBox() override; protected: ReviewMessageBox(QWidget* parent, const QString& title, const QString& icon); diff --git a/launcher/ui/dialogs/ReviewMessageBox.ui b/launcher/ui/dialogs/ReviewMessageBox.ui index ab3bcc2f..bf53ae80 100644 --- a/launcher/ui/dialogs/ReviewMessageBox.ui +++ b/launcher/ui/dialogs/ReviewMessageBox.ui @@ -10,9 +10,6 @@ <height>350</height> </rect> </property> - <property name="windowTitle"> - <string>Confirm mod selection</string> - </property> <property name="sizeGripEnabled"> <bool>true</bool> </property> @@ -39,22 +36,21 @@ <string/> </property> </column> + <column> + <property name="text"> + <string/> + </property> + </column> </widget> </item> <item row="1" column="0"> <widget class="QLabel" name="explainLabel"> - <property name="text"> - <string>You're about to download the following mods:</string> - </property> </widget> </item> <item row="5" column="0" rowspan="2"> <layout class="QHBoxLayout" name="horizontalLayout"> <item> <widget class="QLabel" name="onlyCheckedLabel"> - <property name="text"> - <string>Only mods with a check will be downloaded!</string> - </property> </widget> </item> <item> diff --git a/launcher/ui/dialogs/UpdateDialog.cpp b/launcher/ui/dialogs/UpdateDialog.cpp deleted file mode 100644 index 9e82531a..00000000 --- a/launcher/ui/dialogs/UpdateDialog.cpp +++ /dev/null @@ -1,217 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-only -/* - * PolyMC - 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 "UpdateDialog.h" -#include "ui_UpdateDialog.h" -#include <QDebug> -#include "Application.h" -#include <settings/SettingsObject.h> -#include <Json.h> - -#include "BuildConfig.h" -#include "HoeDown.h" - -UpdateDialog::UpdateDialog(bool hasUpdate, QWidget *parent) : QDialog(parent), ui(new Ui::UpdateDialog) -{ - ui->setupUi(this); - auto channel = APPLICATION->settings()->get("UpdateChannel").toString(); - if(hasUpdate) - { - ui->label->setText(tr("A new %1 update is available!").arg(channel)); - } - else - { - ui->label->setText(tr("No %1 updates found. You are running the latest version.").arg(channel)); - ui->btnUpdateNow->setHidden(true); - ui->btnUpdateLater->setText(tr("Close")); - } - ui->changelogBrowser->setHtml(tr("<center><h1>Loading changelog...</h1></center>")); - loadChangelog(); - restoreGeometry(QByteArray::fromBase64(APPLICATION->settings()->get("UpdateDialogGeometry").toByteArray())); -} - -UpdateDialog::~UpdateDialog() -{ -} - -void UpdateDialog::loadChangelog() -{ - auto channel = APPLICATION->settings()->get("UpdateChannel").toString(); - dljob = new NetJob("Changelog", APPLICATION->network()); - QString url; - if(channel == "stable") - { - url = QString("https://raw.githubusercontent.com/PrismLauncher/PrismLauncher/%1/changelog.md").arg(channel); - m_changelogType = CHANGELOG_MARKDOWN; - } - else - { - url = QString("https://api.github.com/repos/PrismLauncher/PrismLauncher/compare/%1...%2").arg(BuildConfig.GIT_COMMIT, channel); - m_changelogType = CHANGELOG_COMMITS; - } - dljob->addNetAction(Net::Download::makeByteArray(QUrl(url), &changelogData)); - connect(dljob.get(), &NetJob::succeeded, this, &UpdateDialog::changelogLoaded); - connect(dljob.get(), &NetJob::failed, this, &UpdateDialog::changelogFailed); - dljob->start(); -} - -QString reprocessMarkdown(QByteArray markdown) -{ - HoeDown hoedown; - QString output = hoedown.process(markdown); - - // HACK: easier than customizing hoedown - output.replace(QRegularExpression("GH-([0-9]+)"), "<a href=\"https://github.com/PrismLauncher/PrismLauncher/issues/\\1\">GH-\\1</a>"); - qDebug() << output; - return output; -} - -QString reprocessCommits(QByteArray json) -{ - auto channel = APPLICATION->settings()->get("UpdateChannel").toString(); - try - { - QString result; - auto document = Json::requireDocument(json); - auto rootobject = Json::requireObject(document); - auto status = Json::requireString(rootobject, "status"); - auto diff_url = Json::requireString(rootobject, "html_url"); - - auto print_commits = [&]() - { - result += "<table cellspacing=0 cellpadding=2 style='border-width: 1px; border-style: solid'>"; - auto commitarray = Json::requireArray(rootobject, "commits"); - for(int i = commitarray.size() - 1; i >= 0; i--) - { - const auto & commitval = commitarray[i]; - auto commitobj = Json::requireObject(commitval); - auto parents_info = Json::ensureArray(commitobj, "parents"); - // NOTE: this ignores merge commits, because they have more than one parent - if(parents_info.size() > 1) - { - continue; - } - auto commit_url = Json::requireString(commitobj, "html_url"); - auto commit_info = Json::requireObject(commitobj, "commit"); - auto commit_message = Json::requireString(commit_info, "message"); - auto lines = commit_message.split('\n'); - QRegularExpression regexp("(?<prefix>(GH-(?<issuenr>[0-9]+))|(NOISSUE)|(SCRATCH))? *(?<rest>.*) *"); - auto match = regexp.match(lines.takeFirst(), 0, QRegularExpression::NormalMatch); - auto issuenr = match.captured("issuenr"); - auto prefix = match.captured("prefix"); - auto rest = match.captured("rest"); - result += "<tr><td>"; - if(issuenr.length()) - { - result += QString("<a href=\"https://github.com/PrismLauncher/PrismLauncher/issues/%1\">GH-%2</a>").arg(issuenr, issuenr); - } - else if(prefix.length()) - { - result += QString("<a href=\"%1\">%2</a>").arg(commit_url, prefix); - } - else - { - result += QString("<a href=\"%1\">NOISSUE</a>").arg(commit_url); - } - result += "</td>"; - lines.prepend(rest); - result += "<td><p>" + lines.join("<br />") + "</p></td></tr>"; - } - result += "</table>"; - }; - - if(status == "identical") - { - return QObject::tr("<p>There are no code changes between your current version and latest %1.</p>").arg(channel); - } - else if(status == "ahead") - { - result += QObject::tr("<p>Following commits were added since last update:</p>"); - print_commits(); - } - else if(status == "diverged") - { - auto commit_ahead = Json::requireInteger(rootobject, "ahead_by"); - auto commit_behind = Json::requireInteger(rootobject, "behind_by"); - result += QObject::tr("<p>The update removes %1 commits and adds the following %2:</p>").arg(commit_behind).arg(commit_ahead); - print_commits(); - } - result += QObject::tr("<p>You can <a href=\"%1\">look at the changes on github</a>.</p>").arg(diff_url); - return result; - } - catch (const JSONValidationError &e) - { - qWarning() << "Got an unparseable commit log from github:" << e.what(); - qDebug() << json; - } - return QString(); -} - -void UpdateDialog::changelogLoaded() -{ - QString result; - switch(m_changelogType) - { - case CHANGELOG_COMMITS: - result = reprocessCommits(changelogData); - break; - case CHANGELOG_MARKDOWN: - result = reprocessMarkdown(changelogData); - break; - } - changelogData.clear(); - ui->changelogBrowser->setHtml(result); -} - -void UpdateDialog::changelogFailed(QString reason) -{ - ui->changelogBrowser->setHtml(tr("<p align=\"center\" <span style=\"font-size:22pt;\">Failed to fetch changelog... Error: %1</span></p>").arg(reason)); -} - -void UpdateDialog::on_btnUpdateLater_clicked() -{ - reject(); -} - -void UpdateDialog::on_btnUpdateNow_clicked() -{ - done(UPDATE_NOW); -} - -void UpdateDialog::closeEvent(QCloseEvent* evt) -{ - APPLICATION->settings()->set("UpdateDialogGeometry", saveGeometry().toBase64()); - QDialog::closeEvent(evt); -} diff --git a/launcher/ui/dialogs/UpdateDialog.h b/launcher/ui/dialogs/UpdateDialog.h deleted file mode 100644 index 07cbe09f..00000000 --- a/launcher/ui/dialogs/UpdateDialog.h +++ /dev/null @@ -1,67 +0,0 @@ -/* 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 <QDialog> -#include "net/NetJob.h" - -namespace Ui -{ -class UpdateDialog; -} - -enum UpdateAction -{ - UPDATE_LATER = QDialog::Rejected, - UPDATE_NOW = QDialog::Accepted, -}; - -enum ChangelogType -{ - CHANGELOG_MARKDOWN, - CHANGELOG_COMMITS -}; - -class UpdateDialog : public QDialog -{ - Q_OBJECT - -public: - explicit UpdateDialog(bool hasUpdate = true, QWidget *parent = 0); - ~UpdateDialog(); - -public slots: - void on_btnUpdateNow_clicked(); - void on_btnUpdateLater_clicked(); - - /// Starts loading the changelog - void loadChangelog(); - - /// Slot for when the chengelog loads successfully. - void changelogLoaded(); - - /// Slot for when the chengelog fails to load... - void changelogFailed(QString reason); - -protected: - void closeEvent(QCloseEvent * ) override; - -private: - Ui::UpdateDialog *ui; - QByteArray changelogData; - NetJob::Ptr dljob; - ChangelogType m_changelogType = CHANGELOG_MARKDOWN; -}; diff --git a/launcher/ui/dialogs/UpdateDialog.ui b/launcher/ui/dialogs/UpdateDialog.ui deleted file mode 100644 index 5eb9d88a..00000000 --- a/launcher/ui/dialogs/UpdateDialog.ui +++ /dev/null @@ -1,91 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<ui version="4.0"> - <class>UpdateDialog</class> - <widget class="QDialog" name="UpdateDialog"> - <property name="geometry"> - <rect> - <x>0</x> - <y>0</y> - <width>657</width> - <height>673</height> - </rect> - </property> - <property name="windowTitle"> - <string>Launcher Update</string> - </property> - <property name="windowIcon"> - <iconset> - <normaloff>:/icons/toolbar/checkupdate</normaloff>:/icons/toolbar/checkupdate</iconset> - </property> - <layout class="QVBoxLayout" name="verticalLayout"> - <item> - <layout class="QHBoxLayout" name="horizontalLayout_2"> - <item> - <widget class="QLabel" name="label"> - <property name="font"> - <font> - <pointsize>14</pointsize> - </font> - </property> - <property name="text"> - <string/> - </property> - <property name="alignment"> - <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter</set> - </property> - <property name="buddy"> - <cstring>changelogBrowser</cstring> - </property> - </widget> - </item> - </layout> - </item> - <item> - <widget class="QTextBrowser" name="changelogBrowser"> - <property name="openExternalLinks"> - <bool>true</bool> - </property> - </widget> - </item> - <item> - <layout class="QGridLayout" name="gridLayout"> - <item row="0" column="0"> - <widget class="QPushButton" name="btnUpdateNow"> - <property name="sizePolicy"> - <sizepolicy hsizetype="Preferred" vsizetype="Fixed"> - <horstretch>0</horstretch> - <verstretch>0</verstretch> - </sizepolicy> - </property> - <property name="text"> - <string>Update now</string> - </property> - </widget> - </item> - <item row="0" column="1"> - <widget class="QPushButton" name="btnUpdateLater"> - <property name="sizePolicy"> - <sizepolicy hsizetype="Preferred" vsizetype="Fixed"> - <horstretch>0</horstretch> - <verstretch>0</verstretch> - </sizepolicy> - </property> - <property name="text"> - <string>Don't update yet</string> - </property> - </widget> - </item> - </layout> - </item> - </layout> - </widget> - <tabstops> - <tabstop>changelogBrowser</tabstop> - <tabstop>btnUpdateNow</tabstop> - <tabstop>btnUpdateLater</tabstop> - </tabstops> - <resources> - <include location="../../resources/multimc/multimc.qrc"/> - </resources> - <connections/> -</ui> diff --git a/launcher/ui/pages/global/APIPage.cpp b/launcher/ui/pages/global/APIPage.cpp index e3d30475..f662ee1c 100644 --- a/launcher/ui/pages/global/APIPage.cpp +++ b/launcher/ui/pages/global/APIPage.cpp @@ -1,9 +1,10 @@ // 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 Lenny McLennington <lenny@sneed.church> + * Copyright (C) 2023 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 @@ -147,6 +148,8 @@ void APIPage::loadSettings() ui->metaURL->setText(metaURL); QString flameKey = s->get("FlameKeyOverride").toString(); ui->flameKey->setText(flameKey); + QString modrinthToken = s->get("ModrinthToken").toString(); + ui->modrinthToken->setText(modrinthToken); QString customUserAgent = s->get("UserAgentOverride").toString(); ui->userAgentLineEdit->setText(customUserAgent); } @@ -177,6 +180,8 @@ void APIPage::applySettings() s->set("MetaURLOverride", metaURL); QString flameKey = ui->flameKey->text(); s->set("FlameKeyOverride", flameKey); + QString modrinthToken = ui->modrinthToken->text(); + s->set("ModrinthToken", modrinthToken); s->set("UserAgentOverride", ui->userAgentLineEdit->text()); } diff --git a/launcher/ui/pages/global/APIPage.ui b/launcher/ui/pages/global/APIPage.ui index d56a9ef6..40b89d91 100644 --- a/launcher/ui/pages/global/APIPage.ui +++ b/launcher/ui/pages/global/APIPage.ui @@ -196,24 +196,69 @@ </widget> </item> <item> - <widget class="QGroupBox" name="groupBox_flame"> + <widget class="QGroupBox" name="groupBox_modrinth"> <property name="enabled"> <bool>true</bool> </property> <property name="title"> - <string>&CurseForge Core API</string> + <string>&Modrinth API</string> </property> <layout class="QGridLayout" name="gridLayout"> <item row="0" column="0"> <widget class="QLabel" name="label_8"> <property name="text"> - <string>Note: you probably don't need to set this if CurseForge already works.</string> + <string><html><head/><body><p>Note: you only need to set this to access private data. Read the <a href="https://docs.modrinth.com/api-spec/#section/Authentication">documentation</a> for more information.</p></body></html></string> </property> </widget> </item> <item row="2" column="0"> <widget class="QLabel" name="label_7"> <property name="text"> + <string>Enter a custom API token for Modrinth here.</string> + </property> + <property name="textFormat"> + <enum>Qt::RichText</enum> + </property> + <property name="wordWrap"> + <bool>true</bool> + </property> + <property name="openExternalLinks"> + <bool>true</bool> + </property> + </widget> + </item> + <item row="1" column="0"> + <widget class="QLineEdit" name="modrinthToken"> + <property name="enabled"> + <bool>true</bool> + </property> + <property name="placeholderText"> + <string>(None)</string> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item> + <widget class="QGroupBox" name="groupBox_flame"> + <property name="enabled"> + <bool>true</bool> + </property> + <property name="title"> + <string>&CurseForge Core API</string> + </property> + <layout class="QGridLayout" name="gridLayout_2"> + <item row="0" column="0"> + <widget class="QLabel" name="label_10"> + <property name="text"> + <string>Note: you probably don't need to set this if CurseForge already works.</string> + </property> + </widget> + </item> + <item row="2" column="0"> + <widget class="QLabel" name="label_9"> + <property name="text"> <string>Enter a custom API Key for CurseForge here.</string> </property> <property name="textFormat"> diff --git a/launcher/ui/pages/global/LauncherPage.cpp b/launcher/ui/pages/global/LauncherPage.cpp index cae0635f..816dde72 100644 --- a/launcher/ui/pages/global/LauncherPage.cpp +++ b/launcher/ui/pages/global/LauncherPage.cpp @@ -44,14 +44,13 @@ #include <QTextCharFormat> #include <QMenuBar> -#include "updater/UpdateChecker.h" - #include "settings/SettingsObject.h" #include <FileSystem.h> #include "Application.h" #include "BuildConfig.h" #include "DesktopServices.h" #include "ui/themes/ITheme.h" +#include "updater/ExternalUpdater.h" #include <QApplication> #include <QProcess> @@ -80,32 +79,12 @@ LauncherPage::LauncherPage(QWidget *parent) : QWidget(parent), ui(new Ui::Launch m_languageModel = APPLICATION->translations(); loadSettings(); - if(BuildConfig.UPDATER_ENABLED) - { - QObject::connect(APPLICATION->updateChecker().get(), &UpdateChecker::channelListLoaded, this, &LauncherPage::refreshUpdateChannelList); - - if (APPLICATION->updateChecker()->hasChannels()) - { - refreshUpdateChannelList(); - } - else - { - APPLICATION->updateChecker()->updateChanList(false); - } + ui->updateSettingsBox->setHidden(!APPLICATION->updater()); - if (APPLICATION->updateChecker()->getExternalUpdater()) - { - ui->updateChannelComboBox->setVisible(false); - ui->updateChannelDescLabel->setVisible(false); - ui->updateChannelLabel->setVisible(false); - } - } - else - { - ui->updateSettingsBox->setHidden(true); - } connect(ui->fontSizeBox, SIGNAL(valueChanged(int)), SLOT(refreshFontPreview())); connect(ui->consoleFont, SIGNAL(currentFontChanged(QFont)), SLOT(refreshFontPreview())); + + connect(ui->themeCustomizationWidget, &ThemeCustomizationWidget::currentCatChanged, APPLICATION, &Application::currentCatChanged); } LauncherPage::~LauncherPage() @@ -161,8 +140,8 @@ void LauncherPage::on_instDirBrowseBtn_clicked() if (result == QMessageBox::Ok) { ui->instDirTextBox->setText(cooked_dir); - } - } + } + } else { ui->instDirTextBox->setText(cooked_dir); @@ -181,6 +160,7 @@ void LauncherPage::on_iconsDirBrowseBtn_clicked() ui->iconsDirTextBox->setText(cooked_dir); } } + void LauncherPage::on_modsDirBrowseBtn_clicked() { QString raw_dir = QFileDialog::getExistingDirectory(this, tr("Mods Folder"), ui->modsDirTextBox->text()); @@ -193,81 +173,20 @@ void LauncherPage::on_modsDirBrowseBtn_clicked() } } -void LauncherPage::on_metadataDisableBtn_clicked() +void LauncherPage::on_downloadsDirBrowseBtn_clicked() { - ui->metadataWarningLabel->setHidden(!ui->metadataDisableBtn->isChecked()); -} + QString raw_dir = QFileDialog::getExistingDirectory(this, tr("Downloads Folder"), ui->downloadsDirTextBox->text()); -void LauncherPage::refreshUpdateChannelList() -{ - // Stop listening for selection changes. It's going to change a lot while we update it and - // we don't need to update the - // description label constantly. - QObject::disconnect(ui->updateChannelComboBox, SIGNAL(currentIndexChanged(int)), this, - SLOT(updateChannelSelectionChanged(int))); - - QList<UpdateChecker::ChannelListEntry> channelList = APPLICATION->updateChecker()->getChannelList(); - ui->updateChannelComboBox->clear(); - int selection = -1; - for (int i = 0; i < channelList.count(); i++) + if (!raw_dir.isEmpty() && QDir(raw_dir).exists()) { - UpdateChecker::ChannelListEntry entry = channelList.at(i); - - // When it comes to selection, we'll rely on the indexes of a channel entry being the - // same in the - // combo box as it is in the update checker's channel list. - // This probably isn't very safe, but the channel list doesn't change often enough (or - // at all) for - // this to be a big deal. Hope it doesn't break... - ui->updateChannelComboBox->addItem(entry.name); - - // If the update channel we just added was the selected one, set the current index in - // the combo box to it. - if (entry.id == m_currentUpdateChannel) - { - qDebug() << "Selected index" << i << "channel id" << m_currentUpdateChannel; - selection = i; - } + QString cooked_dir = FS::NormalizePath(raw_dir); + ui->downloadsDirTextBox->setText(cooked_dir); } - - ui->updateChannelComboBox->setCurrentIndex(selection); - - // Start listening for selection changes again and update the description label. - QObject::connect(ui->updateChannelComboBox, SIGNAL(currentIndexChanged(int)), this, - SLOT(updateChannelSelectionChanged(int))); - refreshUpdateChannelDesc(); - - // Now that we've updated the channel list, we can enable the combo box. - // It starts off disabled so that if the channel list hasn't been loaded, it will be - // disabled. - ui->updateChannelComboBox->setEnabled(true); -} - -void LauncherPage::updateChannelSelectionChanged(int index) -{ - refreshUpdateChannelDesc(); } -void LauncherPage::refreshUpdateChannelDesc() +void LauncherPage::on_metadataDisableBtn_clicked() { - // Get the channel list. - QList<UpdateChecker::ChannelListEntry> channelList = APPLICATION->updateChecker()->getChannelList(); - int selectedIndex = ui->updateChannelComboBox->currentIndex(); - if (selectedIndex < 0) - { - return; - } - if (selectedIndex < channelList.count()) - { - // Find the channel list entry with the given index. - UpdateChecker::ChannelListEntry selected = channelList.at(selectedIndex); - - // Set the description text. - ui->updateChannelDescLabel->setText(selected.description); - - // Set the currently selected channel ID. - m_currentUpdateChannel = selected.id; - } + ui->metadataWarningLabel->setHidden(!ui->metadataDisableBtn->isChecked()); } void LauncherPage::applySettings() @@ -275,82 +194,9 @@ void LauncherPage::applySettings() auto s = APPLICATION->settings(); // Updates - if (BuildConfig.UPDATER_ENABLED && APPLICATION->updateChecker()->getExternalUpdater()) + if (APPLICATION->updater()) { - APPLICATION->updateChecker()->getExternalUpdater()->setAutomaticallyChecksForUpdates( - ui->autoUpdateCheckBox->isChecked()); - } - else - { - s->set("AutoUpdate", ui->autoUpdateCheckBox->isChecked()); - } - - s->set("UpdateChannel", m_currentUpdateChannel); - auto original = s->get("IconTheme").toString(); - //FIXME: make generic - switch (ui->themeComboBox->currentIndex()) - { - case 0: - s->set("IconTheme", "pe_colored"); - break; - case 1: - s->set("IconTheme", "pe_light"); - break; - case 2: - s->set("IconTheme", "pe_dark"); - break; - case 3: - s->set("IconTheme", "pe_blue"); - break; - case 4: - s->set("IconTheme", "breeze_light"); - break; - case 5: - s->set("IconTheme", "breeze_dark"); - break; - case 6: - s->set("IconTheme", "OSX"); - break; - case 7: - s->set("IconTheme", "iOS"); - break; - case 8: - s->set("IconTheme", "flat"); - break; - case 9: - s->set("IconTheme", "flat_white"); - break; - case 10: - s->set("IconTheme", "multimc"); - break; - case 11: - s->set("IconTheme", "custom"); - break; - } - - if(original != s->get("IconTheme")) - { - APPLICATION->setIconTheme(s->get("IconTheme").toString()); - } - - auto originalAppTheme = s->get("ApplicationTheme").toString(); - auto newAppTheme = ui->themeComboBoxColors->currentData().toString(); - if(originalAppTheme != newAppTheme) - { - s->set("ApplicationTheme", newAppTheme); - APPLICATION->setApplicationTheme(newAppTheme, false); - } - - switch (ui->themeBackgroundCat->currentIndex()) { - case 0: // original cat - s->set("BackgroundCat", "kitteh"); - break; - case 1: // rory the cat - s->set("BackgroundCat", "rory"); - break; - case 2: // rory the cat flat edition - s->set("BackgroundCat", "rory-flat"); - break; + APPLICATION->updater()->setAutomaticallyChecksForUpdates(ui->autoUpdateCheckBox->isChecked()); } s->set("MenuBarInsteadOfToolBar", ui->preferMenuBarCheckBox->isChecked()); @@ -370,6 +216,8 @@ void LauncherPage::applySettings() s->set("InstanceDir", ui->instDirTextBox->text()); s->set("CentralModsDir", ui->modsDirTextBox->text()); s->set("IconsDir", ui->iconsDirTextBox->text()); + s->set("DownloadsDir", ui->downloadsDirTextBox->text()); + s->set("DownloadsDirWatchRecursive", ui->downloadsDirWatchRecursiveCheckBox->isChecked()); auto sortMode = (InstSortMode)ui->sortingModeGroup->checkedId(); switch (sortMode) @@ -390,56 +238,11 @@ void LauncherPage::loadSettings() { auto s = APPLICATION->settings(); // Updates - if (BuildConfig.UPDATER_ENABLED && APPLICATION->updateChecker()->getExternalUpdater()) - { - ui->autoUpdateCheckBox->setChecked( - APPLICATION->updateChecker()->getExternalUpdater()->getAutomaticallyChecksForUpdates()); - } - else + if (APPLICATION->updater()) { - ui->autoUpdateCheckBox->setChecked(s->get("AutoUpdate").toBool()); + ui->autoUpdateCheckBox->setChecked(APPLICATION->updater()->getAutomaticallyChecksForUpdates()); } - m_currentUpdateChannel = s->get("UpdateChannel").toString(); - //FIXME: make generic - auto theme = s->get("IconTheme").toString(); - QStringList iconThemeOptions{"pe_colored", - "pe_light", - "pe_dark", - "pe_blue", - "breeze_light", - "breeze_dark", - "OSX", - "iOS", - "flat", - "flat_white", - "multimc", - "custom"}; - ui->themeComboBox->setCurrentIndex(iconThemeOptions.indexOf(theme)); - - auto cat = s->get("BackgroundCat").toString(); - if (cat == "kitteh") { - ui->themeBackgroundCat->setCurrentIndex(0); - } else if (cat == "rory") { - ui->themeBackgroundCat->setCurrentIndex(1); - } else if (cat == "rory-flat") { - ui->themeBackgroundCat->setCurrentIndex(2); - } - - { - auto currentTheme = s->get("ApplicationTheme").toString(); - auto themes = APPLICATION->getValidApplicationThemes(); - int idx = 0; - for(auto &theme: themes) - { - ui->themeComboBoxColors->addItem(theme->name(), theme->id()); - if(currentTheme == theme->id()) - { - ui->themeComboBoxColors->setCurrentIndex(idx); - } - idx++; - } - } // Toolbar/menu bar settings (not applicable if native menu bar is present) ui->toolsBox->setEnabled(!QMenuBar().isNativeMenuBar()); @@ -471,6 +274,8 @@ void LauncherPage::loadSettings() ui->instDirTextBox->setText(s->get("InstanceDir").toString()); ui->modsDirTextBox->setText(s->get("CentralModsDir").toString()); ui->iconsDirTextBox->setText(s->get("IconsDir").toString()); + ui->downloadsDirTextBox->setText(s->get("DownloadsDir").toString()); + ui->downloadsDirWatchRecursiveCheckBox->setChecked(s->get("DownloadsDirWatchRecursive").toBool()); QString sortMode = s->get("InstSortMode").toString(); diff --git a/launcher/ui/pages/global/LauncherPage.h b/launcher/ui/pages/global/LauncherPage.h index f38c922e..33f66f1b 100644 --- a/launcher/ui/pages/global/LauncherPage.h +++ b/launcher/ui/pages/global/LauncherPage.h @@ -88,25 +88,14 @@ slots: void on_instDirBrowseBtn_clicked(); void on_modsDirBrowseBtn_clicked(); void on_iconsDirBrowseBtn_clicked(); + void on_downloadsDirBrowseBtn_clicked(); void on_metadataDisableBtn_clicked(); /*! - * Updates the list of update channels in the combo box. - */ - void refreshUpdateChannelList(); - - /*! - * Updates the channel description label. - */ - void refreshUpdateChannelDesc(); - - /*! * Updates the font preview */ void refreshFontPreview(); - void updateChannelSelectionChanged(int index); - private: Ui::LauncherPage *ui; diff --git a/launcher/ui/pages/global/LauncherPage.ui b/launcher/ui/pages/global/LauncherPage.ui index c44718a1..55bd3eea 100644 --- a/launcher/ui/pages/global/LauncherPage.ui +++ b/launcher/ui/pages/global/LauncherPage.ui @@ -6,7 +6,7 @@ <rect> <x>0</x> <y>0</y> - <width>514</width> + <width>511</width> <height>629</height> </rect> </property> @@ -58,42 +58,54 @@ </property> </widget> </item> - <item> - <widget class="QLabel" name="updateChannelLabel"> + </layout> + </widget> + </item> + <item> + <widget class="QGroupBox" name="foldersBox"> + <property name="title"> + <string>Folders</string> + </property> + <layout class="QGridLayout" name="foldersBoxLayout"> + <item row="3" column="0"> + <widget class="QLabel" name="labelDownloadsDir"> <property name="text"> - <string>Up&date Channel:</string> + <string>&Downloads:</string> </property> <property name="buddy"> - <cstring>updateChannelComboBox</cstring> + <cstring>downloadsDirTextBox</cstring> </property> </widget> </item> - <item> - <widget class="QComboBox" name="updateChannelComboBox"> - <property name="enabled"> - <bool>false</bool> + <item row="0" column="0"> + <widget class="QLabel" name="labelInstDir"> + <property name="text"> + <string>I&nstances:</string> + </property> + <property name="buddy"> + <cstring>instDirTextBox</cstring> </property> </widget> </item> - <item> - <widget class="QLabel" name="updateChannelDescLabel"> + <item row="0" column="1"> + <widget class="QLineEdit" name="instDirTextBox"/> + </item> + <item row="3" column="1"> + <widget class="QLineEdit" name="downloadsDirTextBox"/> + </item> + <item row="2" column="1"> + <widget class="QLineEdit" name="iconsDirTextBox"/> + </item> + <item row="3" column="2"> + <widget class="QToolButton" name="downloadsDirBrowseBtn"> <property name="text"> - <string>No channel selected.</string> - </property> - <property name="wordWrap"> - <bool>true</bool> + <string notr="true">...</string> </property> </widget> </item> - </layout> - </widget> - </item> - <item> - <widget class="QGroupBox" name="foldersBox"> - <property name="title"> - <string>Folders</string> - </property> - <layout class="QGridLayout" name="foldersBoxLayout"> + <item row="1" column="1"> + <widget class="QLineEdit" name="modsDirTextBox"/> + </item> <item row="1" column="2"> <widget class="QToolButton" name="modsDirBrowseBtn"> <property name="text"> @@ -101,6 +113,16 @@ </property> </widget> </item> + <item row="1" column="0"> + <widget class="QLabel" name="labelModsDir"> + <property name="text"> + <string>&Mods:</string> + </property> + <property name="buddy"> + <cstring>modsDirTextBox</cstring> + </property> + </widget> + </item> <item row="0" column="2"> <widget class="QToolButton" name="instDirBrowseBtn"> <property name="text"> @@ -115,9 +137,6 @@ </property> </widget> </item> - <item row="0" column="1"> - <widget class="QLineEdit" name="instDirTextBox"/> - </item> <item row="2" column="0"> <widget class="QLabel" name="labelIconsDir"> <property name="text"> @@ -128,29 +147,13 @@ </property> </widget> </item> - <item row="1" column="1"> - <widget class="QLineEdit" name="modsDirTextBox"/> - </item> - <item row="0" column="0"> - <widget class="QLabel" name="labelInstDir"> - <property name="text"> - <string>I&nstances:</string> - </property> - <property name="buddy"> - <cstring>instDirTextBox</cstring> + <item row="4" column="1" colspan="2"> + <widget class="QCheckBox" name="downloadsDirWatchRecursiveCheckBox"> + <property name="toolTip"> + <string>When enabled, in addition to the downloads folder, its sub folders will also be searched when looking for resources (e.g. when looking for blocked mods on CurseForge).</string> </property> - </widget> - </item> - <item row="2" column="1"> - <widget class="QLineEdit" name="iconsDirTextBox"/> - </item> - <item row="1" column="0"> - <widget class="QLabel" name="labelModsDir"> <property name="text"> - <string>&Mods:</string> - </property> - <property name="buddy"> - <cstring>modsDirTextBox</cstring> + <string>Check downloads folder recursively</string> </property> </widget> </item> @@ -243,150 +246,9 @@ <property name="title"> <string>Theme</string> </property> - <layout class="QFormLayout" name="formLayout"> - <item row="0" column="0"> - <widget class="QLabel" name="label_3"> - <property name="text"> - <string>&Icons</string> - </property> - <property name="buddy"> - <cstring>themeComboBox</cstring> - </property> - </widget> - </item> - <item row="0" column="1"> - <widget class="QComboBox" name="themeComboBox"> - <property name="sizePolicy"> - <sizepolicy hsizetype="Expanding" vsizetype="Fixed"> - <horstretch>0</horstretch> - <verstretch>0</verstretch> - </sizepolicy> - </property> - <property name="focusPolicy"> - <enum>Qt::StrongFocus</enum> - </property> - <item> - <property name="text"> - <string>Simple (Colored Icons)</string> - </property> - </item> - <item> - <property name="text"> - <string>Simple (Light Icons)</string> - </property> - </item> - <item> - <property name="text"> - <string>Simple (Dark Icons)</string> - </property> - </item> - <item> - <property name="text"> - <string>Simple (Blue Icons)</string> - </property> - </item> - <item> - <property name="text"> - <string>Breeze Light</string> - </property> - </item> - <item> - <property name="text"> - <string>Breeze Dark</string> - </property> - </item> - <item> - <property name="text"> - <string notr="true">OSX</string> - </property> - </item> - <item> - <property name="text"> - <string notr="true">iOS</string> - </property> - </item> - <item> - <property name="text"> - <string>Flat</string> - </property> - </item> - <item> - <property name="text"> - <string>Flat (White)</string> - </property> - </item> - <item> - <property name="text"> - <string>Legacy</string> - </property> - </item> - <item> - <property name="text"> - <string>Custom</string> - </property> - </item> - </widget> - </item> - <item row="1" column="0"> - <widget class="QLabel" name="label_4"> - <property name="text"> - <string>&Colors</string> - </property> - <property name="buddy"> - <cstring>themeComboBoxColors</cstring> - </property> - </widget> - </item> - <item row="1" column="1"> - <widget class="QComboBox" name="themeComboBoxColors"> - <property name="sizePolicy"> - <sizepolicy hsizetype="Expanding" vsizetype="Fixed"> - <horstretch>0</horstretch> - <verstretch>0</verstretch> - </sizepolicy> - </property> - <property name="focusPolicy"> - <enum>Qt::StrongFocus</enum> - </property> - </widget> - </item> - <item row="2" column="0"> - <widget class="QLabel" name="label_5"> - <property name="text"> - <string>C&at</string> - </property> - <property name="buddy"> - <cstring>themeBackgroundCat</cstring> - </property> - </widget> - </item> - <item row="2" column="1"> - <widget class="QComboBox" name="themeBackgroundCat"> - <property name="sizePolicy"> - <sizepolicy hsizetype="Expanding" vsizetype="Fixed"> - <horstretch>0</horstretch> - <verstretch>0</verstretch> - </sizepolicy> - </property> - <property name="focusPolicy"> - <enum>Qt::StrongFocus</enum> - </property> - <item> - <property name="text"> - <string>Background Cat (from MultiMC)</string> - </property> - </item> - <item> - <property name="text"> - <string>Rory ID 11 (drawn by Ashtaka)</string> - </property> - </item> - <item> - <property name="text"> - <string>Rory ID 11 (flat edition, drawn by Ashtaka)</string> - </property> - </item> - </widget> + <layout class="QVBoxLayout" name="verticalLayout_5"> + <item> + <widget class="ThemeCustomizationWidget" name="themeCustomizationWidget" native="true"/> </item> </layout> </widget> @@ -570,10 +432,17 @@ </item> </layout> </widget> + <customwidgets> + <customwidget> + <class>ThemeCustomizationWidget</class> + <extends>QWidget</extends> + <header>ui/widgets/ThemeCustomizationWidget.h</header> + <container>1</container> + </customwidget> + </customwidgets> <tabstops> <tabstop>tabWidget</tabstop> <tabstop>autoUpdateCheckBox</tabstop> - <tabstop>updateChannelComboBox</tabstop> <tabstop>instDirTextBox</tabstop> <tabstop>instDirBrowseBtn</tabstop> <tabstop>modsDirTextBox</tabstop> @@ -582,8 +451,6 @@ <tabstop>iconsDirBrowseBtn</tabstop> <tabstop>sortLastLaunchedBtn</tabstop> <tabstop>sortByNameBtn</tabstop> - <tabstop>themeComboBox</tabstop> - <tabstop>themeComboBoxColors</tabstop> <tabstop>showConsoleCheck</tabstop> <tabstop>autoCloseConsoleCheck</tabstop> <tabstop>showConsoleErrorCheck</tabstop> diff --git a/launcher/ui/pages/global/MinecraftPage.cpp b/launcher/ui/pages/global/MinecraftPage.cpp index cc597fe0..eca3e865 100644 --- a/launcher/ui/pages/global/MinecraftPage.cpp +++ b/launcher/ui/pages/global/MinecraftPage.cpp @@ -46,7 +46,6 @@ MinecraftPage::MinecraftPage(QWidget *parent) : QWidget(parent), ui(new Ui::MinecraftPage) { ui->setupUi(this); - ui->tabWidget->tabBar()->hide(); loadSettings(); updateCheckboxStuff(); } diff --git a/launcher/ui/pages/global/MinecraftPage.ui b/launcher/ui/pages/global/MinecraftPage.ui index 640f436d..103881b5 100644 --- a/launcher/ui/pages/global/MinecraftPage.ui +++ b/launcher/ui/pages/global/MinecraftPage.ui @@ -7,7 +7,7 @@ <x>0</x> <y>0</y> <width>936</width> - <height>1134</height> + <height>541</height> </rect> </property> <property name="sizePolicy"> @@ -39,7 +39,7 @@ </property> <widget class="QWidget" name="minecraftTab"> <attribute name="title"> - <string notr="true">Minecraft</string> + <string notr="true">General</string> </attribute> <layout class="QVBoxLayout" name="verticalLayout_3"> <item> @@ -112,22 +112,29 @@ </widget> </item> <item> - <widget class="QGroupBox" name="nativeLibWorkaroundGroupBox"> + <widget class="QGroupBox" name="gameTimeGroupBox"> <property name="title"> - <string>Native library workarounds</string> + <string>Game time</string> </property> - <layout class="QVBoxLayout" name="verticalLayout_5"> + <layout class="QVBoxLayout" name="verticalLayout_6"> <item> - <widget class="QCheckBox" name="useNativeGLFWCheck"> + <widget class="QCheckBox" name="showGameTime"> <property name="text"> - <string>Use system installation of &GLFW</string> + <string>Show time spent &playing instances</string> </property> </widget> </item> <item> - <widget class="QCheckBox" name="useNativeOpenALCheck"> + <widget class="QCheckBox" name="showGlobalGameTime"> <property name="text"> - <string>Use system installation of &OpenAL</string> + <string>Show time spent playing across &all instances</string> + </property> + </widget> + </item> + <item> + <widget class="QCheckBox" name="recordGameTime"> + <property name="text"> + <string>&Record time spent playing instances</string> </property> </widget> </item> @@ -135,38 +142,28 @@ </widget> </item> <item> - <widget class="QGroupBox" name="perfomanceGroupBox"> + <widget class="QGroupBox" name="groupBox"> <property name="title"> - <string>Performance</string> + <string>Miscellaneous</string> </property> - <layout class="QVBoxLayout" name="verticalLayout_2"> - <item> - <widget class="QCheckBox" name="enableFeralGamemodeCheck"> - <property name="toolTip"> - <string><html><head/><body><p>Enable Feral Interactive's GameMode, to potentially improve gaming performance.</p></body></html></string> - </property> - <property name="text"> - <string>Enable Feral GameMode</string> - </property> - </widget> - </item> + <layout class="QVBoxLayout" name="verticalLayout"> <item> - <widget class="QCheckBox" name="enableMangoHud"> + <widget class="QCheckBox" name="closeAfterLaunchCheck"> <property name="toolTip"> - <string><html><head/><body><p>Enable MangoHud's advanced performance overlay.</p></body></html></string> + <string><html><head/><body><p>The launcher will automatically reopen when the game crashes or exits.</p></body></html></string> </property> <property name="text"> - <string>Enable MangoHud</string> + <string>&Close the launcher after game window opens</string> </property> </widget> </item> <item> - <widget class="QCheckBox" name="useDiscreteGpuCheck"> + <widget class="QCheckBox" name="quitAfterGameStopCheck"> <property name="toolTip"> - <string><html><head/><body><p>Use the discrete GPU instead of the primary GPU.</p></body></html></string> + <string><html><head/><body><p>The launcher will automatically quit after the game exits or crashes.</p></body></html></string> </property> <property name="text"> - <string>Use discrete GPU</string> + <string>&Quit the launcher after game window closes</string> </property> </widget> </item> @@ -174,29 +171,42 @@ </widget> </item> <item> - <widget class="QGroupBox" name="gameTimeGroupBox"> + <spacer name="verticalSpacerMinecraft"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>0</width> + <height>0</height> + </size> + </property> + </spacer> + </item> + </layout> + </widget> + <widget class="QWidget" name="tab"> + <attribute name="title"> + <string>Tweaks</string> + </attribute> + <layout class="QVBoxLayout" name="verticalLayout_12"> + <item> + <widget class="QGroupBox" name="nativeLibWorkaroundGroupBox"> <property name="title"> - <string>Game time</string> + <string>Native library workarounds</string> </property> - <layout class="QVBoxLayout" name="verticalLayout_6"> + <layout class="QVBoxLayout" name="verticalLayout_11"> <item> - <widget class="QCheckBox" name="showGameTime"> - <property name="text"> - <string>Show time spent &playing instances</string> - </property> - </widget> - </item> - <item> - <widget class="QCheckBox" name="showGlobalGameTime"> + <widget class="QCheckBox" name="useNativeGLFWCheck"> <property name="text"> - <string>Show time spent playing across &all instances</string> + <string>Use system installation of &GLFW</string> </property> </widget> </item> <item> - <widget class="QCheckBox" name="recordGameTime"> + <widget class="QCheckBox" name="useNativeOpenALCheck"> <property name="text"> - <string>&Record time spent playing instances</string> + <string>Use system installation of &OpenAL</string> </property> </widget> </item> @@ -204,28 +214,38 @@ </widget> </item> <item> - <widget class="QGroupBox" name="groupBox"> + <widget class="QGroupBox" name="perfomanceGroupBox"> <property name="title"> - <string>Miscellaneous</string> + <string>Performance</string> </property> - <layout class="QVBoxLayout" name="verticalLayout"> + <layout class="QVBoxLayout" name="verticalLayout_2"> <item> - <widget class="QCheckBox" name="closeAfterLaunchCheck"> + <widget class="QCheckBox" name="enableFeralGamemodeCheck"> <property name="toolTip"> - <string><html><head/><body><p>The launcher will automatically reopen when the game crashes or exits.</p></body></html></string> + <string><html><head/><body><p>Enable Feral Interactive's GameMode, to potentially improve gaming performance.</p></body></html></string> </property> <property name="text"> - <string>&Close the launcher after game window opens</string> + <string>Enable Feral GameMode</string> </property> </widget> </item> <item> - <widget class="QCheckBox" name="quitAfterGameStopCheck"> + <widget class="QCheckBox" name="enableMangoHud"> <property name="toolTip"> - <string><html><head/><body><p>The launcher will automatically quit after the game exits or crashes.</p></body></html></string> + <string><html><head/><body><p>Enable MangoHud's advanced performance overlay.</p></body></html></string> </property> <property name="text"> - <string>&Quit the launcher after game window closes</string> + <string>Enable MangoHud</string> + </property> + </widget> + </item> + <item> + <widget class="QCheckBox" name="useDiscreteGpuCheck"> + <property name="toolTip"> + <string><html><head/><body><p>Use the discrete GPU instead of the primary GPU.</p></body></html></string> + </property> + <property name="text"> + <string>Use discrete GPU</string> </property> </widget> </item> @@ -233,14 +253,14 @@ </widget> </item> <item> - <spacer name="verticalSpacerMinecraft"> + <spacer name="verticalSpacer"> <property name="orientation"> <enum>Qt::Vertical</enum> </property> <property name="sizeHint" stdset="0"> <size> - <width>0</width> - <height>0</height> + <width>20</width> + <height>40</height> </size> </property> </spacer> @@ -255,11 +275,6 @@ <tabstop>maximizedCheckBox</tabstop> <tabstop>windowWidthSpinBox</tabstop> <tabstop>windowHeightSpinBox</tabstop> - <tabstop>useNativeGLFWCheck</tabstop> - <tabstop>useNativeOpenALCheck</tabstop> - <tabstop>enableFeralGamemodeCheck</tabstop> - <tabstop>enableMangoHud</tabstop> - <tabstop>useDiscreteGpuCheck</tabstop> </tabstops> <resources/> <connections/> diff --git a/launcher/ui/pages/instance/ExternalResourcesPage.cpp b/launcher/ui/pages/instance/ExternalResourcesPage.cpp index c66d1368..1115ddc3 100644 --- a/launcher/ui/pages/instance/ExternalResourcesPage.cpp +++ b/launcher/ui/pages/instance/ExternalResourcesPage.cpp @@ -1,4 +1,5 @@ #include "ExternalResourcesPage.h" +#include "ui/dialogs/CustomMessageBox.h" #include "ui_ExternalResourcesPage.h" #include "DesktopServices.h" @@ -128,7 +129,7 @@ bool ExternalResourcesPage::eventFilter(QObject* obj, QEvent* ev) { if (ev->type() != QEvent::KeyPress) return QWidget::eventFilter(obj, ev); - + QKeyEvent* keyEvent = static_cast<QKeyEvent*>(ev); if (obj == ui->treeView) return listFilter(keyEvent); @@ -140,7 +141,6 @@ void ExternalResourcesPage::addItem() { if (!m_controlsEnabled) return; - auto list = GuiUtil::BrowseForFiles( helpPage(), tr("Select %1", "Select whatever type of files the page contains. Example: 'Loader Mods'").arg(displayName()), @@ -157,8 +157,50 @@ void ExternalResourcesPage::removeItem() { if (!m_controlsEnabled) return; - + auto selection = m_filterModel->mapSelectionToSource(ui->treeView->selectionModel()->selection()); + + int count = 0; + bool folder = false; + for (auto& i : selection.indexes()) { + if (i.column() == 0) { + count++; + + // if a folder is selected, show the confirmation dialog + if (m_model->at(i.row()).fileinfo().isDir()) + folder = true; + } + } + + QString text; + bool multiple = count > 1; + + if (multiple) { + text = tr("You are about to remove %1 items.\n" + "This may be permanent and they will be gone from the folder.\n\n" + "Are you sure?") + .arg(count); + } else if (folder) { + text = tr("You are about to remove the folder \"%1\".\n" + "This may be permanent and it will be gone from the parent folder.\n\n" + "Are you sure?") + .arg(m_model->at(selection.indexes().at(0).row()).fileinfo().fileName()); + } + + if (!text.isEmpty()) { + auto response = CustomMessageBox::selectable(this, tr("Confirm Removal"), text, QMessageBox::Warning, + QMessageBox::Yes | QMessageBox::No, QMessageBox::No) + ->exec(); + + if (response != QMessageBox::Yes) + return; + } + + removeItems(selection); +} + +void ExternalResourcesPage::removeItems(const QItemSelection& selection) +{ m_model->deleteResources(selection.indexes()); } @@ -209,4 +251,3 @@ bool ExternalResourcesPage::onSelectionChanged(const QModelIndex& current, const return true; } - diff --git a/launcher/ui/pages/instance/ExternalResourcesPage.h b/launcher/ui/pages/instance/ExternalResourcesPage.h index 2d1a5b51..d17fbb7f 100644 --- a/launcher/ui/pages/instance/ExternalResourcesPage.h +++ b/launcher/ui/pages/instance/ExternalResourcesPage.h @@ -50,7 +50,8 @@ class ExternalResourcesPage : public QMainWindow, public BasePage { void filterTextChanged(const QString& newContents); virtual void addItem(); - virtual void removeItem(); + void removeItem(); + virtual void removeItems(const QItemSelection &selection); virtual void enableItem(); virtual void disableItem(); diff --git a/launcher/ui/pages/instance/InstanceSettingsPage.cpp b/launcher/ui/pages/instance/InstanceSettingsPage.cpp index af2ba7c8..4b4c73dc 100644 --- a/launcher/ui/pages/instance/InstanceSettingsPage.cpp +++ b/launcher/ui/pages/instance/InstanceSettingsPage.cpp @@ -48,18 +48,23 @@ #include "JavaCommon.h" #include "Application.h" +#include "minecraft/auth/AccountList.h" #include "java/JavaInstallList.h" #include "java/JavaUtils.h" #include "FileSystem.h" - InstanceSettingsPage::InstanceSettingsPage(BaseInstance *inst, QWidget *parent) : QWidget(parent), ui(new Ui::InstanceSettingsPage), m_instance(inst) { m_settings = inst->settings(); ui->setupUi(this); + accountMenu = new QMenu(this); + // Use undocumented property... https://stackoverflow.com/questions/7121718/create-a-scrollbar-in-a-submenu-qt + accountMenu->setStyleSheet("QMenu { menu-scrollable: 1; }"); + ui->instanceAccountSelector->setMenu(accountMenu); + connect(ui->openGlobalJavaSettingsButton, &QCommandLinkButton::clicked, this, &InstanceSettingsPage::globalSettingsButtonClicked); connect(APPLICATION, &Application::globalSettingsAboutToOpen, this, &InstanceSettingsPage::applySettings); connect(APPLICATION, &Application::globalSettingsClosed, this, &InstanceSettingsPage::loadSettings); @@ -275,6 +280,13 @@ void InstanceSettingsPage::applySettings() m_settings->reset("JoinServerOnLaunchAddress"); } + // Use an account for this instance + bool useAccountForInstance = ui->instanceAccountGroupBox->isChecked(); + m_settings->set("UseAccountForInstance", useAccountForInstance); + if (!useAccountForInstance) { + m_settings->reset("InstanceAccountId"); + } + // FIXME: This should probably be called by a signal instead m_instance->updateRuntimeContext(); } @@ -372,6 +384,9 @@ void InstanceSettingsPage::loadSettings() ui->serverJoinGroupBox->setChecked(m_settings->get("JoinServerOnLaunch").toBool()); ui->serverJoinAddress->setText(m_settings->get("JoinServerOnLaunchAddress").toString()); + + ui->instanceAccountGroupBox->setChecked(m_settings->get("UseAccountForInstance").toBool()); + updateAccountsMenu(); } void InstanceSettingsPage::on_javaDetectBtn_clicked() @@ -437,6 +452,65 @@ void InstanceSettingsPage::on_javaTestBtn_clicked() checker->run(); } +void InstanceSettingsPage::updateAccountsMenu() +{ + accountMenu->clear(); + + auto accounts = APPLICATION->accounts(); + int accountIndex = accounts->findAccountByProfileId(m_settings->get("InstanceAccountId").toString()); + MinecraftAccountPtr defaultAccount = accounts->defaultAccount(); + + if (accountIndex != -1 && accounts->at(accountIndex)) { + defaultAccount = accounts->at(accountIndex); + } + + if (defaultAccount) { + ui->instanceAccountSelector->setText(defaultAccount->profileName()); + ui->instanceAccountSelector->setIcon(getFaceForAccount(defaultAccount)); + } else { + ui->instanceAccountSelector->setText(tr("No default account")); + ui->instanceAccountSelector->setIcon(APPLICATION->getThemedIcon("noaccount")); + } + + for (int i = 0; i < accounts->count(); i++) { + MinecraftAccountPtr account = accounts->at(i); + QAction* action = new QAction(account->profileName(), this); + action->setData(i); + action->setCheckable(true); + if (accountIndex == i) { + action->setChecked(true); + } + action->setIcon(getFaceForAccount(account)); + accountMenu->addAction(action); + connect(action, SIGNAL(triggered(bool)), this, SLOT(changeInstanceAccount())); + } +} + +QIcon InstanceSettingsPage::getFaceForAccount(MinecraftAccountPtr account) +{ + if (auto face = account->getFace(); !face.isNull()) { + return face; + } + + return APPLICATION->getThemedIcon("noaccount"); +} + +void InstanceSettingsPage::changeInstanceAccount() +{ + QAction* sAction = (QAction*)sender(); + + Q_ASSERT(sAction->data().type() == QVariant::Type::Int); + + QVariant data = sAction->data(); + int index = data.toInt(); + auto accounts = APPLICATION->accounts(); + auto account = accounts->at(index); + m_settings->set("InstanceAccountId", account->profileId()); + + ui->instanceAccountSelector->setText(account->profileName()); + ui->instanceAccountSelector->setIcon(getFaceForAccount(account)); +} + void InstanceSettingsPage::on_maxMemSpinBox_valueChanged(int i) { updateThresholds(); diff --git a/launcher/ui/pages/instance/InstanceSettingsPage.h b/launcher/ui/pages/instance/InstanceSettingsPage.h index 7450188d..cb6fbae0 100644 --- a/launcher/ui/pages/instance/InstanceSettingsPage.h +++ b/launcher/ui/pages/instance/InstanceSettingsPage.h @@ -37,12 +37,13 @@ #include <QWidget> -#include "java/JavaChecker.h" -#include "BaseInstance.h" #include <QObjectPtr.h> -#include "ui/pages/BasePage.h" -#include "JavaCommon.h" +#include <QMenu> #include "Application.h" +#include "BaseInstance.h" +#include "JavaCommon.h" +#include "java/JavaChecker.h" +#include "ui/pages/BasePage.h" class JavaChecker; namespace Ui @@ -92,9 +93,14 @@ private slots: void globalSettingsButtonClicked(bool checked); + void updateAccountsMenu(); + QIcon getFaceForAccount(MinecraftAccountPtr account); + void changeInstanceAccount(); + private: Ui::InstanceSettingsPage *ui; BaseInstance *m_instance; SettingsObjectPtr m_settings; unique_qobject_ptr<JavaCommon::TestCheck> checker; + QMenu *accountMenu = nullptr; }; diff --git a/launcher/ui/pages/instance/InstanceSettingsPage.ui b/launcher/ui/pages/instance/InstanceSettingsPage.ui index b064367d..1b986184 100644 --- a/launcher/ui/pages/instance/InstanceSettingsPage.ui +++ b/launcher/ui/pages/instance/InstanceSettingsPage.ui @@ -609,6 +609,48 @@ </widget> </item> <item> + <widget class="QGroupBox" name="instanceAccountGroupBox"> + <property name="title"> + <string>Override default account</string> + </property> + <property name="checkable"> + <bool>true</bool> + </property> + <property name="checked"> + <bool>false</bool> + </property> + <layout class="QVBoxLayout" name="verticalLayout_15"> + <item> + <layout class="QGridLayout" name="instanceAccountLayout"> + <item row="0" column="0"> + <widget class="QLabel" name="instanceAccountNameLabel"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Preferred"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string>Account:</string> + </property> + </widget> + </item> + <item row="0" column="1"> + <widget class="QToolButton" name="instanceAccountSelector"> + <property name="popupMode"> + <enum>QToolButton::InstantPopup</enum> + </property> + <property name="toolButtonStyle"> + <enum>Qt::ToolButtonTextBesideIcon</enum> + </property> + </widget> + </item> + </layout> + </item> + </layout> + </widget> + </item> + <item> <spacer name="verticalSpacerMiscellaneous"> <property name="orientation"> <enum>Qt::Vertical</enum> diff --git a/launcher/ui/pages/instance/LogPage.cpp b/launcher/ui/pages/instance/LogPage.cpp index 31c3e925..639cd711 100644 --- a/launcher/ui/pages/instance/LogPage.cpp +++ b/launcher/ui/pages/instance/LogPage.cpp @@ -1,8 +1,9 @@ // SPDX-License-Identifier: GPL-3.0-only /* - * PolyMC - Minecraft Launcher + * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 Jamie Mansfield <jmansfield@cadixdev.org> * 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 @@ -39,7 +40,7 @@ #include "Application.h" -#include <QIcon> +#include <QIdentityProxyModel> #include <QScrollBar> #include <QShortcut> @@ -277,28 +278,22 @@ void LogPage::on_btnPaste_clicked() //FIXME: turn this into a proper task and move the upload logic out of GuiUtil! m_model->append( MessageLevel::Launcher, - QString("%2: Log upload triggered at: %1").arg( - QDateTime::currentDateTime().toString(Qt::RFC2822Date), - BuildConfig.LAUNCHER_DISPLAYNAME + QString("Log upload triggered at: %1").arg( + QDateTime::currentDateTime().toString(Qt::RFC2822Date) ) ); - auto url = GuiUtil::uploadPaste(m_model->toPlainText(), this); - if(!url.isEmpty()) + auto url = GuiUtil::uploadPaste(tr("Minecraft Log"), m_model->toPlainText(), this); + if(!url.has_value()) { - m_model->append( - MessageLevel::Launcher, - QString("%2: Log uploaded to: %1").arg( - url, - BuildConfig.LAUNCHER_DISPLAYNAME - ) - ); + m_model->append(MessageLevel::Error, QString("Log upload canceled")); + } + else if (url->isNull()) + { + m_model->append(MessageLevel::Error, QString("Log upload failed!")); } else { - m_model->append( - MessageLevel::Error, - QString("%1: Log upload failed!").arg(BuildConfig.LAUNCHER_DISPLAYNAME) - ); + m_model->append(MessageLevel::Launcher, QString("Log uploaded to: %1").arg(url.value())); } } diff --git a/launcher/ui/pages/instance/ManagedPackPage.cpp b/launcher/ui/pages/instance/ManagedPackPage.cpp index 4de80468..d0701a7a 100644 --- a/launcher/ui/pages/instance/ManagedPackPage.cpp +++ b/launcher/ui/pages/instance/ManagedPackPage.cpp @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2022 flow <flowlnlnln@gmail.com> +// SPDX-FileCopyrightText: 2022 flowln <flowlnlnln@gmail.com> // // SPDX-License-Identifier: GPL-3.0-only @@ -9,14 +9,13 @@ #include <QProxyStyle> #include <QStyleFactory> -#include <HoeDown.h> - #include "Application.h" #include "BuildConfig.h" #include "InstanceImportTask.h" #include "InstanceList.h" #include "InstanceTask.h" #include "Json.h" +#include "Markdown.h" #include "modplatform/modrinth/ModrinthPackManifest.h" @@ -31,8 +30,6 @@ class NoBigComboBoxStyle : public QProxyStyle { Q_OBJECT public: - NoBigComboBoxStyle(QStyle* style) : QProxyStyle(style) {} - // clang-format off int styleHint(QStyle::StyleHint hint, const QStyleOption* option = nullptr, const QWidget* widget = nullptr, QStyleHintReturn* returnData = nullptr) const override { @@ -42,6 +39,37 @@ class NoBigComboBoxStyle : public QProxyStyle { return QProxyStyle::styleHint(hint, option, widget, returnData); } // clang-format on + + /** + * Something about QProxyStyle and QStyle objects means they can't be free'd just + * because all the widgets using them are gone. + * They seems to be tied to the QApplicaiton lifecycle. + * So make singletons tied to the lifetime of the application to clean them up and ensure they aren't + * being remade over and over again, thus leaking memory. + */ + public: + static NoBigComboBoxStyle* getInstance(QStyle* style) + { + static QHash<QStyle*, NoBigComboBoxStyle*> s_singleton_instances_ = {}; + static std::mutex s_singleton_instances_mutex_; + + std::lock_guard<std::mutex> lock(s_singleton_instances_mutex_); + auto inst_iter = s_singleton_instances_.constFind(style); + NoBigComboBoxStyle* inst = nullptr; + if (inst_iter == s_singleton_instances_.constEnd() || *inst_iter == nullptr) { + inst = new NoBigComboBoxStyle(style); + inst->setParent(APPLICATION); + s_singleton_instances_.insert(style, inst); + qDebug() << "QProxyStyle NoBigComboBox created for" << style->objectName() << style; + } else { + inst = *inst_iter; + } + return inst; + } + + private: + NoBigComboBoxStyle(QStyle* style) : QProxyStyle(style) {} + }; ManagedPackPage* ManagedPackPage::createPage(BaseInstance* inst, QString type, QWidget* parent) @@ -63,8 +91,10 @@ ManagedPackPage::ManagedPackPage(BaseInstance* inst, InstanceWindow* instance_wi // NOTE: GTK2 themes crash with the proxy style. // This seems like an upstream bug, so there's not much else that can be done. - if (!QStyleFactory::keys().contains("gtk2")) - ui->versionsComboBox->setStyle(new NoBigComboBoxStyle(ui->versionsComboBox->style())); + if (!QStyleFactory::keys().contains("gtk2")){ + auto comboStyle = NoBigComboBoxStyle::getInstance(ui->versionsComboBox->style()); + ui->versionsComboBox->setStyle(comboStyle); + } ui->reloadButton->setVisible(false); connect(ui->reloadButton, &QPushButton::clicked, this, [this](bool){ @@ -263,8 +293,7 @@ void ModrinthManagedPackPage::suggestVersion() auto index = ui->versionsComboBox->currentIndex(); auto version = m_pack.versions.at(index); - HoeDown md_parser; - ui->changelogTextBrowser->setHtml(md_parser.process(version.changelog.toUtf8())); + ui->changelogTextBrowser->setHtml(markdownToHTML(version.changelog.toUtf8())); ManagedPackPage::suggestVersion(); } diff --git a/launcher/ui/pages/instance/ManagedPackPage.h b/launcher/ui/pages/instance/ManagedPackPage.h index d29a5e88..1ac6fc03 100644 --- a/launcher/ui/pages/instance/ManagedPackPage.h +++ b/launcher/ui/pages/instance/ManagedPackPage.h @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2022 flow <flowlnlnln@gmail.com> +// SPDX-FileCopyrightText: 2022 flowln <flowlnlnln@gmail.com> // // SPDX-License-Identifier: GPL-3.0-only @@ -12,6 +12,8 @@ #include "modplatform/flame/FlameAPI.h" #include "modplatform/flame/FlamePackIndex.h" +#include "net/NetJob.h" + #include "ui/pages/BasePage.h" #include <QWidget> diff --git a/launcher/ui/pages/instance/ModFolderPage.cpp b/launcher/ui/pages/instance/ModFolderPage.cpp index 0a2e6155..4548af59 100644 --- a/launcher/ui/pages/instance/ModFolderPage.cpp +++ b/launcher/ui/pages/instance/ModFolderPage.cpp @@ -1,8 +1,9 @@ // SPDX-License-Identifier: GPL-3.0-only /* - * PolyMC - Minecraft Launcher + * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 Jamie Mansfield <jmansfield@cadixdev.org> * 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 @@ -48,8 +49,8 @@ #include "ui/GuiUtil.h" #include "ui/dialogs/CustomMessageBox.h" -#include "ui/dialogs/ModDownloadDialog.h" #include "ui/dialogs/ModUpdateDialog.h" +#include "ui/dialogs/ResourceDownloadDialog.h" #include "DesktopServices.h" @@ -58,7 +59,7 @@ #include "minecraft/mod/Mod.h" #include "minecraft/mod/ModFolderModel.h" -#include "modplatform/ModAPI.h" +#include "modplatform/ResourceAPI.h" #include "Version.h" #include "tasks/ConcurrentTask.h" @@ -139,13 +140,8 @@ bool ModFolderPage::onSelectionChanged(const QModelIndex& current, const QModelI return true; } -void ModFolderPage::removeItem() +void ModFolderPage::removeItems(const QItemSelection &selection) { - - if (!m_controlsEnabled) - return; - - auto selection = m_filterModel->mapSelectionToSource(ui->treeView->selectionModel()->selection()); m_model->deleteMods(selection.indexes()); } @@ -157,12 +153,12 @@ void ModFolderPage::installMods() return; // this is a null instance or a legacy instance auto profile = static_cast<MinecraftInstance*>(m_instance)->getPackProfile(); - if (profile->getModLoaders() == ModAPI::Unspecified) { + if (!profile->getModLoaders().has_value()) { QMessageBox::critical(this, tr("Error"), tr("Please install a mod loader first!")); return; } - ModDownloadDialog mdownload(m_model, this, m_instance); + ResourceDownload::ModDownloadDialog mdownload(this, m_model, m_instance); if (mdownload.exec()) { ConcurrentTask* tasks = new ConcurrentTask(this); connect(tasks, &Task::failed, [this, tasks](QString reason) { @@ -277,3 +273,12 @@ bool CoreModFolderPage::shouldDisplay() const } return false; } + +NilModFolderPage::NilModFolderPage(BaseInstance* inst, std::shared_ptr<ModFolderModel> mods, QWidget* parent) + : ModFolderPage(inst, mods, parent) +{} + +bool NilModFolderPage::shouldDisplay() const +{ + return m_model->dir().exists(); +} diff --git a/launcher/ui/pages/instance/ModFolderPage.h b/launcher/ui/pages/instance/ModFolderPage.h index f20adf34..2fc7b574 100644 --- a/launcher/ui/pages/instance/ModFolderPage.h +++ b/launcher/ui/pages/instance/ModFolderPage.h @@ -1,8 +1,9 @@ // SPDX-License-Identifier: GPL-3.0-only /* - * PolyMC - Minecraft Launcher + * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 Jamie Mansfield <jmansfield@cadixdev.org> * 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 @@ -59,7 +60,7 @@ class ModFolderPage : public ExternalResourcesPage { private slots: void runningStateChanged(bool running); - void removeItem() override; + void removeItems(const QItemSelection &selection) override; void installMods(); void updateMods(); @@ -80,3 +81,16 @@ class CoreModFolderPage : public ModFolderPage { virtual bool shouldDisplay() const override; }; + +class NilModFolderPage : public ModFolderPage { + public: + explicit NilModFolderPage(BaseInstance* inst, std::shared_ptr<ModFolderModel> mods, QWidget* parent = 0); + virtual ~NilModFolderPage() = default; + + virtual QString displayName() const override { return tr("Nilmods"); } + virtual QIcon icon() const override { return APPLICATION->getThemedIcon("coremods"); } + virtual QString id() const override { return "nilmods"; } + virtual QString helpPage() const override { return "Nilmods"; } + + virtual bool shouldDisplay() const override; +}; diff --git a/launcher/ui/pages/instance/OtherLogsPage.cpp b/launcher/ui/pages/instance/OtherLogsPage.cpp index 0c1939c6..bbdd7324 100644 --- a/launcher/ui/pages/instance/OtherLogsPage.cpp +++ b/launcher/ui/pages/instance/OtherLogsPage.cpp @@ -1,7 +1,8 @@ // SPDX-License-Identifier: GPL-3.0-only /* - * PolyMC - Minecraft Launcher + * Prism Launcher - Minecraft Launcher * 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 @@ -204,7 +205,7 @@ void OtherLogsPage::on_btnReload_clicked() void OtherLogsPage::on_btnPaste_clicked() { - GuiUtil::uploadPaste(ui->text->toPlainText(), this); + GuiUtil::uploadPaste(m_currentFile, ui->text->toPlainText(), this); } void OtherLogsPage::on_btnCopy_clicked() @@ -219,13 +220,21 @@ void OtherLogsPage::on_btnDelete_clicked() setControlsEnabled(false); return; } - if (QMessageBox::question(this, tr("Delete"), - tr("Do you really want to delete %1?").arg(m_currentFile), - QMessageBox::Yes, QMessageBox::No) == QMessageBox::No) - { + if (QMessageBox::question(this, tr("Confirm Deletion"), + tr("You are about to delete \"%1\".\n" + "This may be permanent and it will be gone from the logs folder.\n\n" + "Are you sure?") + .arg(m_currentFile), + QMessageBox::Yes, QMessageBox::No) == QMessageBox::No) { return; } QFile file(FS::PathCombine(m_path, m_currentFile)); + + if (FS::trash(file.fileName())) + { + return; + } + if (!file.remove()) { QMessageBox::critical(this, tr("Error"), tr("Unable to delete %1: %2") @@ -243,15 +252,15 @@ void OtherLogsPage::on_btnClean_clicked() return; } QMessageBox *messageBox = new QMessageBox(this); - messageBox->setWindowTitle(tr("Clean up")); + messageBox->setWindowTitle(tr("Confirm Cleanup")); if(toDelete.size() > 5) { - messageBox->setText(tr("Do you really want to delete all log files?")); + messageBox->setText(tr("Are you sure you want to delete all log files?")); messageBox->setDetailedText(toDelete.join('\n')); } else { - messageBox->setText(tr("Do you really want to delete these files?\n%1").arg(toDelete.join('\n'))); + messageBox->setText(tr("Are you sure you want to delete all these files?\n%1").arg(toDelete.join('\n'))); } messageBox->setStandardButtons(QMessageBox::Ok | QMessageBox::Cancel); messageBox->setDefaultButton(QMessageBox::Ok); @@ -267,6 +276,10 @@ void OtherLogsPage::on_btnClean_clicked() for(auto item: toDelete) { QFile file(FS::PathCombine(m_path, item)); + if (FS::trash(file.fileName())) + { + continue; + } if (!file.remove()) { failed.push_back(item); diff --git a/launcher/ui/pages/instance/ResourcePackPage.cpp b/launcher/ui/pages/instance/ResourcePackPage.cpp new file mode 100644 index 00000000..24bfb38d --- /dev/null +++ b/launcher/ui/pages/instance/ResourcePackPage.cpp @@ -0,0 +1,104 @@ +// SPDX-FileCopyrightText: 2023 flowln <flowlnlnln@gmail.com> +// +// SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0 +/* + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2022 Jamie Mansfield <jmansfield@cadixdev.org> + * + * 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 "ResourcePackPage.h" + +#include "ResourceDownloadTask.h" + +#include "ui/dialogs/CustomMessageBox.h" +#include "ui/dialogs/ProgressDialog.h" +#include "ui/dialogs/ResourceDownloadDialog.h" + +ResourcePackPage::ResourcePackPage(MinecraftInstance* instance, std::shared_ptr<ResourcePackFolderModel> model, QWidget* parent) + : ExternalResourcesPage(instance, model, parent) +{ + ui->actionDownloadItem->setText(tr("Download packs")); + ui->actionDownloadItem->setToolTip(tr("Download resource packs from online platforms")); + ui->actionDownloadItem->setEnabled(true); + connect(ui->actionDownloadItem, &QAction::triggered, this, &ResourcePackPage::downloadRPs); + ui->actionsToolbar->insertActionBefore(ui->actionAddItem, ui->actionDownloadItem); + + ui->actionViewConfigs->setVisible(false); +} + +bool ResourcePackPage::onSelectionChanged(const QModelIndex& current, const QModelIndex& previous) +{ + auto sourceCurrent = m_filterModel->mapToSource(current); + int row = sourceCurrent.row(); + auto& rp = static_cast<ResourcePack&>(m_model->at(row)); + ui->frame->updateWithResourcePack(rp); + + return true; +} + +void ResourcePackPage::downloadRPs() +{ + if (!m_controlsEnabled) + return; + if (m_instance->typeName() != "Minecraft") + return; // this is a null instance or a legacy instance + + ResourceDownload::ResourcePackDownloadDialog mdownload(this, std::static_pointer_cast<ResourcePackFolderModel>(m_model), m_instance); + if (mdownload.exec()) { + auto tasks = new ConcurrentTask(this); + connect(tasks, &Task::failed, [this, tasks](QString reason) { + CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->show(); + tasks->deleteLater(); + }); + connect(tasks, &Task::aborted, [this, tasks]() { + CustomMessageBox::selectable(this, tr("Aborted"), tr("Download stopped by user."), QMessageBox::Information)->show(); + tasks->deleteLater(); + }); + connect(tasks, &Task::succeeded, [this, tasks]() { + QStringList warnings = tasks->warnings(); + if (warnings.count()) + CustomMessageBox::selectable(this, tr("Warnings"), warnings.join('\n'), QMessageBox::Warning)->show(); + + tasks->deleteLater(); + }); + + for (auto& task : mdownload.getTasks()) { + tasks->addTask(task); + } + + ProgressDialog loadDialog(this); + loadDialog.setSkipButton(true, tr("Abort")); + loadDialog.execWithTask(tasks); + + m_model->update(); + } +} diff --git a/launcher/ui/pages/instance/ResourcePackPage.h b/launcher/ui/pages/instance/ResourcePackPage.h index 9633e3b4..b04aa2e9 100644 --- a/launcher/ui/pages/instance/ResourcePackPage.h +++ b/launcher/ui/pages/instance/ResourcePackPage.h @@ -1,6 +1,8 @@ -// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2023 flowln <flowlnlnln@gmail.com> +// +// SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0 /* - * PolyMC - Minecraft Launcher + * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 Jamie Mansfield <jmansfield@cadixdev.org> * * This program is free software: you can redistribute it and/or modify @@ -44,12 +46,7 @@ class ResourcePackPage : public ExternalResourcesPage { Q_OBJECT public: - explicit ResourcePackPage(MinecraftInstance *instance, std::shared_ptr<ResourcePackFolderModel> model, QWidget *parent = 0) - : ExternalResourcesPage(instance, model, parent) - { - ui->actionViewConfigs->setVisible(false); - } - virtual ~ResourcePackPage() {} + explicit ResourcePackPage(MinecraftInstance *instance, std::shared_ptr<ResourcePackFolderModel> model, QWidget *parent = 0); QString displayName() const override { return tr("Resource packs"); } QIcon icon() const override { return APPLICATION->getThemedIcon("resourcepacks"); } @@ -63,13 +60,7 @@ public: } public slots: - bool onSelectionChanged(const QModelIndex& current, const QModelIndex& previous) override - { - auto sourceCurrent = m_filterModel->mapToSource(current); - int row = sourceCurrent.row(); - auto& rp = static_cast<ResourcePack&>(m_model->at(row)); - ui->frame->updateWithResourcePack(rp); - - return true; - } + bool onSelectionChanged(const QModelIndex& current, const QModelIndex& previous) override; + void downloadRPs(); }; + diff --git a/launcher/ui/pages/instance/ScreenshotsPage.cpp b/launcher/ui/pages/instance/ScreenshotsPage.cpp index 0092aef3..ca368d3b 100644 --- a/launcher/ui/pages/instance/ScreenshotsPage.cpp +++ b/launcher/ui/pages/instance/ScreenshotsPage.cpp @@ -1,8 +1,9 @@ // SPDX-License-Identifier: GPL-3.0-only /* - * PolyMC - Minecraft Launcher + * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 Jamie Mansfield <jmansfield@cadixdev.org> * 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 @@ -379,6 +380,24 @@ void ScreenshotsPage::on_actionUpload_triggered() if (selection.isEmpty()) return; + + QString text; + if (selection.size() > 1) + text = tr("You are about to upload %1 screenshots.\n\n" + "Are you sure?") + .arg(selection.size()); + else + text = + tr("You are about to upload the selected screenshot.\n\n" + "Are you sure?"); + + auto response = CustomMessageBox::selectable(this, "Confirm Upload", text, QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No, + QMessageBox::No) + ->exec(); + + if (response != QMessageBox::Yes) + return; + QList<ScreenShot::Ptr> uploaded; auto job = NetJob::Ptr(new NetJob("Screenshot Upload", APPLICATION->network())); if(selection.size() < 2) @@ -491,17 +510,32 @@ void ScreenshotsPage::on_actionCopy_File_s_triggered() void ScreenshotsPage::on_actionDelete_triggered() { - auto mbox = CustomMessageBox::selectable( - this, tr("Are you sure?"), tr("This will delete all selected screenshots."), - QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No); - std::unique_ptr<QMessageBox> box(mbox); + auto selected = ui->listView->selectionModel()->selectedIndexes(); + + int count = ui->listView->selectionModel()->selectedRows().size(); + QString text; + if (count > 1) + text = tr("You are about to delete %1 screenshots.\n" + "This may be permanent and they will be gone from the folder.\n\n" + "Are you sure?") + .arg(count); + else + text = tr("You are about to delete the selected screenshot.\n" + "This may be permanent and it will be gone from the folder.\n\n" + "Are you sure?") + .arg(count); - if (box->exec() != QMessageBox::Yes) + auto response = + CustomMessageBox::selectable(this, tr("Confirm Deletion"), text, QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No)->exec(); + + if (response != QMessageBox::Yes) return; - auto selected = ui->listView->selectionModel()->selectedIndexes(); for (auto item : selected) { + if (FS::trash(m_model->filePath(item))) + continue; + m_model->remove(item); } } diff --git a/launcher/ui/pages/instance/ScreenshotsPage.h b/launcher/ui/pages/instance/ScreenshotsPage.h index 2eb0de04..89611b6d 100644 --- a/launcher/ui/pages/instance/ScreenshotsPage.h +++ b/launcher/ui/pages/instance/ScreenshotsPage.h @@ -44,6 +44,7 @@ class QFileSystemModel; class QIdentityProxyModel; +class QItemSelection; namespace Ui { class ScreenshotsPage; diff --git a/launcher/ui/pages/instance/ServersPage.cpp b/launcher/ui/pages/instance/ServersPage.cpp index a625e20b..4b1fa08a 100644 --- a/launcher/ui/pages/instance/ServersPage.cpp +++ b/launcher/ui/pages/instance/ServersPage.cpp @@ -1,8 +1,9 @@ // SPDX-License-Identifier: GPL-3.0-only /* - * PolyMC - Minecraft Launcher + * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 Jamie Mansfield <jmansfield@cadixdev.org> * 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 @@ -35,6 +36,7 @@ */ #include "ServersPage.h" +#include "ui/dialogs/CustomMessageBox.h" #include "ui_ServersPage.h" #include <FileSystem.h> @@ -48,6 +50,7 @@ #include <QFileSystemWatcher> #include <QMenu> +#include <QTimer> static const int COLUMN_COUNT = 2; // 3 , TBD: latency and other nice things. @@ -799,6 +802,17 @@ void ServersPage::on_actionAdd_triggered() void ServersPage::on_actionRemove_triggered() { + auto response = CustomMessageBox::selectable(this, tr("Confirm Removal"), + tr("You are about to remove \"%1\".\n" + "This is permanent and the server will be gone from your list forever (A LONG TIME).\n\n" + "Are you sure?") + .arg(m_model->at(currentServer)->m_name), + QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No, QMessageBox::No) + ->exec(); + + if (response != QMessageBox::Yes) + return; + m_model->removeRow(currentServer); } diff --git a/launcher/ui/pages/instance/ShaderPackPage.cpp b/launcher/ui/pages/instance/ShaderPackPage.cpp new file mode 100644 index 00000000..2d0c10aa --- /dev/null +++ b/launcher/ui/pages/instance/ShaderPackPage.cpp @@ -0,0 +1,98 @@ +// SPDX-FileCopyrightText: 2023 flowln <flowlnlnln@gmail.com> +// +// SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0 +/* + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2022 Jamie Mansfield <jmansfield@cadixdev.org> + * + * 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 "ShaderPackPage.h" +#include "ui_ExternalResourcesPage.h" + +#include "ResourceDownloadTask.h" + +#include "minecraft/mod/ShaderPackFolderModel.h" + +#include "ui/dialogs/CustomMessageBox.h" +#include "ui/dialogs/ProgressDialog.h" +#include "ui/dialogs/ResourceDownloadDialog.h" + + +ShaderPackPage::ShaderPackPage(MinecraftInstance* instance, std::shared_ptr<ShaderPackFolderModel> model, QWidget* parent) + : ExternalResourcesPage(instance, model, parent) +{ + ui->actionDownloadItem->setText(tr("Download shaders")); + ui->actionDownloadItem->setToolTip(tr("Download shaders from online platforms")); + ui->actionDownloadItem->setEnabled(true); + connect(ui->actionDownloadItem, &QAction::triggered, this, &ShaderPackPage::downloadShaders); + ui->actionsToolbar->insertActionBefore(ui->actionAddItem, ui->actionDownloadItem); + + ui->actionViewConfigs->setVisible(false); +} + +void ShaderPackPage::downloadShaders() +{ + if (!m_controlsEnabled) + return; + if (m_instance->typeName() != "Minecraft") + return; // this is a null instance or a legacy instance + + ResourceDownload::ShaderPackDownloadDialog mdownload(this, std::static_pointer_cast<ShaderPackFolderModel>(m_model), m_instance); + if (mdownload.exec()) { + auto tasks = new ConcurrentTask(this); + connect(tasks, &Task::failed, [this, tasks](QString reason) { + CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->show(); + tasks->deleteLater(); + }); + connect(tasks, &Task::aborted, [this, tasks]() { + CustomMessageBox::selectable(this, tr("Aborted"), tr("Download stopped by user."), QMessageBox::Information)->show(); + tasks->deleteLater(); + }); + connect(tasks, &Task::succeeded, [this, tasks]() { + QStringList warnings = tasks->warnings(); + if (warnings.count()) + CustomMessageBox::selectable(this, tr("Warnings"), warnings.join('\n'), QMessageBox::Warning)->show(); + + tasks->deleteLater(); + }); + + for (auto& task : mdownload.getTasks()) { + tasks->addTask(task); + } + + ProgressDialog loadDialog(this); + loadDialog.setSkipButton(true, tr("Abort")); + loadDialog.execWithTask(tasks); + + m_model->update(); + } +} diff --git a/launcher/ui/pages/instance/ShaderPackPage.h b/launcher/ui/pages/instance/ShaderPackPage.h index 7f7ff8c1..a779fd8c 100644 --- a/launcher/ui/pages/instance/ShaderPackPage.h +++ b/launcher/ui/pages/instance/ShaderPackPage.h @@ -1,6 +1,8 @@ -// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2023 flowln <flowlnlnln@gmail.com> +// +// SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0 /* - * PolyMC - Minecraft Launcher + * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 Jamie Mansfield <jmansfield@cadixdev.org> * * This program is free software: you can redistribute it and/or modify @@ -36,28 +38,21 @@ #pragma once #include "ExternalResourcesPage.h" -#include "ui_ExternalResourcesPage.h" - -#include "minecraft/mod/ShaderPackFolderModel.h" class ShaderPackPage : public ExternalResourcesPage { Q_OBJECT public: - explicit ShaderPackPage(MinecraftInstance *instance, std::shared_ptr<ShaderPackFolderModel> model, QWidget *parent = 0) - : ExternalResourcesPage(instance, model, parent) - { - ui->actionViewConfigs->setVisible(false); - } - virtual ~ShaderPackPage() {} + explicit ShaderPackPage(MinecraftInstance *instance, std::shared_ptr<ShaderPackFolderModel> model, QWidget *parent = nullptr); + ~ShaderPackPage() override = default; QString displayName() const override { return tr("Shader packs"); } QIcon icon() const override { return APPLICATION->getThemedIcon("shaderpacks"); } QString id() const override { return "shaderpacks"; } QString helpPage() const override { return "Resource-packs"; } - virtual bool shouldDisplay() const override - { - return true; - } + bool shouldDisplay() const override { return true; } + + public slots: + void downloadShaders(); }; diff --git a/launcher/ui/pages/instance/TexturePackPage.cpp b/launcher/ui/pages/instance/TexturePackPage.cpp new file mode 100644 index 00000000..427aba11 --- /dev/null +++ b/launcher/ui/pages/instance/TexturePackPage.cpp @@ -0,0 +1,106 @@ +// SPDX-FileCopyrightText: 2023 flowln <flowlnlnln@gmail.com> +// +// SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0 +/* + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2022 Jamie Mansfield <jmansfield@cadixdev.org> + * + * 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 "TexturePackPage.h" + +#include "ResourceDownloadTask.h" + +#include "minecraft/mod/TexturePack.h" + +#include "ui/dialogs/CustomMessageBox.h" +#include "ui/dialogs/ProgressDialog.h" +#include "ui/dialogs/ResourceDownloadDialog.h" + +TexturePackPage::TexturePackPage(MinecraftInstance* instance, std::shared_ptr<TexturePackFolderModel> model, QWidget* parent) + : ExternalResourcesPage(instance, model, parent) +{ + ui->actionDownloadItem->setText(tr("Download packs")); + ui->actionDownloadItem->setToolTip(tr("Download texture packs from online platforms")); + ui->actionDownloadItem->setEnabled(true); + connect(ui->actionDownloadItem, &QAction::triggered, this, &TexturePackPage::downloadTPs); + ui->actionsToolbar->insertActionBefore(ui->actionAddItem, ui->actionDownloadItem); + + ui->actionViewConfigs->setVisible(false); +} + +bool TexturePackPage::onSelectionChanged(const QModelIndex& current, const QModelIndex& previous) +{ + auto sourceCurrent = m_filterModel->mapToSource(current); + int row = sourceCurrent.row(); + auto& rp = static_cast<TexturePack&>(m_model->at(row)); + ui->frame->updateWithTexturePack(rp); + + return true; +} + +void TexturePackPage::downloadTPs() +{ + if (!m_controlsEnabled) + return; + if (m_instance->typeName() != "Minecraft") + return; // this is a null instance or a legacy instance + + ResourceDownload::TexturePackDownloadDialog mdownload(this, std::static_pointer_cast<TexturePackFolderModel>(m_model), m_instance); + if (mdownload.exec()) { + auto tasks = new ConcurrentTask(this); + connect(tasks, &Task::failed, [this, tasks](QString reason) { + CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->show(); + tasks->deleteLater(); + }); + connect(tasks, &Task::aborted, [this, tasks]() { + CustomMessageBox::selectable(this, tr("Aborted"), tr("Download stopped by user."), QMessageBox::Information)->show(); + tasks->deleteLater(); + }); + connect(tasks, &Task::succeeded, [this, tasks]() { + QStringList warnings = tasks->warnings(); + if (warnings.count()) + CustomMessageBox::selectable(this, tr("Warnings"), warnings.join('\n'), QMessageBox::Warning)->show(); + + tasks->deleteLater(); + }); + + for (auto& task : mdownload.getTasks()) { + tasks->addTask(task); + } + + ProgressDialog loadDialog(this); + loadDialog.setSkipButton(true, tr("Abort")); + loadDialog.execWithTask(tasks); + + m_model->update(); + } +} diff --git a/launcher/ui/pages/instance/TexturePackPage.h b/launcher/ui/pages/instance/TexturePackPage.h index 69b836ca..47a8fa60 100644 --- a/launcher/ui/pages/instance/TexturePackPage.h +++ b/launcher/ui/pages/instance/TexturePackPage.h @@ -1,6 +1,8 @@ -// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2023 flowln <flowlnlnln@gmail.com> +// +// SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0 /* - * PolyMC - Minecraft Launcher + * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 Jamie Mansfield <jmansfield@cadixdev.org> * * This program is free software: you can redistribute it and/or modify @@ -39,18 +41,12 @@ #include "ui_ExternalResourcesPage.h" #include "minecraft/mod/TexturePackFolderModel.h" -#include "minecraft/mod/TexturePack.h" class TexturePackPage : public ExternalResourcesPage { Q_OBJECT public: - explicit TexturePackPage(MinecraftInstance *instance, std::shared_ptr<TexturePackFolderModel> model, QWidget *parent = 0) - : ExternalResourcesPage(instance, model, parent) - { - ui->actionViewConfigs->setVisible(false); - } - virtual ~TexturePackPage() {} + explicit TexturePackPage(MinecraftInstance *instance, std::shared_ptr<TexturePackFolderModel> model, QWidget* parent = nullptr); QString displayName() const override { return tr("Texture packs"); } QIcon icon() const override { return APPLICATION->getThemedIcon("resourcepacks"); } @@ -63,13 +59,6 @@ public: } public slots: - bool onSelectionChanged(const QModelIndex& current, const QModelIndex& previous) override - { - auto sourceCurrent = m_filterModel->mapToSource(current); - int row = sourceCurrent.row(); - auto& rp = static_cast<TexturePack&>(m_model->at(row)); - ui->frame->updateWithTexturePack(rp); - - return true; - } + bool onSelectionChanged(const QModelIndex& current, const QModelIndex& previous) override; + void downloadTPs(); }; diff --git a/launcher/ui/pages/instance/VersionPage.cpp b/launcher/ui/pages/instance/VersionPage.cpp index c8a65f10..74b7ec7c 100644 --- a/launcher/ui/pages/instance/VersionPage.cpp +++ b/launcher/ui/pages/instance/VersionPage.cpp @@ -1,8 +1,11 @@ -// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2022-2023 Sefa Eyeoglu <contact@scrumplex.net> +// +// SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0 + /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 Jamie Mansfield <jmansfield@cadixdev.org> - * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> + * Copyright (C) 2022-2023 Sefa Eyeoglu <contact@scrumplex.net> * Copyright (C) 2022 TheKodeToad <TheKodeToad@proton.me> * * This program is free software: you can redistribute it and/or modify @@ -162,7 +165,7 @@ VersionPage::VersionPage(MinecraftInstance *inst, QWidget *parent) auto proxy = new IconProxy(ui->packageView); proxy->setSourceModel(m_profile.get()); - m_filterModel = new QSortFilterProxyModel(); + m_filterModel = new QSortFilterProxyModel(this); m_filterModel->setDynamicSortFilter(true); m_filterModel->setFilterCaseSensitivity(Qt::CaseInsensitive); m_filterModel->setSortCaseSensitivity(Qt::CaseInsensitive); @@ -282,6 +285,7 @@ void VersionPage::updateButtons(int row) ui->actionRevert->setEnabled(controlsEnabled && patch && patch->isRevertible()); ui->actionDownload_All->setEnabled(controlsEnabled); ui->actionAdd_Empty->setEnabled(controlsEnabled); + ui->actionImport_Components->setEnabled(controlsEnabled); ui->actionReload->setEnabled(controlsEnabled); ui->actionInstall_mods->setEnabled(controlsEnabled); ui->actionReplace_Minecraft_jar->setEnabled(controlsEnabled); @@ -318,13 +322,29 @@ void VersionPage::on_actionReload_triggered() void VersionPage::on_actionRemove_triggered() { - if (ui->packageView->currentIndex().isValid()) + if (!ui->packageView->currentIndex().isValid()) { - // FIXME: use actual model, not reloading. - if (!m_profile->remove(ui->packageView->currentIndex().row())) - { - QMessageBox::critical(this, tr("Error"), tr("Couldn't remove file")); - } + return; + } + int index = ui->packageView->currentIndex().row(); + auto component = m_profile->getComponent(index); + if (component->isCustom()) + { + auto response = CustomMessageBox::selectable(this, tr("Confirm Removal"), + tr("You are about to remove \"%1\".\n" + "This is permanent and will completely remove the custom component.\n\n" + "Are you sure?") + .arg(component->getName()), + QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No, QMessageBox::No) + ->exec(); + + if (response != QMessageBox::Yes) + return; + } + // FIXME: use actual model, not reloading. + if (!m_profile->remove(index)) + { + QMessageBox::critical(this, tr("Error"), tr("Couldn't remove file")); } updateButtons(); reloadPackProfile(); @@ -359,6 +379,20 @@ void VersionPage::on_actionReplace_Minecraft_jar_triggered() updateButtons(); } +void VersionPage::on_actionImport_Components_triggered() +{ + QStringList list = GuiUtil::BrowseForFiles("component", tr("Select components"), tr("Components (*.json)"), + APPLICATION->settings()->get("CentralModsDir").toString(), this->parentWidget()); + + if (!list.isEmpty()) { + if (!m_profile->installComponents(list)) { + QMessageBox::warning(this, tr("Failed to import components"), + tr("Some components could not be imported. Check logs for details")); + } + } + + updateButtons(); +} void VersionPage::on_actionAdd_Agents_triggered() { @@ -467,7 +501,7 @@ void VersionPage::on_actionDownload_All_triggered() return; } ProgressDialog tDialog(this); - connect(updateTask.get(), SIGNAL(failed(QString)), SLOT(onGameUpdateError(QString))); + connect(updateTask.get(), &Task::failed, this, &VersionPage::onGameUpdateError); // FIXME: unused return value tDialog.execWithTask(updateTask.get()); updateButtons(); @@ -644,7 +678,7 @@ void VersionPage::onGameUpdateError(QString error) CustomMessageBox::selectable(this, tr("Error updating instance"), error, QMessageBox::Warning)->show(); } -Component * VersionPage::current() +ComponentPtr VersionPage::current() { auto row = currentRow(); if(row < 0) @@ -707,6 +741,19 @@ void VersionPage::on_actionRevert_triggered() { return; } + auto component = m_profile->getComponent(version); + + auto response = CustomMessageBox::selectable(this, tr("Confirm Reversion"), + tr("You are about to revert \"%1\".\n" + "This is permanent and will completely revert your customizations.\n\n" + "Are you sure?") + .arg(component->getName()), + QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No, QMessageBox::No) + ->exec(); + + if (response != QMessageBox::Yes) + return; + if(!m_profile->revertToBase(version)) { // TODO: some error box here diff --git a/launcher/ui/pages/instance/VersionPage.h b/launcher/ui/pages/instance/VersionPage.h index 166f36bb..d0087714 100644 --- a/launcher/ui/pages/instance/VersionPage.h +++ b/launcher/ui/pages/instance/VersionPage.h @@ -1,7 +1,11 @@ -// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2022-2023 Sefa Eyeoglu <contact@scrumplex.net> +// +// SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0 + /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 Jamie Mansfield <jmansfield@cadixdev.org> + * Copyright (C) 2022-2023 Sefa Eyeoglu <contact@scrumplex.net> * Copyright (C) 2022 TheKodeToad <TheKodeToad@proton.me> * * This program is free software: you can redistribute it and/or modify @@ -86,6 +90,7 @@ private slots: void on_actionMove_down_triggered(); void on_actionAdd_to_Minecraft_jar_triggered(); void on_actionReplace_Minecraft_jar_triggered(); + void on_actionImport_Components_triggered(); void on_actionAdd_Agents_triggered(); void on_actionRevert_triggered(); void on_actionEdit_triggered(); @@ -99,7 +104,7 @@ private slots: void updateVersionControls(); private: - Component * current(); + ComponentPtr current(); int currentRow(); void updateButtons(int row = -1); void preselect(int row = 0); diff --git a/launcher/ui/pages/instance/VersionPage.ui b/launcher/ui/pages/instance/VersionPage.ui index 4cd50885..4777eafe 100644 --- a/launcher/ui/pages/instance/VersionPage.ui +++ b/launcher/ui/pages/instance/VersionPage.ui @@ -108,6 +108,7 @@ <addaction name="actionReplace_Minecraft_jar"/> <addaction name="actionAdd_Agents"/> <addaction name="actionAdd_Empty"/> + <addaction name="actionImport_Components"/> <addaction name="separator"/> <addaction name="actionMinecraftFolder"/> <addaction name="actionLibrariesFolder"/> @@ -226,10 +227,10 @@ </action> <action name="actionAdd_Agents"> <property name="text"> - <string>Add Agents</string> + <string>Add Agents</string> </property> <property name="toolTip"> - <string>Add Java agents.</string> + <string>Add Java agents.</string> </property> </action> <action name="actionAdd_Empty"> @@ -272,6 +273,14 @@ <string>Open the instance's local libraries folder.</string> </property> </action> + <action name="actionImport_Components"> + <property name="text"> + <string>Import Components</string> + </property> + <property name="toolTip"> + <string>Import existing component JSON files.</string> + </property> + </action> </widget> <customwidgets> <customwidget> diff --git a/launcher/ui/pages/instance/WorldListPage.cpp b/launcher/ui/pages/instance/WorldListPage.cpp index 93458ce4..b6ad159e 100644 --- a/launcher/ui/pages/instance/WorldListPage.cpp +++ b/launcher/ui/pages/instance/WorldListPage.cpp @@ -1,8 +1,9 @@ // SPDX-License-Identifier: GPL-3.0-only /* - * PolyMC - Minecraft Launcher + * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 Jamie Mansfield <jmansfield@cadixdev.org> * 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 @@ -35,6 +36,7 @@ */ #include "WorldListPage.h" +#include "ui/dialogs/CustomMessageBox.h" #include "ui_WorldListPage.h" #include "minecraft/WorldList.h" @@ -43,9 +45,9 @@ #include <QKeyEvent> #include <QClipboard> #include <QMessageBox> +#include <QSortFilterProxyModel> #include <QTreeView> #include <QInputDialog> -#include <QProcess> #include <Qt> #include "tools/MCEditTool.h" @@ -105,6 +107,7 @@ WorldListPage::WorldListPage(BaseInstance *inst, std::shared_ptr<WorldList> worl auto head = ui->worldTreeView->header(); head->setSectionResizeMode(0, QHeaderView::Stretch); head->setSectionResizeMode(1, QHeaderView::ResizeToContents); + head->setSectionResizeMode(4, QHeaderView::ResizeToContents); connect(ui->worldTreeView->selectionModel(), &QItemSelectionModel::currentChanged, this, &WorldListPage::worldChanged); worldChanged(QModelIndex(), QModelIndex()); @@ -192,12 +195,14 @@ void WorldListPage::on_actionRemove_triggered() if(!proxiedIndex.isValid()) return; - auto result = QMessageBox::question(this, - tr("Are you sure?"), - tr("This will remove the selected world permenantly.\n" - "The world will be gone forever (A LONG TIME).\n" - "\n" - "Do you want to continue?")); + auto result = CustomMessageBox::selectable(this, tr("Confirm Deletion"), + tr("You are about to delete \"%1\".\n" + "The world may be gone forever (A LONG TIME).\n\n" + "Are you sure?") + .arg(m_worlds->allWorlds().at(proxiedIndex.row()).name()), + QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No, QMessageBox::No) + ->exec(); + if(result != QMessageBox::Yes) { return; diff --git a/launcher/ui/pages/instance/WorldListPage.ui b/launcher/ui/pages/instance/WorldListPage.ui index 7c68bfae..d74dd079 100644 --- a/launcher/ui/pages/instance/WorldListPage.ui +++ b/launcher/ui/pages/instance/WorldListPage.ui @@ -109,7 +109,7 @@ </action> <action name="actionRemove"> <property name="text"> - <string>Remove</string> + <string>Delete</string> </property> </action> <action name="actionMCEdit"> diff --git a/launcher/ui/pages/modplatform/ModModel.cpp b/launcher/ui/pages/modplatform/ModModel.cpp index ed58eb32..afd8b292 100644 --- a/launcher/ui/pages/modplatform/ModModel.cpp +++ b/launcher/ui/pages/modplatform/ModModel.cpp @@ -1,350 +1,70 @@ +// SPDX-FileCopyrightText: 2023 flowln <flowlnlnln@gmail.com> +// +// SPDX-License-Identifier: GPL-3.0-only + #include "ModModel.h" -#include "BuildConfig.h" -#include "Json.h" -#include "ModPage.h" #include "minecraft/MinecraftInstance.h" #include "minecraft/PackProfile.h" -#include "ui/dialogs/ModDownloadDialog.h" - -#include "ui/widgets/ProjectItem.h" #include <QMessageBox> -namespace ModPlatform { - -// HACK: We need this to prevent callbacks from calling the ListModel after it has already been deleted. -// This leaks a tiny bit of memory per time the user has opened the mod dialog. How to make this better? -static QHash<ListModel*, bool> s_running; - -ListModel::ListModel(ModPage* parent) : QAbstractListModel(parent), m_parent(parent) { s_running.insert(this, true); } - -ListModel::~ListModel() -{ - s_running.find(this).value() = false; -} +namespace ResourceDownload { -auto ListModel::debugName() const -> QString -{ - return m_parent->debugName(); -} +ModModel::ModModel(BaseInstance const& base_inst, ResourceAPI* api) : ResourceModel(api), m_base_instance(base_inst) {} /******** Make data requests ********/ -void ListModel::fetchMore(const QModelIndex& parent) -{ - if (parent.isValid()) - return; - if (nextSearchOffset == 0) { - qWarning() << "fetchMore with 0 offset is wrong..."; - return; - } - performPaginatedSearch(); -} - -auto ListModel::data(const QModelIndex& index, int role) const -> QVariant -{ - int pos = index.row(); - if (pos >= modpacks.size() || pos < 0 || !index.isValid()) { - return QString("INVALID INDEX %1").arg(pos); - } - - ModPlatform::IndexedPack pack = modpacks.at(pos); - switch (role) { - case Qt::ToolTipRole: { - if (pack.description.length() > 100) { - // some magic to prevent to long tooltips and replace html linebreaks - QString edit = pack.description.left(97); - edit = edit.left(edit.lastIndexOf("<br>")).left(edit.lastIndexOf(" ")).append("..."); - return edit; - } - return pack.description; - } - case Qt::DecorationRole: { - if (m_logoMap.contains(pack.logoName)) { - return m_logoMap.value(pack.logoName); - } - QIcon icon = APPLICATION->getThemedIcon("screenshot-placeholder"); - // un-const-ify this - ((ListModel*)this)->requestLogo(pack.logoName, pack.logoUrl); - return icon; - } - case Qt::SizeHintRole: - return QSize(0, 58); - case Qt::UserRole: { - QVariant v; - v.setValue(pack); - return v; - } - // Custom data - case UserDataTypes::TITLE: - return pack.name; - case UserDataTypes::DESCRIPTION: - return pack.description; - case UserDataTypes::SELECTED: - return m_parent->getDialog()->isModSelected(pack.name); - default: - break; - } - - return {}; -} - -bool ListModel::setData(const QModelIndex &index, const QVariant &value, int role) -{ - int pos = index.row(); - if (pos >= modpacks.size() || pos < 0 || !index.isValid()) - return false; - - modpacks[pos] = value.value<ModPlatform::IndexedPack>(); - - return true; -} - -void ListModel::requestModVersions(ModPlatform::IndexedPack const& current, QModelIndex index) -{ - auto profile = (dynamic_cast<MinecraftInstance*>((dynamic_cast<ModPage*>(parent()))->m_instance))->getPackProfile(); - - m_parent->apiProvider()->getVersions({ current.addonId.toString(), getMineVersions(), profile->getModLoaders() }, - [this, current, index](QJsonDocument& doc, QString addonId) { - if (!s_running.constFind(this).value()) - return; - versionRequestSucceeded(doc, addonId, index); - }); -} - -void ListModel::performPaginatedSearch() +ResourceAPI::SearchArgs ModModel::createSearchArguments() { - auto profile = (dynamic_cast<MinecraftInstance*>((dynamic_cast<ModPage*>(parent()))->m_instance))->getPackProfile(); - - m_parent->apiProvider()->searchMods( - this, { nextSearchOffset, currentSearchTerm, getSorts()[currentSort], profile->getModLoaders(), getMineVersions() }); -} + auto profile = static_cast<MinecraftInstance const&>(m_base_instance).getPackProfile(); -void ListModel::requestModInfo(ModPlatform::IndexedPack& current, QModelIndex index) -{ - m_parent->apiProvider()->getModInfo(current, [this, index](QJsonDocument& doc, ModPlatform::IndexedPack& pack) { - if (!s_running.constFind(this).value()) - return; - infoRequestFinished(doc, pack, index); - }); -} + Q_ASSERT(profile); + Q_ASSERT(m_filter); -void ListModel::refresh() -{ - if (jobPtr) { - jobPtr->abort(); - searchState = ResetRequested; - return; - } else { - beginResetModel(); - modpacks.clear(); - endResetModel(); - searchState = None; - } - nextSearchOffset = 0; - performPaginatedSearch(); -} + std::optional<std::list<Version>> versions{}; -void ListModel::searchWithTerm(const QString& term, const int sort, const bool filter_changed) -{ - if (currentSearchTerm == term && currentSearchTerm.isNull() == term.isNull() && currentSort == sort && !filter_changed) { - return; + { // Version filter + if (!m_filter->versions.empty()) + versions = m_filter->versions; } - currentSearchTerm = term; - currentSort = sort; - - refresh(); -} + auto sort = getCurrentSortingMethodByIndex(); -void ListModel::getLogo(const QString& logo, const QString& logoUrl, LogoCallback callback) -{ - if (m_logoMap.contains(logo)) { - callback(APPLICATION->metacache() - ->resolveEntry(m_parent->metaEntryBase(), QString("logos/%1").arg(logo.section(".", 0, 0))) - ->getFullPath()); - } else { - requestLogo(logo, logoUrl); - } + return { ModPlatform::ResourceType::MOD, m_next_search_offset, m_search_term, sort, profile->getModLoaders(), versions }; } -void ListModel::requestLogo(QString logo, QString url) +ResourceAPI::VersionSearchArgs ModModel::createVersionsArguments(QModelIndex& entry) { - if (m_loadingLogos.contains(logo) || m_failedLogos.contains(logo) || url.isEmpty()) { - return; - } + auto& pack = *m_packs[entry.row()]; + auto profile = static_cast<MinecraftInstance const&>(m_base_instance).getPackProfile(); - MetaEntryPtr entry = - APPLICATION->metacache()->resolveEntry(m_parent->metaEntryBase(), QString("logos/%1").arg(logo.section(".", 0, 0))); - auto job = new NetJob(QString("%1 Icon Download %2").arg(m_parent->debugName()).arg(logo), APPLICATION->network()); - job->addNetAction(Net::Download::makeCached(QUrl(url), entry)); + Q_ASSERT(profile); + Q_ASSERT(m_filter); - auto fullPath = entry->getFullPath(); - QObject::connect(job, &NetJob::succeeded, this, [this, logo, fullPath, job] { - job->deleteLater(); - emit logoLoaded(logo, QIcon(fullPath)); - if (waitingCallbacks.contains(logo)) { - waitingCallbacks.value(logo)(fullPath); - } - }); + std::optional<std::list<Version>> versions{}; + if (!m_filter->versions.empty()) + versions = m_filter->versions; - QObject::connect(job, &NetJob::failed, this, [this, logo, job] { - job->deleteLater(); - emit logoFailed(logo); - }); - - job->start(); - m_loadingLogos.append(logo); -} - -/******** Request callbacks ********/ - -void ListModel::logoLoaded(QString logo, QIcon out) -{ - m_loadingLogos.removeAll(logo); - m_logoMap.insert(logo, out); - for (int i = 0; i < modpacks.size(); i++) { - if (modpacks[i].logoName == logo) { - emit dataChanged(createIndex(i, 0), createIndex(i, 0), { Qt::DecorationRole }); - } - } + return { pack, versions, profile->getModLoaders() }; } -void ListModel::logoFailed(QString logo) +ResourceAPI::ProjectInfoArgs ModModel::createInfoArguments(QModelIndex& entry) { - m_failedLogos.append(logo); - m_loadingLogos.removeAll(logo); + auto& pack = *m_packs[entry.row()]; + return { pack }; } -void ListModel::searchRequestFinished(QJsonDocument& doc) +void ModModel::searchWithTerm(const QString& term, unsigned int sort, bool filter_changed) { - jobPtr.reset(); - - QList<ModPlatform::IndexedPack> newList; - auto packs = documentToArray(doc); - - for (auto packRaw : packs) { - auto packObj = packRaw.toObject(); - - ModPlatform::IndexedPack pack; - try { - loadIndexedPack(pack, packObj); - newList.append(pack); - } catch (const JSONValidationError& e) { - qWarning() << "Error while loading mod from " << m_parent->debugName() << ": " << e.cause(); - continue; - } - } - - if (packs.size() < 25) { - searchState = Finished; - } else { - nextSearchOffset += 25; - searchState = CanPossiblyFetchMore; - } - - // When you have a Qt build with assertions turned on, proceeding here will abort the application - if (newList.size() == 0) + if (m_search_term == term && m_search_term.isNull() == term.isNull() && m_current_sort_index == sort && !filter_changed) { return; - - beginInsertRows(QModelIndex(), modpacks.size(), modpacks.size() + newList.size() - 1); - modpacks.append(newList); - endInsertRows(); -} - -void ListModel::searchRequestFailed(QString reason) -{ - auto failed_action = jobPtr->getFailedActions().at(0); - if (!failed_action->m_reply) { - // Network error - QMessageBox::critical(nullptr, tr("Error"), tr("A network error occurred. Could not load mods.")); - } else if (failed_action->m_reply && failed_action->m_reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 409) { - // 409 Gone, notify user to update - QMessageBox::critical(nullptr, tr("Error"), - //: %1 refers to the launcher itself - QString("%1 %2") - .arg(m_parent->displayName()) - .arg(tr("API version too old!\nPlease update %1!").arg(BuildConfig.LAUNCHER_DISPLAYNAME))); } - jobPtr.reset(); - searchState = Finished; -} - -void ListModel::searchRequestAborted() -{ - if (searchState != ResetRequested) - qCritical() << "Search task in ModModel aborted by an unknown reason!"; - - // Retry fetching - jobPtr.reset(); - - beginResetModel(); - modpacks.clear(); - endResetModel(); - - nextSearchOffset = 0; - performPaginatedSearch(); -} + setSearchTerm(term); + m_current_sort_index = sort; -void ListModel::infoRequestFinished(QJsonDocument& doc, ModPlatform::IndexedPack& pack, const QModelIndex& index) -{ - qDebug() << "Loading mod info"; - - try { - auto obj = Json::requireObject(doc); - loadExtraPackInfo(pack, obj); - } catch (const JSONValidationError& e) { - qDebug() << doc; - qWarning() << "Error while reading " << debugName() << " mod info: " << e.cause(); - } - - // Check if the index is still valid for this mod or not - if (pack.addonId == data(index, Qt::UserRole).value<ModPlatform::IndexedPack>().addonId) { - // Cache info :^) - QVariant new_pack; - new_pack.setValue(pack); - if (!setData(index, new_pack, Qt::UserRole)) { - qWarning() << "Failed to cache mod info!"; - } - } - - m_parent->updateUi(); -} - -void ListModel::versionRequestSucceeded(QJsonDocument doc, QString addonId, const QModelIndex& index) -{ - auto& current = m_parent->getCurrent(); - if (addonId != current.addonId) { - return; - } - - auto arr = doc.isObject() ? Json::ensureArray(doc.object(), "data") : doc.array(); - - try { - loadIndexedPackVersions(current, arr); - } catch (const JSONValidationError& e) { - qDebug() << doc; - qWarning() << "Error while reading " << debugName() << " mod version: " << e.cause(); - } - - // Cache info :^) - QVariant new_pack; - new_pack.setValue(current); - if (!setData(index, new_pack, Qt::UserRole)) { - qWarning() << "Failed to cache mod versions!"; - } - - - m_parent->updateModVersions(); + refresh(); } -} // namespace ModPlatform - -/******** Helpers ********/ - -auto ModPlatform::ListModel::getMineVersions() const -> std::list<Version> -{ - return m_parent->getFilter()->versions; -} +} // namespace ResourceDownload diff --git a/launcher/ui/pages/modplatform/ModModel.h b/launcher/ui/pages/modplatform/ModModel.h index 36840649..5d4a7785 100644 --- a/launcher/ui/pages/modplatform/ModModel.h +++ b/launcher/ui/pages/modplatform/ModModel.h @@ -1,92 +1,52 @@ +// SPDX-FileCopyrightText: 2023 flowln <flowlnlnln@gmail.com> +// +// SPDX-License-Identifier: GPL-3.0-only + #pragma once #include <QAbstractListModel> +#include "BaseInstance.h" + #include "modplatform/ModIndex.h" -#include "net/NetJob.h" +#include "modplatform/ResourceAPI.h" + +#include "ui/pages/modplatform/ResourceModel.h" +#include "ui/widgets/ModFilterWidget.h" -class ModPage; class Version; -namespace ModPlatform { +namespace ResourceDownload { -using LogoMap = QMap<QString, QIcon>; -using LogoCallback = std::function<void (QString)>; +class ModPage; -class ListModel : public QAbstractListModel { +class ModModel : public ResourceModel { Q_OBJECT public: - ListModel(ModPage* parent); - ~ListModel() override; - - inline auto rowCount(const QModelIndex& parent) const -> int override { return parent.isValid() ? 0 : modpacks.size(); }; - inline auto columnCount(const QModelIndex& parent) const -> int override { return parent.isValid() ? 0 : 1; }; - inline auto flags(const QModelIndex& index) const -> Qt::ItemFlags override { return QAbstractListModel::flags(index); }; - - auto debugName() const -> QString; - - /* Retrieve information from the model at a given index with the given role */ - auto data(const QModelIndex& index, int role) const -> QVariant override; - bool setData(const QModelIndex &index, const QVariant &value, int role) override; - - inline void setActiveJob(NetJob::Ptr ptr) { jobPtr = ptr; } - inline NetJob* activeJob() { return jobPtr.get(); } + ModModel(const BaseInstance&, ResourceAPI* api); /* Ask the API for more information */ - void fetchMore(const QModelIndex& parent) override; - void refresh(); - void searchWithTerm(const QString& term, const int sort, const bool filter_changed); - void requestModInfo(ModPlatform::IndexedPack& current, QModelIndex index); - void requestModVersions(const ModPlatform::IndexedPack& current, QModelIndex index); + void searchWithTerm(const QString& term, unsigned int sort, bool filter_changed); - virtual void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) = 0; - virtual void loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj) = 0; - virtual void loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) = 0; + void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) override = 0; + void loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj) override = 0; + void loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) override = 0; - void getLogo(const QString& logo, const QString& logoUrl, LogoCallback callback); - - inline auto canFetchMore(const QModelIndex& parent) const -> bool override { return parent.isValid() ? false : searchState == CanPossiblyFetchMore; }; + void setFilter(std::shared_ptr<ModFilterWidget::Filter> filter) { m_filter = filter; } public slots: - void searchRequestFinished(QJsonDocument& doc); - void searchRequestFailed(QString reason); - void searchRequestAborted(); - - void infoRequestFinished(QJsonDocument& doc, ModPlatform::IndexedPack& pack, const QModelIndex& index); - - void versionRequestSucceeded(QJsonDocument doc, QString addonId, const QModelIndex& index); - - protected slots: - - void logoFailed(QString logo); - void logoLoaded(QString logo, QIcon out); - - void performPaginatedSearch(); + ResourceAPI::SearchArgs createSearchArguments() override; + ResourceAPI::VersionSearchArgs createVersionsArguments(QModelIndex&) override; + ResourceAPI::ProjectInfoArgs createInfoArguments(QModelIndex&) override; protected: - virtual auto documentToArray(QJsonDocument& obj) const -> QJsonArray = 0; - virtual auto getSorts() const -> const char** = 0; - - void requestLogo(QString file, QString url); - - inline auto getMineVersions() const -> std::list<Version>; + auto documentToArray(QJsonDocument& obj) const -> QJsonArray override = 0; protected: - ModPage* m_parent; - - QList<ModPlatform::IndexedPack> modpacks; - - LogoMap m_logoMap; - QMap<QString, LogoCallback> waitingCallbacks; - QStringList m_failedLogos; - QStringList m_loadingLogos; + const BaseInstance& m_base_instance; - QString currentSearchTerm; - int currentSort = 0; - int nextSearchOffset = 0; - enum SearchState { None, CanPossiblyFetchMore, ResetRequested, Finished } searchState = None; - - NetJob::Ptr jobPtr; + std::shared_ptr<ModFilterWidget::Filter> m_filter = nullptr; }; -} // namespace ModPlatform + +} // namespace ResourceDownload diff --git a/launcher/ui/pages/modplatform/ModPage.cpp b/launcher/ui/pages/modplatform/ModPage.cpp index 677bc4d6..04be43ad 100644 --- a/launcher/ui/pages/modplatform/ModPage.cpp +++ b/launcher/ui/pages/modplatform/ModPage.cpp @@ -1,4 +1,6 @@ -// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2023 flowln <flowlnlnln@gmail.com> +// +// SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0 /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> @@ -35,61 +37,30 @@ */ #include "ModPage.h" -#include "Application.h" -#include "ui_ModPage.h" +#include "ui_ResourcePage.h" #include <QDesktopServices> #include <QKeyEvent> #include <QRegularExpression> + #include <memory> -#include <HoeDown.h> +#include "Application.h" +#include "ResourceDownloadTask.h" #include "minecraft/MinecraftInstance.h" #include "minecraft/PackProfile.h" -#include "ui/dialogs/ModDownloadDialog.h" -#include "ui/widgets/ProjectItem.h" - - -ModPage::ModPage(ModDownloadDialog* dialog, BaseInstance* instance, ModAPI* api) - : QWidget(dialog) - , m_instance(instance) - , ui(new Ui::ModPage) - , dialog(dialog) - , m_fetch_progress(this, false) - , api(api) -{ - ui->setupUi(this); - - connect(ui->searchButton, &QPushButton::clicked, this, &ModPage::triggerSearch); - connect(ui->modFilterButton, &QPushButton::clicked, this, &ModPage::filterMods); - connect(ui->packView, &QListView::doubleClicked, this, &ModPage::onModSelected); - m_search_timer.setTimerType(Qt::TimerType::CoarseTimer); - m_search_timer.setSingleShot(true); +#include "ui/dialogs/ResourceDownloadDialog.h" - connect(&m_search_timer, &QTimer::timeout, this, &ModPage::triggerSearch); - - ui->searchEdit->installEventFilter(this); - - ui->versionSelectionBox->view()->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); - ui->versionSelectionBox->view()->parentWidget()->setMaximumHeight(300); - - m_fetch_progress.hideIfInactive(true); - m_fetch_progress.setFixedHeight(24); - m_fetch_progress.progressFormat(""); - - ui->gridLayout_3->addWidget(&m_fetch_progress, 0, 0, 1, ui->gridLayout_3->columnCount()); - - ui->packView->setItemDelegate(new ProjectItemDelegate(this)); - ui->packView->installEventFilter(this); - - connect(ui->packDescription, &QTextBrowser::anchorClicked, this, &ModPage::openUrl); -} +namespace ResourceDownload { -ModPage::~ModPage() +ModPage::ModPage(ModDownloadDialog* dialog, BaseInstance& instance) + : ResourcePage(dialog, instance) { - delete ui; + connect(m_ui->searchButton, &QPushButton::clicked, this, &ModPage::triggerSearch); + connect(m_ui->resourceFilterButton, &QPushButton::clicked, this, &ModPage::filterMods); + connect(m_ui->packView, &QListView::doubleClicked, this, &ModPage::onResourceSelected); } void ModPage::setFilterWidget(unique_qobject_ptr<ModFilterWidget>& widget) @@ -99,59 +70,19 @@ void ModPage::setFilterWidget(unique_qobject_ptr<ModFilterWidget>& widget) m_filter_widget.swap(widget); - ui->gridLayout_3->addWidget(m_filter_widget.get(), 0, 0, 1, ui->gridLayout_3->columnCount()); + m_ui->gridLayout_3->addWidget(m_filter_widget.get(), 0, 0, 1, m_ui->gridLayout_3->columnCount()); - m_filter_widget->setInstance(static_cast<MinecraftInstance*>(m_instance)); + m_filter_widget->setInstance(&static_cast<MinecraftInstance&>(m_base_instance)); m_filter = m_filter_widget->getFilter(); connect(m_filter_widget.get(), &ModFilterWidget::filterChanged, this, [&]{ - ui->searchButton->setStyleSheet("text-decoration: underline"); + m_ui->searchButton->setStyleSheet("text-decoration: underline"); }); connect(m_filter_widget.get(), &ModFilterWidget::filterUnchanged, this, [&]{ - ui->searchButton->setStyleSheet("text-decoration: none"); + m_ui->searchButton->setStyleSheet("text-decoration: none"); }); } - -/******** Qt things ********/ - -void ModPage::openedImpl() -{ - updateSelectionButton(); - triggerSearch(); -} - -auto ModPage::eventFilter(QObject* watched, QEvent* event) -> bool -{ - if (watched == ui->searchEdit && event->type() == QEvent::KeyPress) { - auto* keyEvent = dynamic_cast<QKeyEvent*>(event); - if (keyEvent->key() == Qt::Key_Return) { - triggerSearch(); - keyEvent->accept(); - return true; - } else { - if (m_search_timer.isActive()) - m_search_timer.stop(); - - m_search_timer.start(350); - } - } else if (watched == ui->packView && event->type() == QEvent::KeyPress) { - auto* keyEvent = dynamic_cast<QKeyEvent*>(event); - if (keyEvent->key() == Qt::Key_Return) { - onModSelected(); - - // To have the 'select mod' button outlined instead of the 'review and confirm' one - ui->modSelectionButton->setFocus(Qt::FocusReason::ShortcutFocusReason); - ui->packView->setFocus(Qt::FocusReason::NoFocusReason); - - keyEvent->accept(); - return true; - } - } - return QWidget::eventFilter(watched, event); -} - - /******** Callbacks to events in the UI (set up in the derived classes) ********/ void ModPage::filterMods() @@ -165,176 +96,37 @@ void ModPage::triggerSearch() m_filter = m_filter_widget->getFilter(); if (changed) { - ui->packView->clearSelection(); - ui->packDescription->clear(); - ui->versionSelectionBox->clear(); - updateSelectionButton(); - } - - listModel->searchWithTerm(getSearchTerm(), ui->sortByBox->currentIndex(), changed); - m_fetch_progress.watch(listModel->activeJob()); -} - -QString ModPage::getSearchTerm() const -{ - return ui->searchEdit->text(); -} -void ModPage::setSearchTerm(QString term) -{ - ui->searchEdit->setText(term); -} - -void ModPage::onSelectionChanged(QModelIndex curr, QModelIndex prev) -{ - ui->versionSelectionBox->clear(); - - if (!curr.isValid()) { return; } - - current = listModel->data(curr, Qt::UserRole).value<ModPlatform::IndexedPack>(); - - if (!current.versionsLoaded) { - qDebug() << QString("Loading %1 mod versions").arg(debugName()); - - ui->modSelectionButton->setText(tr("Loading versions...")); - ui->modSelectionButton->setEnabled(false); - - listModel->requestModVersions(current, curr); - } else { - for (int i = 0; i < current.versions.size(); i++) { - ui->versionSelectionBox->addItem(current.versions[i].version, QVariant(i)); - } - if (ui->versionSelectionBox->count() == 0) { ui->versionSelectionBox->addItem(tr("No valid version found."), QVariant(-1)); } - + m_ui->packView->clearSelection(); + m_ui->packDescription->clear(); + m_ui->versionSelectionBox->clear(); updateSelectionButton(); } - if(!current.extraDataLoaded){ - qDebug() << QString("Loading %1 mod info").arg(debugName()); - - listModel->requestModInfo(current, curr); - } - - updateUi(); -} - -void ModPage::onVersionSelectionChanged(QString data) -{ - if (data.isNull() || data.isEmpty()) { - selectedVersion = -1; - return; - } - selectedVersion = ui->versionSelectionBox->currentData().toInt(); - updateSelectionButton(); -} - -void ModPage::onModSelected() -{ - if (selectedVersion < 0) - return; - - auto& version = current.versions[selectedVersion]; - if (dialog->isModSelected(current.name, version.fileName)) { - dialog->removeSelectedMod(current.name); - } else { - bool is_indexed = !APPLICATION->settings()->get("ModMetadataDisabled").toBool(); - dialog->addSelectedMod(current.name, new ModDownloadTask(current, version, dialog->mods, is_indexed)); - } - - updateSelectionButton(); - - /* Force redraw on the mods list when the selection changes */ - ui->packView->adjustSize(); + static_cast<ModModel*>(m_model)->searchWithTerm(getSearchTerm(), m_ui->sortByBox->currentData().toUInt(), changed); + m_fetch_progress.watch(m_model->activeSearchJob().get()); } -static const QRegularExpression modrinth(QRegularExpression::anchoredPattern("(?:www\\.)?modrinth\\.com\\/mod\\/([^\\/]+)\\/?")); -static const QRegularExpression curseForge(QRegularExpression::anchoredPattern("(?:www\\.)?curseforge\\.com\\/minecraft\\/mc-mods\\/([^\\/]+)\\/?")); -static const QRegularExpression curseForgeOld(QRegularExpression::anchoredPattern("minecraft\\.curseforge\\.com\\/projects\\/([^\\/]+)\\/?")); - -void ModPage::openUrl(const QUrl& url) +QMap<QString, QString> ModPage::urlHandlers() const { - // do not allow other url schemes for security reasons - if (!(url.scheme() == "http" || url.scheme() == "https")) { - qWarning() << "Unsupported scheme" << url.scheme(); - return; - } - - // detect mod URLs and search instead - - const QString address = url.host() + url.path(); - QRegularExpressionMatch match; - QString page; - - match = modrinth.match(address); - if (match.hasMatch()) - page = "modrinth"; - else if (APPLICATION->capabilities() & Application::SupportsFlame) { - match = curseForge.match(address); - if (!match.hasMatch()) - match = curseForgeOld.match(address); - - if (match.hasMatch()) - page = "curseforge"; - } - - if (!page.isNull()) { - const QString slug = match.captured(1); - - // ensure the user isn't opening the same mod - if (slug != current.slug) { - dialog->selectPage(page); - - ModPage* newPage = dialog->getSelectedPage(); - - QLineEdit* searchEdit = newPage->ui->searchEdit; - ModPlatform::ListModel* model = newPage->listModel; - QListView* view = newPage->ui->packView; - - auto jump = [url, slug, model, view] { - for (int row = 0; row < model->rowCount({}); row++) { - const QModelIndex index = model->index(row); - const auto pack = model->data(index, Qt::UserRole).value<ModPlatform::IndexedPack>(); - - if (pack.slug == slug) { - view->setCurrentIndex(index); - return; - } - } - - // The final fallback. - QDesktopServices::openUrl(url); - }; - - searchEdit->setText(slug); - newPage->triggerSearch(); - - if (model->activeJob()) - connect(model->activeJob(), &Task::finished, jump); - else - jump(); - - return; - } - } - - // open in the user's web browser - QDesktopServices::openUrl(url); + QMap<QString, QString> map; + map.insert(QRegularExpression::anchoredPattern("(?:www\\.)?modrinth\\.com\\/mod\\/([^\\/]+)\\/?"), "modrinth"); + map.insert(QRegularExpression::anchoredPattern("(?:www\\.)?curseforge\\.com\\/minecraft\\/mc-mods\\/([^\\/]+)\\/?"), "curseforge"); + map.insert(QRegularExpression::anchoredPattern("minecraft\\.curseforge\\.com\\/projects\\/([^\\/]+)\\/?"), "curseforge"); + return map; } /******** Make changes to the UI ********/ -void ModPage::retranslate() -{ - ui->retranslateUi(this); -} - -void ModPage::updateModVersions(int prev_count) +void ModPage::updateVersionList() { - auto packProfile = (dynamic_cast<MinecraftInstance*>(m_instance))->getPackProfile(); + m_ui->versionSelectionBox->clear(); + auto packProfile = (dynamic_cast<MinecraftInstance&>(m_base_instance)).getPackProfile(); QString mcVersion = packProfile->getComponentVersion("net.minecraft"); - for (int i = 0; i < current.versions.size(); i++) { - auto version = current.versions[i]; + auto current_pack = getCurrentPack(); + for (int i = 0; i < current_pack.versions.size(); i++) { + auto version = current_pack.versions[i]; bool valid = false; for(auto& mcVer : m_filter->versions){ //NOTE: Flame doesn't care about loader, so passing it changes nothing. @@ -346,88 +138,20 @@ void ModPage::updateModVersions(int prev_count) // Only add the version if it's valid or using the 'Any' filter, but never if the version is opted out if ((valid || m_filter->versions.empty()) && !optedOut(version)) - ui->versionSelectionBox->addItem(version.version, QVariant(i)); + m_ui->versionSelectionBox->addItem(version.version, QVariant(i)); } - if (ui->versionSelectionBox->count() == 0 && prev_count != 0) { - ui->versionSelectionBox->addItem(tr("No valid version found!"), QVariant(-1)); - ui->modSelectionButton->setText(tr("Cannot select invalid version :(")); + if (m_ui->versionSelectionBox->count() == 0) { + m_ui->versionSelectionBox->addItem(tr("No valid version found!"), QVariant(-1)); + m_ui->resourceSelectionButton->setText(tr("Cannot select invalid version :(")); } updateSelectionButton(); } - -void ModPage::updateSelectionButton() +void ModPage::addResourceToDialog(ModPlatform::IndexedPack& pack, ModPlatform::IndexedVersion& version) { - if (!isOpened || selectedVersion < 0) { - ui->modSelectionButton->setEnabled(false); - return; - } - - ui->modSelectionButton->setEnabled(true); - auto& version = current.versions[selectedVersion]; - if (!dialog->isModSelected(current.name, version.fileName)) { - ui->modSelectionButton->setText(tr("Select mod for download")); - } else { - ui->modSelectionButton->setText(tr("Deselect mod for download")); - } + bool is_indexed = !APPLICATION->settings()->get("ModMetadataDisabled").toBool(); + m_parent_dialog->addResource(pack, version, is_indexed); } -void ModPage::updateUi() -{ - QString text = ""; - QString name = current.name; - - if (current.websiteUrl.isEmpty()) - text = name; - else - text = "<a href=\"" + current.websiteUrl + "\">" + name + "</a>"; - - if (!current.authors.empty()) { - auto authorToStr = [](ModPlatform::ModpackAuthor& author) -> QString { - if (author.url.isEmpty()) { return author.name; } - return QString("<a href=\"%1\">%2</a>").arg(author.url, author.name); - }; - QStringList authorStrs; - for (auto& author : current.authors) { - authorStrs.push_back(authorToStr(author)); - } - text += "<br>" + tr(" by ") + authorStrs.join(", "); - } - - if (current.extraDataLoaded) { - if (!current.extraData.donate.isEmpty()) { - text += "<br><br>" + tr("Donate information: "); - auto donateToStr = [](ModPlatform::DonationData& donate) -> QString { - return QString("<a href=\"%1\">%2</a>").arg(donate.url, donate.platform); - }; - QStringList donates; - for (auto& donate : current.extraData.donate) { - donates.append(donateToStr(donate)); - } - text += donates.join(", "); - } - - if (!current.extraData.issuesUrl.isEmpty() - || !current.extraData.sourceUrl.isEmpty() - || !current.extraData.wikiUrl.isEmpty() - || !current.extraData.discordUrl.isEmpty()) { - text += "<br><br>" + tr("External links:") + "<br>"; - } - - if (!current.extraData.issuesUrl.isEmpty()) - text += "- " + tr("Issues: <a href=%1>%1</a>").arg(current.extraData.issuesUrl) + "<br>"; - if (!current.extraData.wikiUrl.isEmpty()) - text += "- " + tr("Wiki: <a href=%1>%1</a>").arg(current.extraData.wikiUrl) + "<br>"; - if (!current.extraData.sourceUrl.isEmpty()) - text += "- " + tr("Source code: <a href=%1>%1</a>").arg(current.extraData.sourceUrl) + "<br>"; - if (!current.extraData.discordUrl.isEmpty()) - text += "- " + tr("Discord: <a href=%1>%1</a>").arg(current.extraData.discordUrl) + "<br>"; - } - - text += "<hr>"; - - HoeDown h; - ui->packDescription->setHtml(text + (current.extraData.body.isEmpty() ? current.description : h.process(current.extraData.body.toUtf8()))); - ui->packDescription->flush(); -} +} // namespace ResourceDownload diff --git a/launcher/ui/pages/modplatform/ModPage.h b/launcher/ui/pages/modplatform/ModPage.h index c9ccbaf2..4ea55efa 100644 --- a/launcher/ui/pages/modplatform/ModPage.h +++ b/launcher/ui/pages/modplatform/ModPage.h @@ -1,105 +1,74 @@ +// SPDX-FileCopyrightText: 2023 flowln <flowlnlnln@gmail.com> +// +// SPDX-License-Identifier: GPL-3.0-only + #pragma once #include <QWidget> -#include "Application.h" -#include "modplatform/ModAPI.h" #include "modplatform/ModIndex.h" -#include "ui/pages/BasePage.h" + +#include "ui/pages/modplatform/ResourcePage.h" #include "ui/pages/modplatform/ModModel.h" #include "ui/widgets/ModFilterWidget.h" -#include "ui/widgets/ProgressWidget.h" - -class ModDownloadDialog; namespace Ui { -class ModPage; +class ResourcePage; } +namespace ResourceDownload { + +class ModDownloadDialog; + /* This page handles most logic related to browsing and selecting mods to download. */ -class ModPage : public QWidget, public BasePage { +class ModPage : public ResourcePage { Q_OBJECT public: template<typename T> - static T* create(ModDownloadDialog* dialog, BaseInstance* instance) + static T* create(ModDownloadDialog* dialog, BaseInstance& instance) { auto page = new T(dialog, instance); + auto model = static_cast<ModModel*>(page->getModel()); - auto filter_widget = ModFilterWidget::create(static_cast<MinecraftInstance*>(instance)->getPackProfile()->getComponentVersion("net.minecraft"), page); + auto filter_widget = ModFilterWidget::create(static_cast<MinecraftInstance&>(instance).getPackProfile()->getComponentVersion("net.minecraft"), page); page->setFilterWidget(filter_widget); + model->setFilter(page->getFilter()); + + connect(model, &ResourceModel::versionListUpdated, page, &ResourcePage::updateVersionList); + connect(model, &ResourceModel::projectInfoUpdated, page, &ResourcePage::updateUi); return page; } - ~ModPage() override; - - /* Affects what the user sees */ - auto displayName() const -> QString override = 0; - auto icon() const -> QIcon override = 0; - auto id() const -> QString override = 0; - auto helpPage() const -> QString override = 0; - - /* Used internally */ - virtual auto metaEntryBase() const -> QString = 0; - virtual auto debugName() const -> QString = 0; - + //: The plural version of 'mod' + [[nodiscard]] inline QString resourcesString() const override { return tr("mods"); } + //: The singular version of 'mods' + [[nodiscard]] inline QString resourceString() const override { return tr("mod"); } - void retranslate() override; + [[nodiscard]] QMap<QString, QString> urlHandlers() const override; - void updateUi(); + void addResourceToDialog(ModPlatform::IndexedPack&, ModPlatform::IndexedVersion&) override; - auto shouldDisplay() const -> bool override = 0; - virtual auto validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, ModAPI::ModLoaderTypes loaders = ModAPI::Unspecified) const -> bool = 0; - virtual bool optedOut(ModPlatform::IndexedVersion& ver) const { return false; }; + virtual auto validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, std::optional<ResourceAPI::ModLoaderTypes> loaders = {}) const -> bool = 0; - auto apiProvider() -> ModAPI* { return api.get(); }; + [[nodiscard]] bool supportsFiltering() const override { return true; }; auto getFilter() const -> const std::shared_ptr<ModFilterWidget::Filter> { return m_filter; } - auto getDialog() const -> const ModDownloadDialog* { return dialog; } - - /** Get the current term in the search bar. */ - auto getSearchTerm() const -> QString; - /** Programatically set the term in the search bar. */ - void setSearchTerm(QString); - void setFilterWidget(unique_qobject_ptr<ModFilterWidget>&); - auto getCurrent() -> ModPlatform::IndexedPack& { return current; } - void updateModVersions(int prev_count = -1); - - void openedImpl() override; - auto eventFilter(QObject* watched, QEvent* event) -> bool override; - - BaseInstance* m_instance; + public slots: + void updateVersionList() override; protected: - ModPage(ModDownloadDialog* dialog, BaseInstance* instance, ModAPI* api); - void updateSelectionButton(); + ModPage(ModDownloadDialog* dialog, BaseInstance& instance); protected slots: virtual void filterMods(); - void triggerSearch(); - void onSelectionChanged(QModelIndex first, QModelIndex second); - void onVersionSelectionChanged(QString data); - void onModSelected(); - virtual void openUrl(const QUrl& url); + void triggerSearch() override; protected: - Ui::ModPage* ui = nullptr; - ModDownloadDialog* dialog = nullptr; - unique_qobject_ptr<ModFilterWidget> m_filter_widget; std::shared_ptr<ModFilterWidget::Filter> m_filter; - - ProgressWidget m_fetch_progress; - - ModPlatform::ListModel* listModel = nullptr; - ModPlatform::IndexedPack current; - - std::unique_ptr<ModAPI> api; - - int selectedVersion = -1; - - // Used to do instant searching with a delay to cache quick changes - QTimer m_search_timer; }; + +} // namespace ResourceDownload diff --git a/launcher/ui/pages/modplatform/ResourceModel.cpp b/launcher/ui/pages/modplatform/ResourceModel.cpp new file mode 100644 index 00000000..472aa851 --- /dev/null +++ b/launcher/ui/pages/modplatform/ResourceModel.cpp @@ -0,0 +1,445 @@ +// SPDX-FileCopyrightText: 2023 flowln <flowlnlnln@gmail.com> +// +// SPDX-License-Identifier: GPL-3.0-only + +#include "ResourceModel.h" + +#include <QCryptographicHash> +#include <QIcon> +#include <QMessageBox> +#include <QPixmapCache> +#include <QUrl> +#include <memory> + +#include "Application.h" +#include "BuildConfig.h" +#include "Json.h" + +#include "net/Download.h" +#include "net/NetJob.h" + +#include "modplatform/ModIndex.h" + +#include "ui/widgets/ProjectItem.h" + +namespace ResourceDownload { + +QHash<ResourceModel*, bool> ResourceModel::s_running_models; + +ResourceModel::ResourceModel(ResourceAPI* api) : QAbstractListModel(), m_api(api) +{ + s_running_models.insert(this, true); +} + +ResourceModel::~ResourceModel() +{ + s_running_models.find(this).value() = false; +} + +auto ResourceModel::data(const QModelIndex& index, int role) const -> QVariant +{ + int pos = index.row(); + if (pos >= m_packs.size() || pos < 0 || !index.isValid()) { + return QString("INVALID INDEX %1").arg(pos); + } + + auto pack = m_packs.at(pos); + switch (role) { + case Qt::ToolTipRole: { + if (pack->description.length() > 100) { + // some magic to prevent to long tooltips and replace html linebreaks + QString edit = pack->description.left(97); + edit = edit.left(edit.lastIndexOf("<br>")).left(edit.lastIndexOf(" ")).append("..."); + return edit; + } + return pack->description; + } + case Qt::DecorationRole: { + if (auto icon_or_none = const_cast<ResourceModel*>(this)->getIcon(const_cast<QModelIndex&>(index), pack->logoUrl); + icon_or_none.has_value()) + return icon_or_none.value(); + + return APPLICATION->getThemedIcon("screenshot-placeholder"); + } + case Qt::SizeHintRole: + return QSize(0, 58); + case Qt::UserRole: { + QVariant v; + v.setValue(*pack); + return v; + } + // Custom data + case UserDataTypes::TITLE: + return pack->name; + case UserDataTypes::DESCRIPTION: + return pack->description; + case UserDataTypes::SELECTED: + return pack->isAnyVersionSelected(); + default: + break; + } + + return {}; +} + +QHash<int, QByteArray> ResourceModel::roleNames() const +{ + QHash<int, QByteArray> roles; + + roles[Qt::ToolTipRole] = "toolTip"; + roles[Qt::DecorationRole] = "decoration"; + roles[Qt::SizeHintRole] = "sizeHint"; + roles[Qt::UserRole] = "pack"; + roles[UserDataTypes::TITLE] = "title"; + roles[UserDataTypes::DESCRIPTION] = "description"; + roles[UserDataTypes::SELECTED] = "selected"; + + return roles; +} + +bool ResourceModel::setData(const QModelIndex& index, const QVariant& value, int role) +{ + int pos = index.row(); + if (pos >= m_packs.size() || pos < 0 || !index.isValid()) + return false; + + m_packs[pos] = std::make_shared<ModPlatform::IndexedPack>(value.value<ModPlatform::IndexedPack>()); + emit dataChanged(index, index); + + return true; +} + +QString ResourceModel::debugName() const +{ + return "ResourceDownload (Model)"; +} + +void ResourceModel::fetchMore(const QModelIndex& parent) +{ + if (parent.isValid() || m_search_state == SearchState::Finished) + return; + + search(); +} + +void ResourceModel::search() +{ + if (hasActiveSearchJob()) + return; + + auto args{ createSearchArguments() }; + + auto callbacks{ createSearchCallbacks() }; + + // Use defaults if no callbacks are set + if (!callbacks.on_succeed) + callbacks.on_succeed = [this](auto& doc) { + if (!s_running_models.constFind(this).value()) + return; + searchRequestSucceeded(doc); + }; + if (!callbacks.on_fail) + callbacks.on_fail = [this](QString reason, int network_error_code) { + if (!s_running_models.constFind(this).value()) + return; + searchRequestFailed(reason, network_error_code); + }; + if (!callbacks.on_abort) + callbacks.on_abort = [this] { + if (!s_running_models.constFind(this).value()) + return; + searchRequestAborted(); + }; + + if (auto job = m_api->searchProjects(std::move(args), std::move(callbacks)); job) + runSearchJob(job); +} + +void ResourceModel::loadEntry(QModelIndex& entry) +{ + auto const& pack = m_packs[entry.row()]; + + if (!hasActiveInfoJob()) + m_current_info_job.clear(); + + if (!pack->versionsLoaded) { + auto args{ createVersionsArguments(entry) }; + auto callbacks{ createVersionsCallbacks(entry) }; + + // Use default if no callbacks are set + if (!callbacks.on_succeed) + callbacks.on_succeed = [this, entry](auto& doc, auto pack) { + if (!s_running_models.constFind(this).value()) + return; + versionRequestSucceeded(doc, pack, entry); + }; + + if (auto job = m_api->getProjectVersions(std::move(args), std::move(callbacks)); job) + runInfoJob(job); + } + + if (!pack->extraDataLoaded) { + auto args{ createInfoArguments(entry) }; + auto callbacks{ createInfoCallbacks(entry) }; + + // Use default if no callbacks are set + if (!callbacks.on_succeed) + callbacks.on_succeed = [this, entry](auto& doc, auto pack) { + if (!s_running_models.constFind(this).value()) + return; + infoRequestSucceeded(doc, pack, entry); + }; + + if (auto job = m_api->getProjectInfo(std::move(args), std::move(callbacks)); job) + runInfoJob(job); + } +} + +void ResourceModel::refresh() +{ + bool reset_requested = false; + + if (hasActiveInfoJob()) { + m_current_info_job.abort(); + reset_requested = true; + } + + if (hasActiveSearchJob()) { + m_current_search_job->abort(); + reset_requested = true; + } + + if (reset_requested) { + m_search_state = SearchState::ResetRequested; + return; + } + + clearData(); + m_search_state = SearchState::None; + + m_next_search_offset = 0; + search(); +} + +void ResourceModel::clearData() +{ + beginResetModel(); + m_packs.clear(); + endResetModel(); +} + +void ResourceModel::runSearchJob(Task::Ptr ptr) +{ + m_current_search_job.reset(ptr); // clean up first + m_current_search_job->start(); +} +void ResourceModel::runInfoJob(Task::Ptr ptr) +{ + if (!m_current_info_job.isRunning()) + m_current_info_job.clear(); + + m_current_info_job.addTask(ptr); + + if (!m_current_info_job.isRunning()) + m_current_info_job.run(); +} + +std::optional<ResourceAPI::SortingMethod> ResourceModel::getCurrentSortingMethodByIndex() const +{ + std::optional<ResourceAPI::SortingMethod> sort{}; + + { // Find sorting method by ID + auto sorting_methods = getSortingMethods(); + auto method = std::find_if(sorting_methods.constBegin(), sorting_methods.constEnd(), + [this](auto const& e) { return m_current_sort_index == e.index; }); + if (method != sorting_methods.constEnd()) + sort = *method; + } + + return sort; +} + +std::optional<QIcon> ResourceModel::getIcon(QModelIndex& index, const QUrl& url) +{ + QPixmap pixmap; + if (QPixmapCache::find(url.toString(), &pixmap)) + return { pixmap }; + + if (!m_current_icon_job) + m_current_icon_job.reset(new NetJob("IconJob", APPLICATION->network())); + + if (m_currently_running_icon_actions.contains(url)) + return {}; + if (m_failed_icon_actions.contains(url)) + return {}; + + auto cache_entry = APPLICATION->metacache()->resolveEntry( + metaEntryBase(), + QString("logos/%1").arg(QString(QCryptographicHash::hash(url.toEncoded(), QCryptographicHash::Algorithm::Sha1).toHex()))); + auto icon_fetch_action = Net::Download::makeCached(url, cache_entry); + + auto full_file_path = cache_entry->getFullPath(); + connect(icon_fetch_action.get(), &NetAction::succeeded, this, [=] { + auto icon = QIcon(full_file_path); + QPixmapCache::insert(url.toString(), icon.pixmap(icon.actualSize({ 64, 64 }))); + + m_currently_running_icon_actions.remove(url); + + emit dataChanged(index, index, { Qt::DecorationRole }); + }); + connect(icon_fetch_action.get(), &NetAction::failed, this, [=] { + m_currently_running_icon_actions.remove(url); + m_failed_icon_actions.insert(url); + }); + + m_currently_running_icon_actions.insert(url); + + m_current_icon_job->addNetAction(icon_fetch_action); + if (!m_current_icon_job->isRunning()) + QMetaObject::invokeMethod(m_current_icon_job.get(), &NetJob::start); + + return {}; +} + +// No 'forgor to implement' shall pass here :blobfox_knife: +#define NEED_FOR_CALLBACK_ASSERT(name) \ + Q_ASSERT_X(0 != 0, #name, "You NEED to re-implement this if you intend on using the default callbacks.") + +QJsonArray ResourceModel::documentToArray(QJsonDocument& doc) const +{ + NEED_FOR_CALLBACK_ASSERT("documentToArray"); + return {}; +} +void ResourceModel::loadIndexedPack(ModPlatform::IndexedPack&, QJsonObject&) +{ + NEED_FOR_CALLBACK_ASSERT("loadIndexedPack"); +} +void ResourceModel::loadExtraPackInfo(ModPlatform::IndexedPack&, QJsonObject&) +{ + NEED_FOR_CALLBACK_ASSERT("loadExtraPackInfo"); +} +void ResourceModel::loadIndexedPackVersions(ModPlatform::IndexedPack&, QJsonArray&) +{ + NEED_FOR_CALLBACK_ASSERT("loadIndexedPackVersions"); +} + +/* Default callbacks */ + +void ResourceModel::searchRequestSucceeded(QJsonDocument& doc) +{ + QList<ModPlatform::IndexedPack::Ptr> newList; + auto packs = documentToArray(doc); + + for (auto packRaw : packs) { + auto packObj = packRaw.toObject(); + + ModPlatform::IndexedPack::Ptr pack = std::make_shared<ModPlatform::IndexedPack>(); + try { + loadIndexedPack(*pack, packObj); + newList.append(pack); + } catch (const JSONValidationError& e) { + qWarning() << "Error while loading resource from " << debugName() << ": " << e.cause(); + continue; + } + } + + if (packs.size() < 25) { + m_search_state = SearchState::Finished; + } else { + m_next_search_offset += 25; + m_search_state = SearchState::CanFetchMore; + } + + // When you have a Qt build with assertions turned on, proceeding here will abort the application + if (newList.size() == 0) + return; + + beginInsertRows(QModelIndex(), m_packs.size(), m_packs.size() + newList.size() - 1); + m_packs.append(newList); + endInsertRows(); +} + +void ResourceModel::searchRequestFailed(QString reason, int network_error_code) +{ + switch (network_error_code) { + default: + // Network error + QMessageBox::critical(nullptr, tr("Error"), tr("A network error occurred. Could not load mods.")); + break; + case 409: + // 409 Gone, notify user to update + QMessageBox::critical(nullptr, tr("Error"), + QString("%1").arg(tr("API version too old!\nPlease update %1!").arg(BuildConfig.LAUNCHER_DISPLAYNAME))); + break; + } + + m_search_state = SearchState::Finished; +} + +void ResourceModel::searchRequestAborted() +{ + if (m_search_state != SearchState::ResetRequested) + qCritical() << "Search task in" << debugName() << "aborted by an unknown reason!"; + + // Retry fetching + clearData(); + + m_next_search_offset = 0; + search(); +} + +void ResourceModel::versionRequestSucceeded(QJsonDocument& doc, ModPlatform::IndexedPack& pack, const QModelIndex& index) +{ + auto current_pack = data(index, Qt::UserRole).value<ModPlatform::IndexedPack>(); + + // Check if the index is still valid for this resource or not + if (pack.addonId != current_pack.addonId) + return; + + try { + auto arr = doc.isObject() ? Json::ensureArray(doc.object(), "data") : doc.array(); + loadIndexedPackVersions(current_pack, arr); + } catch (const JSONValidationError& e) { + qDebug() << doc; + qWarning() << "Error while reading " << debugName() << " resource version: " << e.cause(); + } + + // Cache info :^) + QVariant new_pack; + new_pack.setValue(current_pack); + if (!setData(index, new_pack, Qt::UserRole)) { + qWarning() << "Failed to cache resource versions!"; + return; + } + + emit versionListUpdated(); +} + +void ResourceModel::infoRequestSucceeded(QJsonDocument& doc, ModPlatform::IndexedPack& pack, const QModelIndex& index) +{ + auto current_pack = data(index, Qt::UserRole).value<ModPlatform::IndexedPack>(); + + // Check if the index is still valid for this resource or not + if (pack.addonId != current_pack.addonId) + return; + + try { + auto obj = Json::requireObject(doc); + loadExtraPackInfo(current_pack, obj); + } catch (const JSONValidationError& e) { + qDebug() << doc; + qWarning() << "Error while reading " << debugName() << " resource info: " << e.cause(); + } + + // Cache info :^) + QVariant new_pack; + new_pack.setValue(current_pack); + if (!setData(index, new_pack, Qt::UserRole)) { + qWarning() << "Failed to cache resource info!"; + return; + } + + emit projectInfoUpdated(); +} + +} // namespace ResourceDownload diff --git a/launcher/ui/pages/modplatform/ResourceModel.h b/launcher/ui/pages/modplatform/ResourceModel.h new file mode 100644 index 00000000..1ec42cda --- /dev/null +++ b/launcher/ui/pages/modplatform/ResourceModel.h @@ -0,0 +1,147 @@ +// SPDX-FileCopyrightText: 2023 flowln <flowlnlnln@gmail.com> +// +// SPDX-License-Identifier: GPL-3.0-only + +#pragma once + +#include <optional> + +#include <QAbstractListModel> + +#include "QObjectPtr.h" + +#include "modplatform/ResourceAPI.h" + +#include "tasks/ConcurrentTask.h" + +class NetJob; +class ResourceAPI; + +namespace ModPlatform { +struct IndexedPack; +} + +namespace ResourceDownload { + +class ResourceModel : public QAbstractListModel { + Q_OBJECT + + Q_PROPERTY(QString search_term MEMBER m_search_term WRITE setSearchTerm) + + public: + ResourceModel(ResourceAPI* api); + ~ResourceModel() override; + + [[nodiscard]] auto data(const QModelIndex&, int role) const -> QVariant override; + [[nodiscard]] auto roleNames() const -> QHash<int, QByteArray> override; + bool setData(const QModelIndex& index, const QVariant& value, int role) override; + + [[nodiscard]] virtual auto debugName() const -> QString; + [[nodiscard]] virtual auto metaEntryBase() const -> QString = 0; + + [[nodiscard]] inline int rowCount(const QModelIndex& parent) const override { return parent.isValid() ? 0 : m_packs.size(); } + [[nodiscard]] inline int columnCount(const QModelIndex& parent) const override { return parent.isValid() ? 0 : 1; } + [[nodiscard]] inline auto flags(const QModelIndex& index) const -> Qt::ItemFlags override { return QAbstractListModel::flags(index); } + + [[nodiscard]] bool hasActiveSearchJob() const { return m_current_search_job && m_current_search_job->isRunning(); } + [[nodiscard]] bool hasActiveInfoJob() const { return m_current_info_job.isRunning(); } + [[nodiscard]] Task::Ptr activeSearchJob() { return hasActiveSearchJob() ? m_current_search_job : nullptr; } + + [[nodiscard]] auto getSortingMethods() const { return m_api->getSortingMethods(); } + + public slots: + void fetchMore(const QModelIndex& parent) override; + // NOTE: Can't use [[nodiscard]] here because of https://bugreports.qt.io/browse/QTBUG-58628 on Qt 5.12 + inline bool canFetchMore(const QModelIndex& parent) const override + { + return parent.isValid() ? false : m_search_state == SearchState::CanFetchMore; + } + + void setSearchTerm(QString term) { m_search_term = term; } + + virtual ResourceAPI::SearchArgs createSearchArguments() = 0; + virtual ResourceAPI::SearchCallbacks createSearchCallbacks() { return {}; } + + virtual ResourceAPI::VersionSearchArgs createVersionsArguments(QModelIndex&) = 0; + virtual ResourceAPI::VersionSearchCallbacks createVersionsCallbacks(QModelIndex&) { return {}; } + + virtual ResourceAPI::ProjectInfoArgs createInfoArguments(QModelIndex&) = 0; + virtual ResourceAPI::ProjectInfoCallbacks createInfoCallbacks(QModelIndex&) { return {}; } + + /** Requests the API for more entries. */ + virtual void search(); + + /** Applies any processing / extra requests needed to fully load the specified entry's information. */ + virtual void loadEntry(QModelIndex&); + + /** Schedule a refresh, clearing the current state. */ + void refresh(); + + /** Gets the icon at the URL for the given index. If it's not fetched yet, fetch it and update when fisinhed. */ + std::optional<QIcon> getIcon(QModelIndex&, const QUrl&); + + protected: + /** Resets the model's data. */ + void clearData(); + + void runSearchJob(Task::Ptr); + void runInfoJob(Task::Ptr); + + [[nodiscard]] auto getCurrentSortingMethodByIndex() const -> std::optional<ResourceAPI::SortingMethod>; + + /** Converts a JSON document to a common array format. + * + * This is needed so that different providers, with different JSON structures, can be parsed + * uniformally. You NEED to re-implement this if you intend on using the default callbacks. + */ + [[nodiscard]] virtual auto documentToArray(QJsonDocument&) const -> QJsonArray; + + /** Functions to load data into a pack. + * + * Those are needed for the same reason as documentToArray, and NEED to be re-implemented in the same way. + */ + + virtual void loadIndexedPack(ModPlatform::IndexedPack&, QJsonObject&); + virtual void loadExtraPackInfo(ModPlatform::IndexedPack&, QJsonObject&); + virtual void loadIndexedPackVersions(ModPlatform::IndexedPack&, QJsonArray&); + + protected: + /* Basic search parameters */ + enum class SearchState { None, CanFetchMore, ResetRequested, Finished } m_search_state = SearchState::None; + int m_next_search_offset = 0; + QString m_search_term; + unsigned int m_current_sort_index = 0; + + std::unique_ptr<ResourceAPI> m_api; + + // Job for searching for new entries + shared_qobject_ptr<Task> m_current_search_job; + // Job for fetching versions and extra info on existing entries + ConcurrentTask m_current_info_job; + + shared_qobject_ptr<NetJob> m_current_icon_job; + QSet<QUrl> m_currently_running_icon_actions; + QSet<QUrl> m_failed_icon_actions; + + QList<ModPlatform::IndexedPack::Ptr> m_packs; + + // HACK: We need this to prevent callbacks from calling the model after it has already been deleted. + // This leaks a tiny bit of memory per time the user has opened a resource dialog. How to make this better? + static QHash<ResourceModel*, bool> s_running_models; + + private: + /* Default search request callbacks */ + void searchRequestSucceeded(QJsonDocument&); + void searchRequestFailed(QString reason, int network_error_code); + void searchRequestAborted(); + + void versionRequestSucceeded(QJsonDocument&, ModPlatform::IndexedPack&, const QModelIndex&); + + void infoRequestSucceeded(QJsonDocument&, ModPlatform::IndexedPack&, const QModelIndex&); + + signals: + void versionListUpdated(); + void projectInfoUpdated(); +}; + +} // namespace ResourceDownload diff --git a/launcher/ui/pages/modplatform/ResourcePackModel.cpp b/launcher/ui/pages/modplatform/ResourcePackModel.cpp new file mode 100644 index 00000000..18c14bf8 --- /dev/null +++ b/launcher/ui/pages/modplatform/ResourcePackModel.cpp @@ -0,0 +1,46 @@ +// SPDX-FileCopyrightText: 2023 flowln <flowlnlnln@gmail.com> +// +// SPDX-License-Identifier: GPL-3.0-only + +#include "ResourcePackModel.h" + +#include <QMessageBox> + +namespace ResourceDownload { + +ResourcePackResourceModel::ResourcePackResourceModel(BaseInstance const& base_inst, ResourceAPI* api) + : ResourceModel(api), m_base_instance(base_inst){}; + +/******** Make data requests ********/ + +ResourceAPI::SearchArgs ResourcePackResourceModel::createSearchArguments() +{ + auto sort = getCurrentSortingMethodByIndex(); + return { ModPlatform::ResourceType::RESOURCE_PACK, m_next_search_offset, m_search_term, sort }; +} + +ResourceAPI::VersionSearchArgs ResourcePackResourceModel::createVersionsArguments(QModelIndex& entry) +{ + auto& pack = m_packs[entry.row()]; + return { *pack }; +} + +ResourceAPI::ProjectInfoArgs ResourcePackResourceModel::createInfoArguments(QModelIndex& entry) +{ + auto& pack = m_packs[entry.row()]; + return { *pack }; +} + +void ResourcePackResourceModel::searchWithTerm(const QString& term, unsigned int sort) +{ + if (m_search_term == term && m_search_term.isNull() == term.isNull() && m_current_sort_index == sort) { + return; + } + + setSearchTerm(term); + m_current_sort_index = sort; + + refresh(); +} + +} // namespace ResourceDownload diff --git a/launcher/ui/pages/modplatform/ResourcePackModel.h b/launcher/ui/pages/modplatform/ResourcePackModel.h new file mode 100644 index 00000000..e2b4a195 --- /dev/null +++ b/launcher/ui/pages/modplatform/ResourcePackModel.h @@ -0,0 +1,43 @@ +// SPDX-FileCopyrightText: 2023 flowln <flowlnlnln@gmail.com> +// +// SPDX-License-Identifier: GPL-3.0-only + +#pragma once + +#include <QAbstractListModel> + +#include "BaseInstance.h" + +#include "modplatform/ModIndex.h" + +#include "ui/pages/modplatform/ResourceModel.h" + +class Version; + +namespace ResourceDownload { + +class ResourcePackResourceModel : public ResourceModel { + Q_OBJECT + + public: + ResourcePackResourceModel(BaseInstance const&, ResourceAPI*); + + /* Ask the API for more information */ + void searchWithTerm(const QString& term, unsigned int sort); + + void loadIndexedPack(ModPlatform::IndexedPack&, QJsonObject&) override = 0; + void loadExtraPackInfo(ModPlatform::IndexedPack&, QJsonObject&) override = 0; + void loadIndexedPackVersions(ModPlatform::IndexedPack&, QJsonArray&) override = 0; + + public slots: + ResourceAPI::SearchArgs createSearchArguments() override; + ResourceAPI::VersionSearchArgs createVersionsArguments(QModelIndex&) override; + ResourceAPI::ProjectInfoArgs createInfoArguments(QModelIndex&) override; + + protected: + const BaseInstance& m_base_instance; + + auto documentToArray(QJsonDocument& obj) const -> QJsonArray override = 0; +}; + +} // namespace ResourceDownload diff --git a/launcher/ui/pages/modplatform/ResourcePackPage.cpp b/launcher/ui/pages/modplatform/ResourcePackPage.cpp new file mode 100644 index 00000000..52fb4802 --- /dev/null +++ b/launcher/ui/pages/modplatform/ResourcePackPage.cpp @@ -0,0 +1,46 @@ +// SPDX-FileCopyrightText: 2023 flowln <flowlnlnln@gmail.com> +// +// SPDX-License-Identifier: GPL-3.0-only + +#include "ResourcePackPage.h" +#include "ui_ResourcePage.h" + +#include "ResourcePackModel.h" + +#include "ui/dialogs/ResourceDownloadDialog.h" + +#include <QRegularExpression> + +namespace ResourceDownload { + +ResourcePackResourcePage::ResourcePackResourcePage(ResourceDownloadDialog* dialog, BaseInstance& instance) + : ResourcePage(dialog, instance) +{ + connect(m_ui->searchButton, &QPushButton::clicked, this, &ResourcePackResourcePage::triggerSearch); + connect(m_ui->packView, &QListView::doubleClicked, this, &ResourcePackResourcePage::onResourceSelected); +} + +/******** Callbacks to events in the UI (set up in the derived classes) ********/ + +void ResourcePackResourcePage::triggerSearch() +{ + m_ui->packView->clearSelection(); + m_ui->packDescription->clear(); + m_ui->versionSelectionBox->clear(); + + updateSelectionButton(); + + static_cast<ResourcePackResourceModel*>(m_model)->searchWithTerm(getSearchTerm(), m_ui->sortByBox->currentData().toUInt()); + m_fetch_progress.watch(m_model->activeSearchJob().get()); +} + +QMap<QString, QString> ResourcePackResourcePage::urlHandlers() const +{ + QMap<QString, QString> map; + map.insert(QRegularExpression::anchoredPattern("(?:www\\.)?modrinth\\.com\\/resourcepack\\/([^\\/]+)\\/?"), "modrinth"); + map.insert(QRegularExpression::anchoredPattern("(?:www\\.)?curseforge\\.com\\/minecraft\\/texture-packs\\/([^\\/]+)\\/?"), "curseforge"); + map.insert(QRegularExpression::anchoredPattern("minecraft\\.curseforge\\.com\\/projects\\/([^\\/]+)\\/?"), "curseforge"); + return map; +} + +} // namespace ResourceDownload diff --git a/launcher/ui/pages/modplatform/ResourcePackPage.h b/launcher/ui/pages/modplatform/ResourcePackPage.h new file mode 100644 index 00000000..8c5cf08b --- /dev/null +++ b/launcher/ui/pages/modplatform/ResourcePackPage.h @@ -0,0 +1,50 @@ +// SPDX-FileCopyrightText: 2023 flowln <flowlnlnln@gmail.com> +// +// SPDX-License-Identifier: GPL-3.0-only + +#pragma once + +#include "ui/pages/modplatform/ResourcePage.h" +#include "ui/pages/modplatform/ResourcePackModel.h" + +namespace Ui { +class ResourcePage; +} + +namespace ResourceDownload { + +class ResourcePackDownloadDialog; + +class ResourcePackResourcePage : public ResourcePage { + Q_OBJECT + + public: + template <typename T> + static T* create(ResourcePackDownloadDialog* dialog, BaseInstance& instance) + { + auto page = new T(dialog, instance); + auto model = static_cast<ResourcePackResourceModel*>(page->getModel()); + + connect(model, &ResourceModel::versionListUpdated, page, &ResourcePage::updateVersionList); + connect(model, &ResourceModel::projectInfoUpdated, page, &ResourcePage::updateUi); + + return page; + } + + //: The plural version of 'resource pack' + [[nodiscard]] inline QString resourcesString() const override { return tr("resource packs"); } + //: The singular version of 'resource packs' + [[nodiscard]] inline QString resourceString() const override { return tr("resource pack"); } + + [[nodiscard]] bool supportsFiltering() const override { return false; }; + + [[nodiscard]] QMap<QString, QString> urlHandlers() const override; + + protected: + ResourcePackResourcePage(ResourceDownloadDialog* dialog, BaseInstance& instance); + + protected slots: + void triggerSearch() override; +}; + +} // namespace ResourceDownload diff --git a/launcher/ui/pages/modplatform/ResourcePage.cpp b/launcher/ui/pages/modplatform/ResourcePage.cpp new file mode 100644 index 00000000..f75bb886 --- /dev/null +++ b/launcher/ui/pages/modplatform/ResourcePage.cpp @@ -0,0 +1,413 @@ +// SPDX-FileCopyrightText: 2023 flowln <flowlnlnln@gmail.com> +// +// SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0 +/* + * 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 + * 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 "ResourcePage.h" +#include "ui_ResourcePage.h" + +#include <QDesktopServices> +#include <QKeyEvent> + +#include "Markdown.h" +#include "ResourceDownloadTask.h" + +#include "minecraft/MinecraftInstance.h" + +#include "ui/dialogs/ResourceDownloadDialog.h" +#include "ui/pages/modplatform/ResourceModel.h" +#include "ui/widgets/ProjectItem.h" + +namespace ResourceDownload { + +ResourcePage::ResourcePage(ResourceDownloadDialog* parent, BaseInstance& base_instance) + : QWidget(parent), m_base_instance(base_instance), m_ui(new Ui::ResourcePage), m_parent_dialog(parent), m_fetch_progress(this, false) +{ + m_ui->setupUi(this); + + m_ui->searchEdit->installEventFilter(this); + + m_ui->versionSelectionBox->view()->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); + m_ui->versionSelectionBox->view()->parentWidget()->setMaximumHeight(300); + + m_search_timer.setTimerType(Qt::TimerType::CoarseTimer); + m_search_timer.setSingleShot(true); + + connect(&m_search_timer, &QTimer::timeout, this, &ResourcePage::triggerSearch); + + m_fetch_progress.hideIfInactive(true); + m_fetch_progress.setFixedHeight(24); + m_fetch_progress.progressFormat(""); + + m_ui->gridLayout_3->addWidget(&m_fetch_progress, 0, 0, 1, m_ui->gridLayout_3->columnCount()); + + m_ui->packView->setItemDelegate(new ProjectItemDelegate(this)); + m_ui->packView->installEventFilter(this); + + connect(m_ui->packDescription, &QTextBrowser::anchorClicked, this, &ResourcePage::openUrl); +} + +ResourcePage::~ResourcePage() +{ + delete m_ui; + if (m_model) + delete m_model; +} + +void ResourcePage::retranslate() +{ + m_ui->retranslateUi(this); +} + +void ResourcePage::openedImpl() +{ + if (!supportsFiltering()) + m_ui->resourceFilterButton->setVisible(false); + + //: String in the search bar of the mod downloading dialog + m_ui->searchEdit->setPlaceholderText(tr("Search for %1...").arg(resourcesString())); + m_ui->resourceSelectionButton->setText(tr("Select %1 for download").arg(resourceString())); + + updateSelectionButton(); + triggerSearch(); +} + +auto ResourcePage::eventFilter(QObject* watched, QEvent* event) -> bool +{ + if (event->type() == QEvent::KeyPress) { + auto* keyEvent = static_cast<QKeyEvent*>(event); + if (watched == m_ui->searchEdit) { + if (keyEvent->key() == Qt::Key_Return) { + triggerSearch(); + keyEvent->accept(); + return true; + } else { + if (m_search_timer.isActive()) + m_search_timer.stop(); + + m_search_timer.start(350); + } + } else if (watched == m_ui->packView) { + if (keyEvent->key() == Qt::Key_Return) { + onResourceSelected(); + + // To have the 'select mod' button outlined instead of the 'review and confirm' one + m_ui->resourceSelectionButton->setFocus(Qt::FocusReason::ShortcutFocusReason); + m_ui->packView->setFocus(Qt::FocusReason::NoFocusReason); + + keyEvent->accept(); + return true; + } + } + } + + return QWidget::eventFilter(watched, event); +} + +QString ResourcePage::getSearchTerm() const +{ + return m_ui->searchEdit->text(); +} + +void ResourcePage::setSearchTerm(QString term) +{ + m_ui->searchEdit->setText(term); +} + +void ResourcePage::addSortings() +{ + Q_ASSERT(m_model); + + auto sorts = m_model->getSortingMethods(); + std::sort(sorts.begin(), sorts.end(), [](auto const& l, auto const& r) { return l.index < r.index; }); + + for (auto&& sorting : sorts) + m_ui->sortByBox->addItem(sorting.readable_name, QVariant(sorting.index)); +} + +bool ResourcePage::setCurrentPack(ModPlatform::IndexedPack pack) +{ + QVariant v; + v.setValue(pack); + return m_model->setData(m_ui->packView->currentIndex(), v, Qt::UserRole); +} + +ModPlatform::IndexedPack ResourcePage::getCurrentPack() const +{ + return m_model->data(m_ui->packView->currentIndex(), Qt::UserRole).value<ModPlatform::IndexedPack>(); +} + +void ResourcePage::updateUi() +{ + auto current_pack = getCurrentPack(); + + QString text = ""; + QString name = current_pack.name; + + if (current_pack.websiteUrl.isEmpty()) + text = name; + else + text = "<a href=\"" + current_pack.websiteUrl + "\">" + name + "</a>"; + + if (!current_pack.authors.empty()) { + auto authorToStr = [](ModPlatform::ModpackAuthor& author) -> QString { + if (author.url.isEmpty()) { + return author.name; + } + return QString("<a href=\"%1\">%2</a>").arg(author.url, author.name); + }; + QStringList authorStrs; + for (auto& author : current_pack.authors) { + authorStrs.push_back(authorToStr(author)); + } + text += "<br>" + tr(" by ") + authorStrs.join(", "); + } + + if (current_pack.extraDataLoaded) { + if (!current_pack.extraData.donate.isEmpty()) { + text += "<br><br>" + tr("Donate information: "); + auto donateToStr = [](ModPlatform::DonationData& donate) -> QString { + return QString("<a href=\"%1\">%2</a>").arg(donate.url, donate.platform); + }; + QStringList donates; + for (auto& donate : current_pack.extraData.donate) { + donates.append(donateToStr(donate)); + } + text += donates.join(", "); + } + + if (!current_pack.extraData.issuesUrl.isEmpty() || !current_pack.extraData.sourceUrl.isEmpty() || + !current_pack.extraData.wikiUrl.isEmpty() || !current_pack.extraData.discordUrl.isEmpty()) { + text += "<br><br>" + tr("External links:") + "<br>"; + } + + if (!current_pack.extraData.issuesUrl.isEmpty()) + text += "- " + tr("Issues: <a href=%1>%1</a>").arg(current_pack.extraData.issuesUrl) + "<br>"; + if (!current_pack.extraData.wikiUrl.isEmpty()) + text += "- " + tr("Wiki: <a href=%1>%1</a>").arg(current_pack.extraData.wikiUrl) + "<br>"; + if (!current_pack.extraData.sourceUrl.isEmpty()) + text += "- " + tr("Source code: <a href=%1>%1</a>").arg(current_pack.extraData.sourceUrl) + "<br>"; + if (!current_pack.extraData.discordUrl.isEmpty()) + text += "- " + tr("Discord: <a href=%1>%1</a>").arg(current_pack.extraData.discordUrl) + "<br>"; + } + + text += "<hr>"; + + m_ui->packDescription->setHtml( + text + (current_pack.extraData.body.isEmpty() ? current_pack.description : markdownToHTML(current_pack.extraData.body))); + m_ui->packDescription->flush(); +} + +void ResourcePage::updateSelectionButton() +{ + if (!isOpened || m_selected_version_index < 0) { + m_ui->resourceSelectionButton->setEnabled(false); + return; + } + + m_ui->resourceSelectionButton->setEnabled(true); + if (!getCurrentPack().isVersionSelected(m_selected_version_index)) { + m_ui->resourceSelectionButton->setText(tr("Select %1 for download").arg(resourceString())); + } else { + m_ui->resourceSelectionButton->setText(tr("Deselect %1 for download").arg(resourceString())); + } +} + +void ResourcePage::updateVersionList() +{ + auto current_pack = getCurrentPack(); + + m_ui->versionSelectionBox->blockSignals(true); + m_ui->versionSelectionBox->clear(); + m_ui->versionSelectionBox->blockSignals(false); + + for (int i = 0; i < current_pack.versions.size(); i++) { + auto& version = current_pack.versions[i]; + if (optedOut(version)) + continue; + + m_ui->versionSelectionBox->addItem(current_pack.versions[i].version, QVariant(i)); + } + + if (m_ui->versionSelectionBox->count() == 0) { + m_ui->versionSelectionBox->addItem(tr("No valid version found."), QVariant(-1)); + m_ui->resourceSelectionButton->setText(tr("Cannot select invalid version :(")); + } + + updateSelectionButton(); +} + +void ResourcePage::onSelectionChanged(QModelIndex curr, QModelIndex prev) +{ + if (!curr.isValid()) { + return; + } + + auto current_pack = getCurrentPack(); + + bool request_load = false; + if (!current_pack.versionsLoaded) { + m_ui->resourceSelectionButton->setText(tr("Loading versions...")); + m_ui->resourceSelectionButton->setEnabled(false); + + request_load = true; + } else { + updateVersionList(); + } + + if (!current_pack.extraDataLoaded) + request_load = true; + + if (request_load) + m_model->loadEntry(curr); + + updateUi(); +} + +void ResourcePage::onVersionSelectionChanged(QString data) +{ + if (data.isNull() || data.isEmpty()) { + m_selected_version_index = -1; + return; + } + + m_selected_version_index = m_ui->versionSelectionBox->currentData().toInt(); + updateSelectionButton(); +} + +void ResourcePage::addResourceToDialog(ModPlatform::IndexedPack& pack, ModPlatform::IndexedVersion& version) +{ + m_parent_dialog->addResource(pack, version); +} + +void ResourcePage::removeResourceFromDialog(ModPlatform::IndexedPack& pack, ModPlatform::IndexedVersion& version) +{ + m_parent_dialog->removeResource(pack, version); +} + +void ResourcePage::onResourceSelected() +{ + if (m_selected_version_index < 0) + return; + + auto current_pack = getCurrentPack(); + if (!current_pack.versionsLoaded) + return; + + auto& version = current_pack.versions[m_selected_version_index]; + if (version.is_currently_selected) + removeResourceFromDialog(current_pack, version); + else + addResourceToDialog(current_pack, version); + + // Save the modified pack (and prevent warning in release build) + [[maybe_unused]] bool set = setCurrentPack(current_pack); + Q_ASSERT(set); + + updateSelectionButton(); + + /* Force redraw on the resource list when the selection changes */ + m_ui->packView->adjustSize(); +} + +void ResourcePage::openUrl(const QUrl& url) +{ + // do not allow other url schemes for security reasons + if (!(url.scheme() == "http" || url.scheme() == "https")) { + qWarning() << "Unsupported scheme" << url.scheme(); + return; + } + + // detect URLs and search instead + + const QString address = url.host() + url.path(); + QRegularExpressionMatch match; + QString page; + + auto handlers = urlHandlers(); + for (auto it = handlers.constKeyValueBegin(); it != handlers.constKeyValueEnd(); it++) { + auto&& [regex, candidate] = *it; + if (match = QRegularExpression(regex).match(address); match.hasMatch()) { + page = candidate; + break; + } + } + + if (!page.isNull()) { + const QString slug = match.captured(1); + + // ensure the user isn't opening the same mod + if (slug != getCurrentPack().slug) { + m_parent_dialog->selectPage(page); + + auto newPage = m_parent_dialog->getSelectedPage(); + + QLineEdit* searchEdit = newPage->m_ui->searchEdit; + auto model = newPage->m_model; + QListView* view = newPage->m_ui->packView; + + auto jump = [url, slug, model, view] { + for (int row = 0; row < model->rowCount({}); row++) { + const QModelIndex index = model->index(row); + const auto pack = model->data(index, Qt::UserRole).value<ModPlatform::IndexedPack>(); + + if (pack.slug == slug) { + view->setCurrentIndex(index); + return; + } + } + + // The final fallback. + QDesktopServices::openUrl(url); + }; + + searchEdit->setText(slug); + newPage->triggerSearch(); + + if (model->hasActiveSearchJob()) + connect(model->activeSearchJob().get(), &Task::finished, jump); + else + jump(); + + return; + } + } + + // open in the user's web browser + QDesktopServices::openUrl(url); +} + +} // namespace ResourceDownload diff --git a/launcher/ui/pages/modplatform/ResourcePage.h b/launcher/ui/pages/modplatform/ResourcePage.h new file mode 100644 index 00000000..1896d53e --- /dev/null +++ b/launcher/ui/pages/modplatform/ResourcePage.h @@ -0,0 +1,111 @@ +// SPDX-FileCopyrightText: 2023 flowln <flowlnlnln@gmail.com> +// +// SPDX-License-Identifier: GPL-3.0-only + +#pragma once + +#include <QTimer> +#include <QWidget> + +#include "modplatform/ModIndex.h" +#include "modplatform/ResourceAPI.h" + +#include "ui/pages/BasePage.h" +#include "ui/widgets/ProgressWidget.h" + +namespace Ui { +class ResourcePage; +} + +class BaseInstance; + +namespace ResourceDownload { + +class ResourceDownloadDialog; +class ResourceModel; + +class ResourcePage : public QWidget, public BasePage { + Q_OBJECT + public: + ~ResourcePage() override; + + /* Affects what the user sees */ + [[nodiscard]] auto displayName() const -> QString override = 0; + [[nodiscard]] auto icon() const -> QIcon override = 0; + [[nodiscard]] auto id() const -> QString override = 0; + [[nodiscard]] auto helpPage() const -> QString override = 0; + [[nodiscard]] bool shouldDisplay() const override = 0; + + /* Used internally */ + [[nodiscard]] virtual auto metaEntryBase() const -> QString = 0; + [[nodiscard]] virtual auto debugName() const -> QString = 0; + + //: The plural version of 'resource' + [[nodiscard]] virtual inline QString resourcesString() const { return tr("resources"); } + //: The singular version of 'resources' + [[nodiscard]] virtual inline QString resourceString() const { return tr("resource"); } + + /* Features this resource's page supports */ + [[nodiscard]] virtual bool supportsFiltering() const = 0; + + void retranslate() override; + void openedImpl() override; + auto eventFilter(QObject* watched, QEvent* event) -> bool override; + + /** Get the current term in the search bar. */ + [[nodiscard]] auto getSearchTerm() const -> QString; + /** Programatically set the term in the search bar. */ + void setSearchTerm(QString); + + [[nodiscard]] bool setCurrentPack(ModPlatform::IndexedPack); + [[nodiscard]] auto getCurrentPack() const -> ModPlatform::IndexedPack; + [[nodiscard]] auto getDialog() const -> const ResourceDownloadDialog* { return m_parent_dialog; } + [[nodiscard]] auto getModel() const -> ResourceModel* { return m_model; } + + protected: + ResourcePage(ResourceDownloadDialog* parent, BaseInstance&); + + void addSortings(); + + public slots: + virtual void updateUi(); + virtual void updateSelectionButton(); + virtual void updateVersionList(); + + virtual void addResourceToDialog(ModPlatform::IndexedPack&, ModPlatform::IndexedVersion&); + virtual void removeResourceFromDialog(ModPlatform::IndexedPack&, ModPlatform::IndexedVersion&); + + protected slots: + virtual void triggerSearch() {} + + void onSelectionChanged(QModelIndex first, QModelIndex second); + void onVersionSelectionChanged(QString data); + void onResourceSelected(); + + // NOTE: Can't use [[nodiscard]] here because of https://bugreports.qt.io/browse/QTBUG-58628 on Qt 5.12 + + /** Associates regex expressions to pages in the order they're given in the map. */ + virtual QMap<QString, QString> urlHandlers() const = 0; + virtual void openUrl(const QUrl&); + + /** Whether the version is opted out or not. Currently only makes sense in CF. */ + virtual bool optedOut(ModPlatform::IndexedVersion& ver) const { return false; }; + + public: + BaseInstance& m_base_instance; + + protected: + Ui::ResourcePage* m_ui; + + ResourceDownloadDialog* m_parent_dialog = nullptr; + ResourceModel* m_model = nullptr; + + int m_selected_version_index = -1; + + ProgressWidget m_fetch_progress; + + // Used to do instant searching with a delay to cache quick changes + QTimer m_search_timer; +}; + +} // namespace ResourceDownload diff --git a/launcher/ui/pages/modplatform/ModPage.ui b/launcher/ui/pages/modplatform/ResourcePage.ui index 94365aa5..73a9d3b1 100644 --- a/launcher/ui/pages/modplatform/ModPage.ui +++ b/launcher/ui/pages/modplatform/ResourcePage.ui @@ -1,7 +1,7 @@ <?xml version="1.0" encoding="UTF-8"?> <ui version="4.0"> - <class>ModPage</class> - <widget class="QWidget" name="ModPage"> + <class>ResourcePage</class> + <widget class="QWidget" name="ResourcePage"> <property name="geometry"> <rect> <x>0</x> @@ -49,11 +49,7 @@ </widget> </item> <item row="0" column="0"> - <widget class="QLineEdit" name="searchEdit"> - <property name="placeholderText"> - <string>Search for mods...</string> - </property> - </widget> + <widget class="QLineEdit" name="searchEdit"/> </item> <item row="2" column="0" colspan="4"> <layout class="QGridLayout" name="gridLayout_4" columnstretch="0,0,0"> @@ -74,16 +70,12 @@ <widget class="QComboBox" name="sortByBox"/> </item> <item row="1" column="2"> - <widget class="QPushButton" name="modSelectionButton"> - <property name="text"> - <string>Select mod for download</string> - </property> - </widget> + <widget class="QPushButton" name="resourceSelectionButton"/> </item> </layout> </item> <item row="0" column="1"> - <widget class="QPushButton" name="modFilterButton"> + <widget class="QPushButton" name="resourceFilterButton"> <property name="text"> <string>Filter options</string> </property> diff --git a/launcher/ui/pages/modplatform/ShaderPackModel.cpp b/launcher/ui/pages/modplatform/ShaderPackModel.cpp new file mode 100644 index 00000000..aabd3be6 --- /dev/null +++ b/launcher/ui/pages/modplatform/ShaderPackModel.cpp @@ -0,0 +1,46 @@ +// SPDX-FileCopyrightText: 2023 flowln <flowlnlnln@gmail.com> +// +// SPDX-License-Identifier: GPL-3.0-only + +#include "ShaderPackModel.h" + +#include <QMessageBox> + +namespace ResourceDownload { + +ShaderPackResourceModel::ShaderPackResourceModel(BaseInstance const& base_inst, ResourceAPI* api) + : ResourceModel(api), m_base_instance(base_inst){}; + +/******** Make data requests ********/ + +ResourceAPI::SearchArgs ShaderPackResourceModel::createSearchArguments() +{ + auto sort = getCurrentSortingMethodByIndex(); + return { ModPlatform::ResourceType::SHADER_PACK, m_next_search_offset, m_search_term, sort }; +} + +ResourceAPI::VersionSearchArgs ShaderPackResourceModel::createVersionsArguments(QModelIndex& entry) +{ + auto& pack = m_packs[entry.row()]; + return { *pack }; +} + +ResourceAPI::ProjectInfoArgs ShaderPackResourceModel::createInfoArguments(QModelIndex& entry) +{ + auto& pack = m_packs[entry.row()]; + return { *pack }; +} + +void ShaderPackResourceModel::searchWithTerm(const QString& term, unsigned int sort) +{ + if (m_search_term == term && m_search_term.isNull() == term.isNull() && m_current_sort_index == sort) { + return; + } + + setSearchTerm(term); + m_current_sort_index = sort; + + refresh(); +} + +} // namespace ResourceDownload diff --git a/launcher/ui/pages/modplatform/ShaderPackModel.h b/launcher/ui/pages/modplatform/ShaderPackModel.h new file mode 100644 index 00000000..f3c695e9 --- /dev/null +++ b/launcher/ui/pages/modplatform/ShaderPackModel.h @@ -0,0 +1,43 @@ +// SPDX-FileCopyrightText: 2023 flowln <flowlnlnln@gmail.com> +// +// SPDX-License-Identifier: GPL-3.0-only + +#pragma once + +#include <QAbstractListModel> + +#include "BaseInstance.h" + +#include "modplatform/ModIndex.h" + +#include "ui/pages/modplatform/ResourceModel.h" + +class Version; + +namespace ResourceDownload { + +class ShaderPackResourceModel : public ResourceModel { + Q_OBJECT + + public: + ShaderPackResourceModel(BaseInstance const&, ResourceAPI*); + + /* Ask the API for more information */ + void searchWithTerm(const QString& term, unsigned int sort); + + void loadIndexedPack(ModPlatform::IndexedPack&, QJsonObject&) override = 0; + void loadExtraPackInfo(ModPlatform::IndexedPack&, QJsonObject&) override = 0; + void loadIndexedPackVersions(ModPlatform::IndexedPack&, QJsonArray&) override = 0; + + public slots: + ResourceAPI::SearchArgs createSearchArguments() override; + ResourceAPI::VersionSearchArgs createVersionsArguments(QModelIndex&) override; + ResourceAPI::ProjectInfoArgs createInfoArguments(QModelIndex&) override; + + protected: + const BaseInstance& m_base_instance; + + auto documentToArray(QJsonDocument& obj) const -> QJsonArray override = 0; +}; + +} // namespace ResourceDownload diff --git a/launcher/ui/pages/modplatform/ShaderPackPage.cpp b/launcher/ui/pages/modplatform/ShaderPackPage.cpp new file mode 100644 index 00000000..251c07e7 --- /dev/null +++ b/launcher/ui/pages/modplatform/ShaderPackPage.cpp @@ -0,0 +1,54 @@ +// SPDX-FileCopyrightText: 2023 flowln <flowlnlnln@gmail.com> +// +// SPDX-License-Identifier: GPL-3.0-only + +#include "ShaderPackPage.h" +#include "ui_ResourcePage.h" + +#include "ShaderPackModel.h" + +#include "ui/dialogs/ResourceDownloadDialog.h" + +#include <QRegularExpression> + +namespace ResourceDownload { + +ShaderPackResourcePage::ShaderPackResourcePage(ShaderPackDownloadDialog* dialog, BaseInstance& instance) + : ResourcePage(dialog, instance) +{ + connect(m_ui->searchButton, &QPushButton::clicked, this, &ShaderPackResourcePage::triggerSearch); + connect(m_ui->packView, &QListView::doubleClicked, this, &ShaderPackResourcePage::onResourceSelected); +} + +/******** Callbacks to events in the UI (set up in the derived classes) ********/ + +void ShaderPackResourcePage::triggerSearch() +{ + m_ui->packView->clearSelection(); + m_ui->packDescription->clear(); + m_ui->versionSelectionBox->clear(); + + updateSelectionButton(); + + static_cast<ShaderPackResourceModel*>(m_model)->searchWithTerm(getSearchTerm(), m_ui->sortByBox->currentData().toUInt()); + m_fetch_progress.watch(m_model->activeSearchJob().get()); +} + +QMap<QString, QString> ShaderPackResourcePage::urlHandlers() const +{ + QMap<QString, QString> map; + map.insert(QRegularExpression::anchoredPattern("(?:www\\.)?modrinth\\.com\\/shaders\\/([^\\/]+)\\/?"), "modrinth"); + map.insert(QRegularExpression::anchoredPattern("(?:www\\.)?curseforge\\.com\\/minecraft\\/customization\\/([^\\/]+)\\/?"), "curseforge"); + map.insert(QRegularExpression::anchoredPattern("minecraft\\.curseforge\\.com\\/projects\\/([^\\/]+)\\/?"), "curseforge"); + return map; +} + +void ShaderPackResourcePage::addResourceToDialog(ModPlatform::IndexedPack& pack, ModPlatform::IndexedVersion& version) +{ + if (version.loaders.contains(QStringLiteral("canvas"))) + version.custom_target_folder = QStringLiteral("resourcepacks"); + + m_parent_dialog->addResource(pack, version); +} + +} // namespace ResourceDownload diff --git a/launcher/ui/pages/modplatform/ShaderPackPage.h b/launcher/ui/pages/modplatform/ShaderPackPage.h new file mode 100644 index 00000000..9039c4d9 --- /dev/null +++ b/launcher/ui/pages/modplatform/ShaderPackPage.h @@ -0,0 +1,52 @@ +// SPDX-FileCopyrightText: 2023 flowln <flowlnlnln@gmail.com> +// +// SPDX-License-Identifier: GPL-3.0-only + +#pragma once + +#include "ui/pages/modplatform/ResourcePage.h" +#include "ui/pages/modplatform/ShaderPackModel.h" + +namespace Ui { +class ResourcePage; +} + +namespace ResourceDownload { + +class ShaderPackDownloadDialog; + +class ShaderPackResourcePage : public ResourcePage { + Q_OBJECT + + public: + template <typename T> + static T* create(ShaderPackDownloadDialog* dialog, BaseInstance& instance) + { + auto page = new T(dialog, instance); + auto model = static_cast<ShaderPackResourceModel*>(page->getModel()); + + connect(model, &ResourceModel::versionListUpdated, page, &ResourcePage::updateVersionList); + connect(model, &ResourceModel::projectInfoUpdated, page, &ResourcePage::updateUi); + + return page; + } + + //: The plural version of 'shader pack' + [[nodiscard]] inline QString resourcesString() const override { return tr("shader packs"); } + //: The singular version of 'shader packs' + [[nodiscard]] inline QString resourceString() const override { return tr("shader pack"); } + + [[nodiscard]] bool supportsFiltering() const override { return false; }; + + void addResourceToDialog(ModPlatform::IndexedPack&, ModPlatform::IndexedVersion&) override; + + [[nodiscard]] QMap<QString, QString> urlHandlers() const override; + + protected: + ShaderPackResourcePage(ShaderPackDownloadDialog* dialog, BaseInstance& instance); + + protected slots: + void triggerSearch() override; +}; + +} // namespace ResourceDownload diff --git a/launcher/ui/pages/modplatform/TexturePackModel.cpp b/launcher/ui/pages/modplatform/TexturePackModel.cpp new file mode 100644 index 00000000..fa636951 --- /dev/null +++ b/launcher/ui/pages/modplatform/TexturePackModel.cpp @@ -0,0 +1,84 @@ +// SPDX-FileCopyrightText: 2023 flowln <flowlnlnln@gmail.com> +// +// SPDX-License-Identifier: GPL-3.0-only + +#include "TexturePackModel.h" + +#include "Application.h" + +#include "meta/Index.h" +#include "meta/Version.h" + +static std::list<Version> s_availableVersions = {}; + +namespace ResourceDownload { +TexturePackResourceModel::TexturePackResourceModel(BaseInstance const& inst, ResourceAPI* api) + : ResourcePackResourceModel(inst, api), m_version_list(APPLICATION->metadataIndex()->get("net.minecraft")) +{ + if (!m_version_list->isLoaded()) { + qDebug() << "Loading version list..."; + auto task = m_version_list->getLoadTask(); + if (!task->isRunning()) + task->start(); + } +} + +void waitOnVersionListLoad(Meta::VersionList::Ptr version_list) +{ + QEventLoop load_version_list_loop; + + QTimer time_limit_for_list_load; + time_limit_for_list_load.setTimerType(Qt::TimerType::CoarseTimer); + time_limit_for_list_load.setSingleShot(true); + time_limit_for_list_load.callOnTimeout(&load_version_list_loop, &QEventLoop::quit); + time_limit_for_list_load.start(4000); + + auto task = version_list->getLoadTask(); + QObject::connect(task.get(), &Task::finished, &load_version_list_loop, &QEventLoop::quit); + + load_version_list_loop.exec(); + if (time_limit_for_list_load.isActive()) + time_limit_for_list_load.stop(); +} + +ResourceAPI::SearchArgs TexturePackResourceModel::createSearchArguments() +{ + if (s_availableVersions.empty()) + waitOnVersionListLoad(m_version_list); + + auto args = ResourcePackResourceModel::createSearchArguments(); + + if (!m_version_list->isLoaded()) { + qCritical() << "The version list could not be loaded. Falling back to showing all entries."; + return args; + } + + if (s_availableVersions.empty()) { + for (auto&& version : m_version_list->versions()) { + // FIXME: This duplicates the logic in meta for the 'texturepacks' trait. However, we don't have access to that + // information from the index file alone. Also, downloading every version's file isn't a very good idea. + if (auto ver = version->toComparableVersion(); ver <= maximumTexturePackVersion()) + s_availableVersions.push_back(ver); + } + } + + Q_ASSERT(!s_availableVersions.empty()); + + args.versions = s_availableVersions; + + return args; +} + +ResourceAPI::VersionSearchArgs TexturePackResourceModel::createVersionsArguments(QModelIndex& entry) +{ + auto args = ResourcePackResourceModel::createVersionsArguments(entry); + if (!m_version_list->isLoaded()) { + qCritical() << "The version list could not be loaded. Falling back to showing all entries."; + return args; + } + + args.mcVersions = s_availableVersions; + return args; +} + +} // namespace ResourceDownload diff --git a/launcher/ui/pages/modplatform/TexturePackModel.h b/launcher/ui/pages/modplatform/TexturePackModel.h new file mode 100644 index 00000000..bb2db5cf --- /dev/null +++ b/launcher/ui/pages/modplatform/TexturePackModel.h @@ -0,0 +1,27 @@ +// SPDX-FileCopyrightText: 2023 flowln <flowlnlnln@gmail.com> +// +// SPDX-License-Identifier: GPL-3.0-only + +#pragma once + +#include "meta/VersionList.h" +#include "ui/pages/modplatform/ResourcePackModel.h" + +namespace ResourceDownload { + +class TexturePackResourceModel : public ResourcePackResourceModel { + Q_OBJECT + + public: + TexturePackResourceModel(BaseInstance const& inst, ResourceAPI* api); + + [[nodiscard]] inline ::Version maximumTexturePackVersion() const { return { "1.6" }; } + + ResourceAPI::SearchArgs createSearchArguments() override; + ResourceAPI::VersionSearchArgs createVersionsArguments(QModelIndex&) override; + + protected: + Meta::VersionList::Ptr m_version_list; +}; + +} // namespace ResourceDownload diff --git a/launcher/ui/pages/modplatform/TexturePackPage.h b/launcher/ui/pages/modplatform/TexturePackPage.h new file mode 100644 index 00000000..0bdce2f9 --- /dev/null +++ b/launcher/ui/pages/modplatform/TexturePackPage.h @@ -0,0 +1,50 @@ +// SPDX-FileCopyrightText: 2023 flowln <flowlnlnln@gmail.com> +// +// SPDX-License-Identifier: GPL-3.0-only + +#pragma once + +#include "ui_ResourcePage.h" +#include "ui/dialogs/ResourceDownloadDialog.h" +#include "ui/pages/modplatform/ResourcePackPage.h" +#include "ui/pages/modplatform/TexturePackModel.h" + +namespace Ui { +class ResourcePage; +} + +namespace ResourceDownload { + +class TexturePackDownloadDialog; + +class TexturePackResourcePage : public ResourcePackResourcePage { + Q_OBJECT + + public: + template <typename T> + static T* create(TexturePackDownloadDialog* dialog, BaseInstance& instance) + { + auto page = new T(dialog, instance); + auto model = static_cast<TexturePackResourceModel*>(page->getModel()); + + connect(model, &ResourceModel::versionListUpdated, page, &ResourcePage::updateVersionList); + connect(model, &ResourceModel::projectInfoUpdated, page, &ResourcePage::updateUi); + + return page; + } + + //: The plural version of 'texture pack' + [[nodiscard]] inline QString resourcesString() const override { return tr("texture packs"); } + //: The singular version of 'texture packs' + [[nodiscard]] inline QString resourceString() const override { return tr("texture pack"); } + + protected: + TexturePackResourcePage(TexturePackDownloadDialog* dialog, BaseInstance& instance) + : ResourcePackResourcePage(dialog, instance) + { + connect(m_ui->searchButton, &QPushButton::clicked, this, &TexturePackResourcePage::triggerSearch); + connect(m_ui->packView, &QListView::doubleClicked, this, &TexturePackResourcePage::onResourceSelected); + } +}; + +} // namespace ResourceDownload diff --git a/launcher/ui/pages/modplatform/atlauncher/AtlListModel.cpp b/launcher/ui/pages/modplatform/atlauncher/AtlListModel.cpp index 2ce04068..9ad26f47 100644 --- a/launcher/ui/pages/modplatform/atlauncher/AtlListModel.cpp +++ b/launcher/ui/pages/modplatform/atlauncher/AtlListModel.cpp @@ -86,14 +86,14 @@ void ListModel::request() modpacks.clear(); endResetModel(); - auto *netJob = new NetJob("Atl::Request", APPLICATION->network()); + auto netJob = makeShared<NetJob>("Atl::Request", APPLICATION->network()); auto url = QString(BuildConfig.ATL_DOWNLOAD_SERVER_URL + "launcher/json/packsnew.json"); netJob->addNetAction(Net::Download::makeByteArray(QUrl(url), &response)); jobPtr = netJob; jobPtr->start(); - QObject::connect(netJob, &NetJob::succeeded, this, &ListModel::requestFinished); - QObject::connect(netJob, &NetJob::failed, this, &ListModel::requestFailed); + QObject::connect(netJob.get(), &NetJob::succeeded, this, &ListModel::requestFinished); + QObject::connect(netJob.get(), &NetJob::failed, this, &ListModel::requestFailed); } void ListModel::requestFinished() diff --git a/launcher/ui/pages/modplatform/flame/FlameModModel.cpp b/launcher/ui/pages/modplatform/flame/FlameModModel.cpp deleted file mode 100644 index bc2c686c..00000000 --- a/launcher/ui/pages/modplatform/flame/FlameModModel.cpp +++ /dev/null @@ -1,31 +0,0 @@ -#include "FlameModModel.h" -#include "Json.h" -#include "modplatform/flame/FlameModIndex.h" - -namespace FlameMod { - -// NOLINTNEXTLINE(modernize-avoid-c-arrays) -const char* ListModel::sorts[6]{ "Featured", "Popularity", "LastUpdated", "Name", "Author", "TotalDownloads" }; - -void ListModel::loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) -{ - FlameMod::loadIndexedPack(m, obj); -} - -// We already deal with the URLs when initializing the pack, due to the API response's structure -void ListModel::loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj) -{ - FlameMod::loadBody(m, obj); -} - -void ListModel::loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) -{ - FlameMod::loadIndexedPackVersions(m, arr, APPLICATION->network(), m_parent->m_instance); -} - -auto ListModel::documentToArray(QJsonDocument& obj) const -> QJsonArray -{ - return Json::ensureArray(obj.object(), "data"); -} - -} // namespace FlameMod diff --git a/launcher/ui/pages/modplatform/flame/FlameModModel.h b/launcher/ui/pages/modplatform/flame/FlameModModel.h deleted file mode 100644 index 6a6aef2e..00000000 --- a/launcher/ui/pages/modplatform/flame/FlameModModel.h +++ /dev/null @@ -1,26 +0,0 @@ -#pragma once - -#include "FlameModPage.h" - -namespace FlameMod { - -class ListModel : public ModPlatform::ListModel { - Q_OBJECT - - public: - ListModel(FlameModPage* parent) : ModPlatform::ListModel(parent) {} - ~ListModel() override = default; - - private: - void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) override; - void loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj) override; - void loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) override; - - auto documentToArray(QJsonDocument& obj) const -> QJsonArray override; - - // NOLINTNEXTLINE(modernize-avoid-c-arrays) - static const char* sorts[6]; - inline auto getSorts() const -> const char** override { return sorts; }; -}; - -} // namespace FlameMod diff --git a/launcher/ui/pages/modplatform/flame/FlameModPage.cpp b/launcher/ui/pages/modplatform/flame/FlameModPage.cpp deleted file mode 100644 index bad78c97..00000000 --- a/launcher/ui/pages/modplatform/flame/FlameModPage.cpp +++ /dev/null @@ -1,97 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-only -/* - * 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 - * 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 "FlameModPage.h" -#include "ui_ModPage.h" - -#include "FlameModModel.h" -#include "ui/dialogs/ModDownloadDialog.h" - -FlameModPage::FlameModPage(ModDownloadDialog* dialog, BaseInstance* instance) - : ModPage(dialog, instance, new FlameAPI()) -{ - listModel = new FlameMod::ListModel(this); - ui->packView->setModel(listModel); - - // index is used to set the sorting with the flame api - ui->sortByBox->addItem(tr("Sort by Featured")); - ui->sortByBox->addItem(tr("Sort by Popularity")); - ui->sortByBox->addItem(tr("Sort by Last Updated")); - ui->sortByBox->addItem(tr("Sort by Name")); - ui->sortByBox->addItem(tr("Sort by Author")); - ui->sortByBox->addItem(tr("Sort by Downloads")); - - // sometimes Qt just ignores virtual slots and doesn't work as intended it seems, - // so it's best not to connect them in the parent's contructor... - connect(ui->sortByBox, SIGNAL(currentIndexChanged(int)), this, SLOT(triggerSearch())); - connect(ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &FlameModPage::onSelectionChanged); - connect(ui->versionSelectionBox, &QComboBox::currentTextChanged, this, &FlameModPage::onVersionSelectionChanged); - connect(ui->modSelectionButton, &QPushButton::clicked, this, &FlameModPage::onModSelected); - - ui->packDescription->setMetaEntry(metaEntryBase()); -} - -auto FlameModPage::validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, ModAPI::ModLoaderTypes loaders) const -> bool -{ - Q_UNUSED(loaders); - return ver.mcVersion.contains(mineVer) && !ver.downloadUrl.isEmpty(); -} - -bool FlameModPage::optedOut(ModPlatform::IndexedVersion& ver) const -{ - return ver.downloadUrl.isEmpty(); -} - -// I don't know why, but doing this on the parent class makes it so that -// other mod providers start loading before being selected, at least with -// my Qt, so we need to implement this in every derived class... -auto FlameModPage::shouldDisplay() const -> bool { return true; } - -void FlameModPage::openUrl(const QUrl& url) -{ - if (url.scheme().isEmpty()) { - QString query = url.query(QUrl::FullyDecoded); - - if (query.startsWith("remoteUrl=")) { - // attempt to resolve url from warning page - query.remove(0, 10); - ModPage::openUrl({QUrl::fromPercentEncoding(query.toUtf8())}); // double decoding is necessary - return; - } - } - - ModPage::openUrl(url); -} diff --git a/launcher/ui/pages/modplatform/flame/FlameModPage.h b/launcher/ui/pages/modplatform/flame/FlameModPage.h deleted file mode 100644 index 58479ab9..00000000 --- a/launcher/ui/pages/modplatform/flame/FlameModPage.h +++ /dev/null @@ -1,70 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-only -/* - * 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 - * 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 "modplatform/ModAPI.h" -#include "ui/pages/modplatform/ModPage.h" - -#include "modplatform/flame/FlameAPI.h" - -class FlameModPage : public ModPage { - Q_OBJECT - - public: - static FlameModPage* create(ModDownloadDialog* dialog, BaseInstance* instance) - { - return ModPage::create<FlameModPage>(dialog, instance); - } - - FlameModPage(ModDownloadDialog* dialog, BaseInstance* instance); - ~FlameModPage() override = default; - - inline auto displayName() const -> QString override { return "CurseForge"; } - inline auto icon() const -> QIcon override { return APPLICATION->getThemedIcon("flame"); } - inline auto id() const -> QString override { return "curseforge"; } - inline auto helpPage() const -> QString override { return "Mod-platform"; } - - inline auto debugName() const -> QString override { return "Flame"; } - inline auto metaEntryBase() const -> QString override { return "FlameMods"; }; - - auto validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, ModAPI::ModLoaderTypes loaders = ModAPI::Unspecified) const -> bool override; - bool optedOut(ModPlatform::IndexedVersion& ver) const override; - - auto shouldDisplay() const -> bool override; - - void openUrl(const QUrl& url) override; -}; diff --git a/launcher/ui/pages/modplatform/flame/FlameModel.cpp b/launcher/ui/pages/modplatform/flame/FlameModel.cpp index 127c3de5..5961ea02 100644 --- a/launcher/ui/pages/modplatform/flame/FlameModel.cpp +++ b/launcher/ui/pages/modplatform/flame/FlameModel.cpp @@ -155,7 +155,7 @@ void ListModel::fetchMore(const QModelIndex& parent) void ListModel::performPaginatedSearch() { - NetJob* netJob = new NetJob("Flame::Search", APPLICATION->network()); + auto netJob = makeShared<NetJob>("Flame::Search", APPLICATION->network()); auto searchUrl = QString( "https://api.curseforge.com/v1/mods/search?" "gameId=432&" @@ -172,8 +172,8 @@ void ListModel::performPaginatedSearch() netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchUrl), &response)); jobPtr = netJob; jobPtr->start(); - QObject::connect(netJob, &NetJob::succeeded, this, &ListModel::searchRequestFinished); - QObject::connect(netJob, &NetJob::failed, this, &ListModel::searchRequestFailed); + QObject::connect(netJob.get(), &NetJob::succeeded, this, &ListModel::searchRequestFinished); + QObject::connect(netJob.get(), &NetJob::failed, this, &ListModel::searchRequestFailed); } void ListModel::searchWithTerm(const QString& term, int sort) diff --git a/launcher/ui/pages/modplatform/flame/FlameResourceModels.cpp b/launcher/ui/pages/modplatform/flame/FlameResourceModels.cpp new file mode 100644 index 00000000..e3d0bc14 --- /dev/null +++ b/launcher/ui/pages/modplatform/flame/FlameResourceModels.cpp @@ -0,0 +1,119 @@ +// SPDX-FileCopyrightText: 2023 flowln <flowlnlnln@gmail.com> +// +// SPDX-License-Identifier: GPL-3.0-only + +#include "FlameResourceModels.h" + +#include "Json.h" + +#include "modplatform/flame/FlameAPI.h" +#include "modplatform/flame/FlameModIndex.h" + +namespace ResourceDownload { + +FlameModModel::FlameModModel(BaseInstance const& base) : ModModel(base, new FlameAPI) {} + +void FlameModModel::loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) +{ + FlameMod::loadIndexedPack(m, obj); +} + +// We already deal with the URLs when initializing the pack, due to the API response's structure +void FlameModModel::loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj) +{ + FlameMod::loadBody(m, obj); +} + +void FlameModModel::loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) +{ + FlameMod::loadIndexedPackVersions(m, arr, APPLICATION->network(), &m_base_instance); +} + +auto FlameModModel::documentToArray(QJsonDocument& obj) const -> QJsonArray +{ + return Json::ensureArray(obj.object(), "data"); +} + +FlameResourcePackModel::FlameResourcePackModel(const BaseInstance& base) : ResourcePackResourceModel(base, new FlameAPI) {} + +void FlameResourcePackModel::loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) +{ + FlameMod::loadIndexedPack(m, obj); +} + +// We already deal with the URLs when initializing the pack, due to the API response's structure +void FlameResourcePackModel::loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj) +{ + FlameMod::loadBody(m, obj); +} + +void FlameResourcePackModel::loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) +{ + FlameMod::loadIndexedPackVersions(m, arr, APPLICATION->network(), &m_base_instance); +} + +auto FlameResourcePackModel::documentToArray(QJsonDocument& obj) const -> QJsonArray +{ + return Json::ensureArray(obj.object(), "data"); +} + +FlameTexturePackModel::FlameTexturePackModel(const BaseInstance& base) : TexturePackResourceModel(base, new FlameAPI) {} + +void FlameTexturePackModel::loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) +{ + FlameMod::loadIndexedPack(m, obj); +} + +// We already deal with the URLs when initializing the pack, due to the API response's structure +void FlameTexturePackModel::loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj) +{ + FlameMod::loadBody(m, obj); +} + +void FlameTexturePackModel::loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) +{ + FlameMod::loadIndexedPackVersions(m, arr, APPLICATION->network(), &m_base_instance); + + QVector<ModPlatform::IndexedVersion> filtered_versions(m.versions.size()); + + // FIXME: Client-side version filtering. This won't take into account any user-selected filtering. + for (auto const& version : m.versions) { + auto const& mc_versions = version.mcVersion; + + if (std::any_of(mc_versions.constBegin(), mc_versions.constEnd(), + [this](auto const& mc_version){ return Version(mc_version) <= maximumTexturePackVersion(); })) + filtered_versions.push_back(version); + } + + m.versions = filtered_versions; +} + +ResourceAPI::SearchArgs FlameTexturePackModel::createSearchArguments() +{ + auto args = TexturePackResourceModel::createSearchArguments(); + + auto profile = static_cast<const MinecraftInstance&>(m_base_instance).getPackProfile(); + QString instance_minecraft_version = profile->getComponentVersion("net.minecraft"); + + // Bypass the texture pack logic, because we can't do multiple versions in the API query + args.versions = { instance_minecraft_version }; + + return args; +} + +ResourceAPI::VersionSearchArgs FlameTexturePackModel::createVersionsArguments(QModelIndex& entry) +{ + auto args = TexturePackResourceModel::createVersionsArguments(entry); + + // Bypass the texture pack logic, because we can't do multiple versions in the API query + args.mcVersions = {}; + + return args; +} + +auto FlameTexturePackModel::documentToArray(QJsonDocument& obj) const -> QJsonArray +{ + return Json::ensureArray(obj.object(), "data"); +} + +} // namespace ResourceDownload diff --git a/launcher/ui/pages/modplatform/flame/FlameResourceModels.h b/launcher/ui/pages/modplatform/flame/FlameResourceModels.h new file mode 100644 index 00000000..0252ac40 --- /dev/null +++ b/launcher/ui/pages/modplatform/flame/FlameResourceModels.h @@ -0,0 +1,70 @@ +// SPDX-FileCopyrightText: 2023 flowln <flowlnlnln@gmail.com> +// +// SPDX-License-Identifier: GPL-3.0-only + +#pragma once + +#include "ui/pages/modplatform/ModModel.h" +#include "ui/pages/modplatform/ResourcePackModel.h" +#include "ui/pages/modplatform/flame/FlameResourcePages.h" + +namespace ResourceDownload { + +class FlameModModel : public ModModel { + Q_OBJECT + + public: + FlameModModel(const BaseInstance&); + ~FlameModModel() override = default; + + private: + [[nodiscard]] QString debugName() const override { return Flame::debugName() + " (Model)"; } + [[nodiscard]] QString metaEntryBase() const override { return Flame::metaEntryBase(); } + + void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) override; + void loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj) override; + void loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) override; + + auto documentToArray(QJsonDocument& obj) const -> QJsonArray override; +}; + +class FlameResourcePackModel : public ResourcePackResourceModel { + Q_OBJECT + + public: + FlameResourcePackModel(const BaseInstance&); + ~FlameResourcePackModel() override = default; + + private: + [[nodiscard]] QString debugName() const override { return Flame::debugName() + " (Model)"; } + [[nodiscard]] QString metaEntryBase() const override { return Flame::metaEntryBase(); } + + void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) override; + void loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj) override; + void loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) override; + + auto documentToArray(QJsonDocument& obj) const -> QJsonArray override; +}; + +class FlameTexturePackModel : public TexturePackResourceModel { + Q_OBJECT + + public: + FlameTexturePackModel(const BaseInstance&); + ~FlameTexturePackModel() override = default; + + private: + [[nodiscard]] QString debugName() const override { return Flame::debugName() + " (Model)"; } + [[nodiscard]] QString metaEntryBase() const override { return Flame::metaEntryBase(); } + + void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) override; + void loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj) override; + void loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) override; + + ResourceAPI::SearchArgs createSearchArguments() override; + ResourceAPI::VersionSearchArgs createVersionsArguments(QModelIndex&) override; + + auto documentToArray(QJsonDocument& obj) const -> QJsonArray override; +}; + +} // namespace ResourceDownload diff --git a/launcher/ui/pages/modplatform/flame/FlameResourcePages.cpp b/launcher/ui/pages/modplatform/flame/FlameResourcePages.cpp new file mode 100644 index 00000000..f93e27e6 --- /dev/null +++ b/launcher/ui/pages/modplatform/flame/FlameResourcePages.cpp @@ -0,0 +1,182 @@ +// SPDX-FileCopyrightText: 2023 flowln <flowlnlnln@gmail.com> +// +// SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0 +/* + * 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 + * 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 "FlameResourcePages.h" +#include "ui_ResourcePage.h" + +#include "FlameResourceModels.h" +#include "ui/dialogs/ResourceDownloadDialog.h" + +namespace ResourceDownload { + +static bool isOptedOut(ModPlatform::IndexedVersion const& ver) +{ + return ver.downloadUrl.isEmpty(); +} + +FlameModPage::FlameModPage(ModDownloadDialog* dialog, BaseInstance& instance) + : ModPage(dialog, instance) +{ + m_model = new FlameModModel(instance); + m_ui->packView->setModel(m_model); + + addSortings(); + + // sometimes Qt just ignores virtual slots and doesn't work as intended it seems, + // so it's best not to connect them in the parent's contructor... + connect(m_ui->sortByBox, SIGNAL(currentIndexChanged(int)), this, SLOT(triggerSearch())); + connect(m_ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &FlameModPage::onSelectionChanged); + connect(m_ui->versionSelectionBox, &QComboBox::currentTextChanged, this, &FlameModPage::onVersionSelectionChanged); + connect(m_ui->resourceSelectionButton, &QPushButton::clicked, this, &FlameModPage::onResourceSelected); + + m_ui->packDescription->setMetaEntry(metaEntryBase()); +} + +auto FlameModPage::validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, std::optional<ResourceAPI::ModLoaderTypes> loaders) const -> bool +{ + Q_UNUSED(loaders); + return ver.mcVersion.contains(mineVer) && !ver.downloadUrl.isEmpty(); +} + +bool FlameModPage::optedOut(ModPlatform::IndexedVersion& ver) const +{ + return isOptedOut(ver); +} + +void FlameModPage::openUrl(const QUrl& url) +{ + if (url.scheme().isEmpty()) { + QString query = url.query(QUrl::FullyDecoded); + + if (query.startsWith("remoteUrl=")) { + // attempt to resolve url from warning page + query.remove(0, 10); + ModPage::openUrl({QUrl::fromPercentEncoding(query.toUtf8())}); // double decoding is necessary + return; + } + } + + ModPage::openUrl(url); +} + +FlameResourcePackPage::FlameResourcePackPage(ResourcePackDownloadDialog* dialog, BaseInstance& instance) + : ResourcePackResourcePage(dialog, instance) +{ + m_model = new FlameResourcePackModel(instance); + m_ui->packView->setModel(m_model); + + addSortings(); + + // sometimes Qt just ignores virtual slots and doesn't work as intended it seems, + // so it's best not to connect them in the parent's contructor... + connect(m_ui->sortByBox, SIGNAL(currentIndexChanged(int)), this, SLOT(triggerSearch())); + connect(m_ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &FlameResourcePackPage::onSelectionChanged); + connect(m_ui->versionSelectionBox, &QComboBox::currentTextChanged, this, &FlameResourcePackPage::onVersionSelectionChanged); + connect(m_ui->resourceSelectionButton, &QPushButton::clicked, this, &FlameResourcePackPage::onResourceSelected); + + m_ui->packDescription->setMetaEntry(metaEntryBase()); +} + +bool FlameResourcePackPage::optedOut(ModPlatform::IndexedVersion& ver) const +{ + return isOptedOut(ver); +} + +void FlameResourcePackPage::openUrl(const QUrl& url) +{ + if (url.scheme().isEmpty()) { + QString query = url.query(QUrl::FullyDecoded); + + if (query.startsWith("remoteUrl=")) { + // attempt to resolve url from warning page + query.remove(0, 10); + ResourcePackResourcePage::openUrl({QUrl::fromPercentEncoding(query.toUtf8())}); // double decoding is necessary + return; + } + } + + ResourcePackResourcePage::openUrl(url); +} + +FlameTexturePackPage::FlameTexturePackPage(TexturePackDownloadDialog* dialog, BaseInstance& instance) + : TexturePackResourcePage(dialog, instance) +{ + m_model = new FlameTexturePackModel(instance); + m_ui->packView->setModel(m_model); + + addSortings(); + + // sometimes Qt just ignores virtual slots and doesn't work as intended it seems, + // so it's best not to connect them in the parent's contructor... + connect(m_ui->sortByBox, SIGNAL(currentIndexChanged(int)), this, SLOT(triggerSearch())); + connect(m_ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &FlameTexturePackPage::onSelectionChanged); + connect(m_ui->versionSelectionBox, &QComboBox::currentTextChanged, this, &FlameTexturePackPage::onVersionSelectionChanged); + connect(m_ui->resourceSelectionButton, &QPushButton::clicked, this, &FlameTexturePackPage::onResourceSelected); + + m_ui->packDescription->setMetaEntry(metaEntryBase()); +} + +bool FlameTexturePackPage::optedOut(ModPlatform::IndexedVersion& ver) const +{ + return isOptedOut(ver); +} + +void FlameTexturePackPage::openUrl(const QUrl& url) +{ + if (url.scheme().isEmpty()) { + QString query = url.query(QUrl::FullyDecoded); + + if (query.startsWith("remoteUrl=")) { + // attempt to resolve url from warning page + query.remove(0, 10); + ResourcePackResourcePage::openUrl({QUrl::fromPercentEncoding(query.toUtf8())}); // double decoding is necessary + return; + } + } + + TexturePackResourcePage::openUrl(url); +} + +// I don't know why, but doing this on the parent class makes it so that +// other mod providers start loading before being selected, at least with +// my Qt, so we need to implement this in every derived class... +auto FlameModPage::shouldDisplay() const -> bool { return true; } +auto FlameResourcePackPage::shouldDisplay() const -> bool { return true; } +auto FlameTexturePackPage::shouldDisplay() const -> bool { return true; } + +} // namespace ResourceDownload diff --git a/launcher/ui/pages/modplatform/flame/FlameResourcePages.h b/launcher/ui/pages/modplatform/flame/FlameResourcePages.h new file mode 100644 index 00000000..103a6bb9 --- /dev/null +++ b/launcher/ui/pages/modplatform/flame/FlameResourcePages.h @@ -0,0 +1,141 @@ +// SPDX-FileCopyrightText: 2023 flowln <flowlnlnln@gmail.com> +// +// SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0 +/* + * 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 + * 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 "Application.h" + +#include "modplatform/ResourceAPI.h" + +#include "ui/pages/modplatform/ModPage.h" +#include "ui/pages/modplatform/ResourcePackPage.h" +#include "ui/pages/modplatform/TexturePackPage.h" + +namespace ResourceDownload { + +namespace Flame { +static inline QString displayName() { return "CurseForge"; } +static inline QIcon icon() { return APPLICATION->getThemedIcon("flame"); } +static inline QString id() { return "curseforge"; } +static inline QString debugName() { return "Flame"; } +static inline QString metaEntryBase() { return "FlameMods"; } +} + +class FlameModPage : public ModPage { + Q_OBJECT + + public: + static FlameModPage* create(ModDownloadDialog* dialog, BaseInstance& instance) + { + return ModPage::create<FlameModPage>(dialog, instance); + } + + FlameModPage(ModDownloadDialog* dialog, BaseInstance& instance); + ~FlameModPage() override = default; + + [[nodiscard]] bool shouldDisplay() const override; + + [[nodiscard]] inline auto displayName() const -> QString override { return Flame::displayName(); } + [[nodiscard]] inline auto icon() const -> QIcon override { return Flame::icon(); } + [[nodiscard]] inline auto id() const -> QString override { return Flame::id(); } + [[nodiscard]] inline auto debugName() const -> QString override { return Flame::debugName(); } + [[nodiscard]] inline auto metaEntryBase() const -> QString override { return Flame::metaEntryBase(); } + + [[nodiscard]] inline auto helpPage() const -> QString override { return "Mod-platform"; } + + bool validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, std::optional<ResourceAPI::ModLoaderTypes> loaders = {}) const override; + bool optedOut(ModPlatform::IndexedVersion& ver) const override; + + void openUrl(const QUrl& url) override; +}; + +class FlameResourcePackPage : public ResourcePackResourcePage { + Q_OBJECT + + public: + static FlameResourcePackPage* create(ResourcePackDownloadDialog* dialog, BaseInstance& instance) + { + return ResourcePackResourcePage::create<FlameResourcePackPage>(dialog, instance); + } + + FlameResourcePackPage(ResourcePackDownloadDialog* dialog, BaseInstance& instance); + ~FlameResourcePackPage() override = default; + + [[nodiscard]] bool shouldDisplay() const override; + + [[nodiscard]] inline auto displayName() const -> QString override { return Flame::displayName(); } + [[nodiscard]] inline auto icon() const -> QIcon override { return Flame::icon(); } + [[nodiscard]] inline auto id() const -> QString override { return Flame::id(); } + [[nodiscard]] inline auto debugName() const -> QString override { return Flame::debugName(); } + [[nodiscard]] inline auto metaEntryBase() const -> QString override { return Flame::metaEntryBase(); } + + [[nodiscard]] inline auto helpPage() const -> QString override { return ""; } + + bool optedOut(ModPlatform::IndexedVersion& ver) const override; + + void openUrl(const QUrl& url) override; +}; + +class FlameTexturePackPage : public TexturePackResourcePage { + Q_OBJECT + + public: + static FlameTexturePackPage* create(TexturePackDownloadDialog* dialog, BaseInstance& instance) + { + return TexturePackResourcePage::create<FlameTexturePackPage>(dialog, instance); + } + + FlameTexturePackPage(TexturePackDownloadDialog* dialog, BaseInstance& instance); + ~FlameTexturePackPage() override = default; + + [[nodiscard]] bool shouldDisplay() const override; + + [[nodiscard]] inline auto displayName() const -> QString override { return Flame::displayName(); } + [[nodiscard]] inline auto icon() const -> QIcon override { return Flame::icon(); } + [[nodiscard]] inline auto id() const -> QString override { return Flame::id(); } + [[nodiscard]] inline auto debugName() const -> QString override { return Flame::debugName(); } + [[nodiscard]] inline auto metaEntryBase() const -> QString override { return Flame::metaEntryBase(); } + + [[nodiscard]] inline auto helpPage() const -> QString override { return ""; } + + bool optedOut(ModPlatform::IndexedVersion& ver) const override; + + void openUrl(const QUrl& url) override; +}; + +} // namespace ResourceDownload diff --git a/launcher/ui/pages/modplatform/ftb/FtbFilterModel.cpp b/launcher/ui/pages/modplatform/ftb/FtbFilterModel.cpp deleted file mode 100644 index e2b548f2..00000000 --- a/launcher/ui/pages/modplatform/ftb/FtbFilterModel.cpp +++ /dev/null @@ -1,93 +0,0 @@ -/* - * Copyright 2020-2021 Jamie Mansfield <jmansfield@cadixdev.org> - * - * 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 "FtbFilterModel.h" - -#include <QDebug> - -#include "modplatform/modpacksch/FTBPackManifest.h" - -#include "StringUtils.h" - -namespace Ftb { - -FilterModel::FilterModel(QObject *parent) : QSortFilterProxyModel(parent) -{ - currentSorting = Sorting::ByPlays; - sortings.insert(tr("Sort by Plays"), Sorting::ByPlays); - sortings.insert(tr("Sort by Installs"), Sorting::ByInstalls); - sortings.insert(tr("Sort by Name"), Sorting::ByName); -} - -const QMap<QString, FilterModel::Sorting> FilterModel::getAvailableSortings() -{ - return sortings; -} - -QString FilterModel::translateCurrentSorting() -{ - return sortings.key(currentSorting); -} - -void FilterModel::setSorting(Sorting sorting) -{ - currentSorting = sorting; - invalidate(); -} - -FilterModel::Sorting FilterModel::getCurrentSorting() -{ - return currentSorting; -} - -void FilterModel::setSearchTerm(const QString& term) -{ - searchTerm = term.trimmed(); - invalidate(); -} - -bool FilterModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const -{ - if (searchTerm.isEmpty()) { - return true; - } - - auto index = sourceModel()->index(sourceRow, 0, sourceParent); - auto pack = sourceModel()->data(index, Qt::UserRole).value<ModpacksCH::Modpack>(); - return pack.name.contains(searchTerm, Qt::CaseInsensitive); -} - -bool FilterModel::lessThan(const QModelIndex &left, const QModelIndex &right) const -{ - ModpacksCH::Modpack leftPack = sourceModel()->data(left, Qt::UserRole).value<ModpacksCH::Modpack>(); - ModpacksCH::Modpack rightPack = sourceModel()->data(right, Qt::UserRole).value<ModpacksCH::Modpack>(); - - if (currentSorting == ByPlays) { - return leftPack.plays < rightPack.plays; - } - else if (currentSorting == ByInstalls) { - return leftPack.installs < rightPack.installs; - } - else if (currentSorting == ByName) { - return StringUtils::naturalCompare(leftPack.name, rightPack.name, Qt::CaseSensitive) >= 0; - } - - // Invalid sorting set, somehow... - qWarning() << "Invalid sorting set!"; - return true; -} - -} diff --git a/launcher/ui/pages/modplatform/ftb/FtbFilterModel.h b/launcher/ui/pages/modplatform/ftb/FtbFilterModel.h deleted file mode 100644 index 1be28e99..00000000 --- a/launcher/ui/pages/modplatform/ftb/FtbFilterModel.h +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright 2020-2021 Jamie Mansfield <jmansfield@cadixdev.org> - * - * 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 <QtCore/QSortFilterProxyModel> - -namespace Ftb { - -class FilterModel : public QSortFilterProxyModel -{ - Q_OBJECT - -public: - FilterModel(QObject* parent = Q_NULLPTR); - enum Sorting { - ByPlays, - ByInstalls, - ByName, - }; - const QMap<QString, Sorting> getAvailableSortings(); - QString translateCurrentSorting(); - void setSorting(Sorting sorting); - Sorting getCurrentSorting(); - void setSearchTerm(const QString& term); - -protected: - bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override; - bool lessThan(const QModelIndex &left, const QModelIndex &right) const override; - -private: - QMap<QString, Sorting> sortings; - Sorting currentSorting; - QString searchTerm { "" }; - -}; - -} diff --git a/launcher/ui/pages/modplatform/ftb/FtbListModel.cpp b/launcher/ui/pages/modplatform/ftb/FtbListModel.cpp deleted file mode 100644 index ce2b2b18..00000000 --- a/launcher/ui/pages/modplatform/ftb/FtbListModel.cpp +++ /dev/null @@ -1,304 +0,0 @@ -/* - * Copyright 2020-2021 Jamie Mansfield <jmansfield@cadixdev.org> - * - * 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 "FtbListModel.h" - -#include "BuildConfig.h" -#include "Application.h" -#include "Json.h" - -#include <QPainter> - -namespace Ftb { - -ListModel::ListModel(QObject *parent) : QAbstractListModel(parent) -{ -} - -ListModel::~ListModel() -{ -} - -int ListModel::rowCount(const QModelIndex &parent) const -{ - return parent.isValid() ? 0 : modpacks.size(); -} - -int ListModel::columnCount(const QModelIndex &parent) const -{ - return parent.isValid() ? 0 : 1; -} - -QVariant ListModel::data(const QModelIndex &index, int role) const -{ - int pos = index.row(); - if(pos >= modpacks.size() || pos < 0 || !index.isValid()) - { - return QString("INVALID INDEX %1").arg(pos); - } - - ModpacksCH::Modpack pack = modpacks.at(pos); - if(role == Qt::DisplayRole) - { - return pack.name; - } - else if (role == Qt::ToolTipRole) - { - return pack.synopsis; - } - else if(role == Qt::DecorationRole) - { - QIcon placeholder = APPLICATION->getThemedIcon("screenshot-placeholder"); - - auto iter = m_logoMap.find(pack.name); - if (iter != m_logoMap.end()) { - auto & logo = *iter; - if(!logo.result.isNull()) { - return logo.result; - } - return placeholder; - } - - for(auto art : pack.art) { - if(art.type == "square") { - ((ListModel *)this)->requestLogo(pack.name, art.url); - } - } - return placeholder; - } - else if(role == Qt::UserRole) - { - QVariant v; - v.setValue(pack); - return v; - } - - return QVariant(); -} - -void ListModel::getLogo(const QString &logo, const QString &logoUrl, LogoCallback callback) -{ - if(m_logoMap.contains(logo)) - { - callback(APPLICATION->metacache()->resolveEntry("ModpacksCHPacks", QString("logos/%1").arg(logo.section(".", 0, 0)))->getFullPath()); - } - else - { - requestLogo(logo, logoUrl); - } -} - -void ListModel::request() -{ - m_aborted = false; - - beginResetModel(); - modpacks.clear(); - endResetModel(); - - auto *netJob = new NetJob("Ftb::Request", APPLICATION->network()); - auto url = QString(BuildConfig.MODPACKSCH_API_BASE_URL + "public/modpack/all"); - netJob->addNetAction(Net::Download::makeByteArray(QUrl(url), &response)); - jobPtr = netJob; - jobPtr->start(); - - QObject::connect(netJob, &NetJob::succeeded, this, &ListModel::requestFinished); - QObject::connect(netJob, &NetJob::failed, this, &ListModel::requestFailed); -} - -void ListModel::abortRequest() -{ - m_aborted = jobPtr->abort(); - jobPtr.reset(); -} - -void ListModel::requestFinished() -{ - jobPtr.reset(); - remainingPacks.clear(); - - QJsonParseError parse_error {}; - QJsonDocument doc = QJsonDocument::fromJson(response, &parse_error); - if(parse_error.error != QJsonParseError::NoError) { - qWarning() << "Error while parsing JSON response from ModpacksCH at " << parse_error.offset << " reason: " << parse_error.errorString(); - qWarning() << response; - return; - } - - auto packs = doc.object().value("packs").toArray(); - for(auto pack : packs) { - auto packId = pack.toInt(); - remainingPacks.append(packId); - } - - if(!remainingPacks.isEmpty()) { - currentPack = remainingPacks.at(0); - requestPack(); - } -} - -void ListModel::requestFailed(QString reason) -{ - jobPtr.reset(); - remainingPacks.clear(); -} - -void ListModel::requestPack() -{ - auto *netJob = new NetJob("Ftb::Search", APPLICATION->network()); - auto searchUrl = QString(BuildConfig.MODPACKSCH_API_BASE_URL + "public/modpack/%1").arg(currentPack); - netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchUrl), &response)); - jobPtr = netJob; - jobPtr->start(); - - QObject::connect(netJob, &NetJob::succeeded, this, &ListModel::packRequestFinished); - QObject::connect(netJob, &NetJob::failed, this, &ListModel::packRequestFailed); -} - -void ListModel::packRequestFinished() -{ - if (!jobPtr || m_aborted) - return; - - jobPtr.reset(); - remainingPacks.removeOne(currentPack); - - QJsonParseError parse_error; - QJsonDocument doc = QJsonDocument::fromJson(response, &parse_error); - - if(parse_error.error != QJsonParseError::NoError) { - qWarning() << "Error while parsing JSON response from ModpacksCH at " << parse_error.offset << " reason: " << parse_error.errorString(); - qWarning() << response; - return; - } - - auto obj = doc.object(); - - ModpacksCH::Modpack pack; - try - { - ModpacksCH::loadModpack(pack, obj); - } - catch (const JSONValidationError &e) - { - qDebug() << QString::fromUtf8(response); - qWarning() << "Error while reading pack manifest from ModpacksCH: " << e.cause(); - return; - } - - // Since there is no guarantee that packs have a version, this will just - // ignore those "dud" packs. - if (pack.versions.empty()) - { - qWarning() << "ModpacksCH Pack " << pack.id << " ignored. reason: lacking any versions"; - } - else - { - beginInsertRows(QModelIndex(), modpacks.size(), modpacks.size()); - modpacks.append(pack); - endInsertRows(); - } - - if(!remainingPacks.isEmpty()) { - currentPack = remainingPacks.at(0); - requestPack(); - } -} - -void ListModel::packRequestFailed(QString reason) -{ - jobPtr.reset(); - remainingPacks.removeOne(currentPack); -} - -void ListModel::logoLoaded(QString logo, bool stale) -{ - auto & logoObj = m_logoMap[logo]; - logoObj.downloadJob.reset(); - QString smallPath = logoObj.fullpath + ".small"; - - QFileInfo smallInfo(smallPath); - - if(stale || !smallInfo.exists()) { - QImage image(logoObj.fullpath); - if (image.isNull()) - { - logoObj.failed = true; - return; - } - QImage small; - if (image.width() > image.height()) { - small = image.scaledToWidth(512).scaledToWidth(256, Qt::SmoothTransformation); - } - else { - small = image.scaledToHeight(512).scaledToHeight(256, Qt::SmoothTransformation); - } - QPoint offset((256 - small.width()) / 2, (256 - small.height()) / 2); - QImage square(QSize(256, 256), QImage::Format_ARGB32); - square.fill(Qt::transparent); - - QPainter painter(&square); - painter.drawImage(offset, small); - painter.end(); - - square.save(logoObj.fullpath + ".small", "PNG"); - } - - logoObj.result = QIcon(logoObj.fullpath + ".small"); - for(int i = 0; i < modpacks.size(); i++) { - if(modpacks[i].name == logo) { - emit dataChanged(createIndex(i, 0), createIndex(i, 0), {Qt::DecorationRole}); - } - } -} - -void ListModel::logoFailed(QString logo) -{ - m_logoMap[logo].failed = true; - m_logoMap[logo].downloadJob.reset(); -} - -void ListModel::requestLogo(QString logo, QString url) -{ - if(m_logoMap.contains(logo)) { - return; - } - - MetaEntryPtr entry = APPLICATION->metacache()->resolveEntry("ModpacksCHPacks", QString("logos/%1").arg(logo.section(".", 0, 0))); - - bool stale = entry->isStale(); - - NetJob *job = new NetJob(QString("ModpacksCH Icon Download %1").arg(logo), APPLICATION->network()); - job->addNetAction(Net::Download::makeCached(QUrl(url), entry)); - - auto fullPath = entry->getFullPath(); - QObject::connect(job, &NetJob::finished, this, [this, logo, fullPath, stale] - { - logoLoaded(logo, stale); - }); - - QObject::connect(job, &NetJob::failed, this, [this, logo] - { - logoFailed(logo); - }); - - auto &newLogoEntry = m_logoMap[logo]; - newLogoEntry.downloadJob = job; - newLogoEntry.fullpath = fullPath; - job->start(); -} - -} diff --git a/launcher/ui/pages/modplatform/ftb/FtbListModel.h b/launcher/ui/pages/modplatform/ftb/FtbListModel.h deleted file mode 100644 index d7a120f0..00000000 --- a/launcher/ui/pages/modplatform/ftb/FtbListModel.h +++ /dev/null @@ -1,83 +0,0 @@ -/* - * Copyright 2020-2021 Jamie Mansfield <jmansfield@cadixdev.org> - * - * 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 <QAbstractListModel> - -#include "modplatform/modpacksch/FTBPackManifest.h" -#include "net/NetJob.h" -#include <QIcon> - -namespace Ftb { - -struct Logo { - QString fullpath; - NetJob::Ptr downloadJob; - QIcon result; - bool failed = false; -}; - -typedef QMap<QString, Logo> LogoMap; -typedef std::function<void(QString)> LogoCallback; - -class ListModel : public QAbstractListModel -{ - Q_OBJECT - -public: - ListModel(QObject *parent); - virtual ~ListModel(); - - int rowCount(const QModelIndex &parent) const override; - int columnCount(const QModelIndex &parent) const override; - QVariant data(const QModelIndex &index, int role) const override; - - void request(); - void abortRequest(); - - void getLogo(const QString &logo, const QString &logoUrl, LogoCallback callback); - - [[nodiscard]] bool isMakingRequest() const { return jobPtr.get(); } - [[nodiscard]] bool wasAborted() const { return m_aborted; } - -private slots: - void requestFinished(); - void requestFailed(QString reason); - - void requestPack(); - void packRequestFinished(); - void packRequestFailed(QString reason); - - void logoFailed(QString logo); - void logoLoaded(QString logo, bool stale); - -private: - void requestLogo(QString file, QString url); - -private: - bool m_aborted = false; - - QList<ModpacksCH::Modpack> modpacks; - LogoMap m_logoMap; - - NetJob::Ptr jobPtr; - int currentPack; - QList<int> remainingPacks; - QByteArray response; -}; - -} diff --git a/launcher/ui/pages/modplatform/ftb/FtbPage.cpp b/launcher/ui/pages/modplatform/ftb/FtbPage.cpp deleted file mode 100644 index b08f3bc4..00000000 --- a/launcher/ui/pages/modplatform/ftb/FtbPage.cpp +++ /dev/null @@ -1,200 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-only -/* - * PolyMC - Minecraft Launcher - * Copyright (c) 2022 Jamie Mansfield <jmansfield@cadixdev.org> - * 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 2020-2021 Jamie Mansfield <jmansfield@cadixdev.org> - * Copyright 2021 Philip T <me@phit.link> - * - * 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 "FtbPage.h" -#include "ui_FtbPage.h" - -#include <QKeyEvent> - -#include "ui/dialogs/NewInstanceDialog.h" -#include "modplatform/modpacksch/FTBPackInstallTask.h" - -#include "HoeDown.h" - -FtbPage::FtbPage(NewInstanceDialog* dialog, QWidget *parent) - : QWidget(parent), ui(new Ui::FtbPage), dialog(dialog) -{ - ui->setupUi(this); - - filterModel = new Ftb::FilterModel(this); - listModel = new Ftb::ListModel(this); - filterModel->setSourceModel(listModel); - ui->packView->setModel(filterModel); - ui->packView->setSortingEnabled(true); - ui->packView->header()->hide(); - ui->packView->setIndentation(0); - - ui->searchEdit->installEventFilter(this); - - ui->versionSelectionBox->view()->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); - ui->versionSelectionBox->view()->parentWidget()->setMaximumHeight(300); - - for(int i = 0; i < filterModel->getAvailableSortings().size(); i++) - { - ui->sortByBox->addItem(filterModel->getAvailableSortings().keys().at(i)); - } - ui->sortByBox->setCurrentText(filterModel->translateCurrentSorting()); - - connect(ui->searchEdit, &QLineEdit::textChanged, this, &FtbPage::triggerSearch); - connect(ui->sortByBox, &QComboBox::currentTextChanged, this, &FtbPage::onSortingSelectionChanged); - connect(ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &FtbPage::onSelectionChanged); - connect(ui->versionSelectionBox, &QComboBox::currentTextChanged, this, &FtbPage::onVersionSelectionChanged); - - ui->packDescription->setMetaEntry("FTBPacks"); -} - -FtbPage::~FtbPage() -{ - delete ui; -} - -bool FtbPage::eventFilter(QObject* watched, QEvent* event) -{ - if (watched == ui->searchEdit && event->type() == QEvent::KeyPress) { - QKeyEvent* keyEvent = static_cast<QKeyEvent*>(event); - if (keyEvent->key() == Qt::Key_Return) { - triggerSearch(); - keyEvent->accept(); - return true; - } - } - return QWidget::eventFilter(watched, event); -} - -bool FtbPage::shouldDisplay() const -{ - return true; -} - -void FtbPage::retranslate() -{ - ui->retranslateUi(this); -} - -void FtbPage::openedImpl() -{ - if(!initialised || listModel->wasAborted()) - { - listModel->request(); - initialised = true; - } - - suggestCurrent(); -} - -void FtbPage::closedImpl() -{ - if (listModel->isMakingRequest()) - listModel->abortRequest(); -} - -void FtbPage::suggestCurrent() -{ - if(!isOpened) - { - return; - } - - if (selectedVersion.isEmpty()) - { - dialog->setSuggestedPack(); - return; - } - - dialog->setSuggestedPack(selected.name, selectedVersion, new ModpacksCH::PackInstallTask(selected, selectedVersion, this)); - for(auto art : selected.art) { - if(art.type == "square") { - QString editedLogoName; - editedLogoName = selected.name; - - listModel->getLogo(selected.name, art.url, [this, editedLogoName](QString logo) - { - dialog->setSuggestedIconFromFile(logo + ".small", editedLogoName); - }); - } - } -} - -void FtbPage::triggerSearch() -{ - filterModel->setSearchTerm(ui->searchEdit->text()); -} - -void FtbPage::onSortingSelectionChanged(QString data) -{ - auto toSet = filterModel->getAvailableSortings().value(data); - filterModel->setSorting(toSet); -} - -void FtbPage::onSelectionChanged(QModelIndex first, QModelIndex second) -{ - ui->versionSelectionBox->clear(); - - if(!first.isValid()) - { - if(isOpened) - { - dialog->setSuggestedPack(); - } - return; - } - - selected = filterModel->data(first, Qt::UserRole).value<ModpacksCH::Modpack>(); - - HoeDown hoedown; - QString output = hoedown.process(selected.description.toUtf8()); - ui->packDescription->setHtml(output); - - // reverse foreach, so that the newest versions are first - for (auto i = selected.versions.size(); i--;) { - ui->versionSelectionBox->addItem(selected.versions.at(i).name); - } - - suggestCurrent(); -} - -void FtbPage::onVersionSelectionChanged(QString data) -{ - if(data.isNull() || data.isEmpty()) - { - selectedVersion = ""; - return; - } - - selectedVersion = data; - suggestCurrent(); -} diff --git a/launcher/ui/pages/modplatform/ftb/FtbPage.h b/launcher/ui/pages/modplatform/ftb/FtbPage.h deleted file mode 100644 index 631ae7f5..00000000 --- a/launcher/ui/pages/modplatform/ftb/FtbPage.h +++ /dev/null @@ -1,105 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-only -/* - * PolyMC - Minecraft Launcher - * Copyright (c) 2022 Jamie Mansfield <jmansfield@cadixdev.org> - * - * 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 "FtbFilterModel.h" -#include "FtbListModel.h" - -#include <QWidget> - -#include "Application.h" -#include "ui/pages/BasePage.h" -#include "tasks/Task.h" - -namespace Ui -{ - class FtbPage; -} - -class NewInstanceDialog; - -class FtbPage : public QWidget, public BasePage -{ -Q_OBJECT - -public: - explicit FtbPage(NewInstanceDialog* dialog, QWidget *parent = 0); - virtual ~FtbPage(); - virtual QString displayName() const override - { - return "FTB"; - } - virtual QIcon icon() const override - { - return APPLICATION->getThemedIcon("ftb_logo"); - } - virtual QString id() const override - { - return "ftb"; - } - virtual QString helpPage() const override - { - return "FTB-platform"; - } - virtual bool shouldDisplay() const override; - void retranslate() override; - - void openedImpl() override; - void closedImpl() override; - - bool eventFilter(QObject * watched, QEvent * event) override; - -private: - void suggestCurrent(); - -private slots: - void triggerSearch(); - - void onSortingSelectionChanged(QString data); - void onSelectionChanged(QModelIndex first, QModelIndex second); - void onVersionSelectionChanged(QString data); - -private: - Ui::FtbPage *ui = nullptr; - NewInstanceDialog* dialog = nullptr; - Ftb::ListModel* listModel = nullptr; - Ftb::FilterModel* filterModel = nullptr; - - ModpacksCH::Modpack selected; - QString selectedVersion; - - bool initialised { false }; -}; diff --git a/launcher/ui/pages/modplatform/ftb/FtbPage.ui b/launcher/ui/pages/modplatform/ftb/FtbPage.ui deleted file mode 100644 index 8de0f4e6..00000000 --- a/launcher/ui/pages/modplatform/ftb/FtbPage.ui +++ /dev/null @@ -1,86 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<ui version="4.0"> - <class>FtbPage</class> - <widget class="QWidget" name="FtbPage"> - <property name="geometry"> - <rect> - <x>0</x> - <y>0</y> - <width>875</width> - <height>745</height> - </rect> - </property> - <layout class="QGridLayout" name="gridLayout"> - <item row="2" column="0" colspan="2"> - <layout class="QGridLayout" name="gridLayout_4" columnstretch="0,0,0" rowminimumheight="0" columnminimumwidth="0,0,0"> - <item row="0" column="2"> - <widget class="QComboBox" name="versionSelectionBox"/> - </item> - <item row="0" column="1"> - <widget class="QLabel" name="label"> - <property name="text"> - <string>Version selected:</string> - </property> - <property name="alignment"> - <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set> - </property> - </widget> - </item> - <item row="0" column="0"> - <widget class="QComboBox" name="sortByBox"/> - </item> - </layout> - </item> - <item row="0" column="0"> - <widget class="QLineEdit" name="searchEdit"> - <property name="placeholderText"> - <string>Search and filter...</string> - </property> - <property name="clearButtonEnabled"> - <bool>true</bool> - </property> - </widget> - </item> - <item row="1" column="0" colspan="2"> - <layout class="QGridLayout" name="gridLayout_3"> - <item row="0" column="0"> - <widget class="QTreeView" name="packView"> - <property name="alternatingRowColors"> - <bool>true</bool> - </property> - <property name="iconSize"> - <size> - <width>48</width> - <height>48</height> - </size> - </property> - </widget> - </item> - <item row="0" column="1"> - <widget class="ProjectDescriptionPage" name="packDescription"> - <property name="openExternalLinks"> - <bool>true</bool> - </property> - <property name="openLinks"> - <bool>true</bool> - </property> - </widget> - </item> - </layout> - </item> - </layout> - </widget> - <customwidgets> - <customwidget> - <class>ProjectDescriptionPage</class> - <extends>QTextBrowser</extends> - <header>ui/widgets/ProjectDescriptionPage.h</header> - </customwidget> - </customwidgets> - <tabstops> - <tabstop>searchEdit</tabstop> - <tabstop>versionSelectionBox</tabstop> - </tabstops> - <resources/> - <connections/> -</ui> diff --git a/launcher/ui/pages/modplatform/legacy_ftb/ListModel.cpp b/launcher/ui/pages/modplatform/legacy_ftb/ListModel.cpp index 6b1f6b89..2343b79f 100644 --- a/launcher/ui/pages/modplatform/legacy_ftb/ListModel.cpp +++ b/launcher/ui/pages/modplatform/legacy_ftb/ListModel.cpp @@ -35,6 +35,8 @@ #include "ListModel.h" #include "Application.h" +#include "net/HttpMetaCache.h" +#include "net/NetJob.h" #include "StringUtils.h" #include <Version.h> diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthModModel.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthModModel.cpp deleted file mode 100644 index af92e63e..00000000 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthModModel.cpp +++ /dev/null @@ -1,48 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-only -/* - * PolyMC - 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/>. - */ - -#include "ModrinthModModel.h" - -#include "modplatform/modrinth/ModrinthPackIndex.h" - -namespace Modrinth { - -// NOLINTNEXTLINE(modernize-avoid-c-arrays) -const char* ListModel::sorts[5]{ "relevance", "downloads", "follows", "updated", "newest" }; - -void ListModel::loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) -{ - Modrinth::loadIndexedPack(m, obj); -} - -void ListModel::loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj) -{ - Modrinth::loadExtraPackData(m, obj); -} - -void ListModel::loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) -{ - Modrinth::loadIndexedPackVersions(m, arr, APPLICATION->network(), m_parent->m_instance); -} - -auto ListModel::documentToArray(QJsonDocument& obj) const -> QJsonArray -{ - return obj.object().value("hits").toArray(); -} - -} // namespace Modrinth diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthModModel.h b/launcher/ui/pages/modplatform/modrinth/ModrinthModModel.h deleted file mode 100644 index 386897fd..00000000 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthModModel.h +++ /dev/null @@ -1,44 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-only -/* - * PolyMC - 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/>. - */ - -#pragma once - -#include "ModrinthModPage.h" - -namespace Modrinth { - -class ListModel : public ModPlatform::ListModel { - Q_OBJECT - - public: - ListModel(ModrinthModPage* parent) : ModPlatform::ListModel(parent){}; - ~ListModel() override = default; - - private: - void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) override; - void loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj) override; - void loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) override; - - auto documentToArray(QJsonDocument& obj) const -> QJsonArray override; - - // NOLINTNEXTLINE(modernize-avoid-c-arrays) - static const char* sorts[5]; - inline auto getSorts() const -> const char** override { return sorts; }; -}; - -} // namespace Modrinth diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthModPage.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthModPage.cpp deleted file mode 100644 index c531ea90..00000000 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthModPage.cpp +++ /dev/null @@ -1,84 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-only -/* - * PolyMC - 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 "ModrinthModPage.h" -#include "modplatform/modrinth/ModrinthAPI.h" -#include "ui_ModPage.h" - -#include "ModrinthModModel.h" -#include "ui/dialogs/ModDownloadDialog.h" - -ModrinthModPage::ModrinthModPage(ModDownloadDialog* dialog, BaseInstance* instance) - : ModPage(dialog, instance, new ModrinthAPI()) -{ - listModel = new Modrinth::ListModel(this); - ui->packView->setModel(listModel); - - // index is used to set the sorting with the modrinth api - ui->sortByBox->addItem(tr("Sort by Relevance")); - ui->sortByBox->addItem(tr("Sort by Downloads")); - ui->sortByBox->addItem(tr("Sort by Follows")); - ui->sortByBox->addItem(tr("Sort by Last Updated")); - ui->sortByBox->addItem(tr("Sort by Newest")); - - // sometimes Qt just ignores virtual slots and doesn't work as intended it seems, - // so it's best not to connect them in the parent's constructor... - connect(ui->sortByBox, SIGNAL(currentIndexChanged(int)), this, SLOT(triggerSearch())); - connect(ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &ModrinthModPage::onSelectionChanged); - connect(ui->versionSelectionBox, &QComboBox::currentTextChanged, this, &ModrinthModPage::onVersionSelectionChanged); - connect(ui->modSelectionButton, &QPushButton::clicked, this, &ModrinthModPage::onModSelected); - - ui->packDescription->setMetaEntry(metaEntryBase()); -} - -auto ModrinthModPage::validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, ModAPI::ModLoaderTypes loaders) const -> bool -{ - auto loaderStrings = ModrinthAPI::getModLoaderStrings(loaders); - - auto loaderCompatible = false; - for (auto remoteLoader : ver.loaders) - { - if (loaderStrings.contains(remoteLoader)) { - loaderCompatible = true; - break; - } - } - return ver.mcVersion.contains(mineVer) && loaderCompatible; -} - -// I don't know why, but doing this on the parent class makes it so that -// other mod providers start loading before being selected, at least with -// my Qt, so we need to implement this in every derived class... -auto ModrinthModPage::shouldDisplay() const -> bool { return true; } diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthModPage.h b/launcher/ui/pages/modplatform/modrinth/ModrinthModPage.h deleted file mode 100644 index 40d82e6f..00000000 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthModPage.h +++ /dev/null @@ -1,66 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-only -/* - * PolyMC - 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 "modplatform/ModAPI.h" -#include "ui/pages/modplatform/ModPage.h" - -#include "modplatform/modrinth/ModrinthAPI.h" - -class ModrinthModPage : public ModPage { - Q_OBJECT - - public: - static ModrinthModPage* create(ModDownloadDialog* dialog, BaseInstance* instance) - { - return ModPage::create<ModrinthModPage>(dialog, instance); - } - - ModrinthModPage(ModDownloadDialog* dialog, BaseInstance* instance); - ~ModrinthModPage() override = default; - - inline auto displayName() const -> QString override { return "Modrinth"; } - inline auto icon() const -> QIcon override { return APPLICATION->getThemedIcon("modrinth"); } - inline auto id() const -> QString override { return "modrinth"; } - inline auto helpPage() const -> QString override { return "Mod-platform"; } - - inline auto debugName() const -> QString override { return "Modrinth"; } - inline auto metaEntryBase() const -> QString override { return "ModrinthPacks"; }; - - auto validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, ModAPI::ModLoaderTypes loaders = ModAPI::Unspecified) const -> bool override; - - auto shouldDisplay() const -> bool override; -}; diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp index e6704eef..346a00b0 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp @@ -40,7 +40,6 @@ #include "Json.h" #include "minecraft/MinecraftInstance.h" #include "minecraft/PackProfile.h" -#include "ui/dialogs/ModDownloadDialog.h" #include "ui/widgets/ProjectItem.h" #include <QMessageBox> @@ -128,7 +127,7 @@ bool ModpackListModel::setData(const QModelIndex &index, const QVariant &value, void ModpackListModel::performPaginatedSearch() { // TODO: Move to standalone API - NetJob* netJob = new NetJob("Modrinth::SearchModpack", APPLICATION->network()); + auto netJob = makeShared<NetJob>("Modrinth::SearchModpack", APPLICATION->network()); auto searchAllUrl = QString(BuildConfig.MODRINTH_PROD_URL + "/search?" "offset=%1&" @@ -143,7 +142,7 @@ void ModpackListModel::performPaginatedSearch() netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchAllUrl), &m_all_response)); - QObject::connect(netJob, &NetJob::succeeded, this, [this] { + QObject::connect(netJob.get(), &NetJob::succeeded, this, [this] { QJsonParseError parse_error_all{}; QJsonDocument doc_all = QJsonDocument::fromJson(m_all_response, &parse_error_all); @@ -156,7 +155,7 @@ void ModpackListModel::performPaginatedSearch() searchRequestFinished(doc_all); }); - QObject::connect(netJob, &NetJob::failed, this, &ModpackListModel::searchRequestFailed); + QObject::connect(netJob.get(), &NetJob::failed, this, &ModpackListModel::searchRequestFailed); jobPtr = netJob; jobPtr->start(); diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h index 3be377a1..6e6be4b9 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h @@ -38,6 +38,7 @@ #include <QAbstractListModel> #include "modplatform/modrinth/ModrinthPackManifest.h" +#include "net/NetJob.h" #include "ui/pages/modplatform/modrinth/ModrinthPage.h" class ModPage; diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp index 8ab2ad1d..0bb11d83 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp @@ -42,11 +42,10 @@ #include "BuildConfig.h" #include "InstanceImportTask.h" #include "Json.h" +#include "Markdown.h" #include "ui/widgets/ProjectItem.h" -#include <HoeDown.h> - #include <QComboBox> #include <QKeyEvent> #include <QPushButton> @@ -280,8 +279,7 @@ void ModrinthPage::updateUI() text += "<hr>"; - HoeDown h; - text += h.process(current.extra.body.toUtf8()); + text += markdownToHTML(current.extra.body.toUtf8()); ui->packDescription->setHtml(text + current.description); ui->packDescription->flush(); diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthResourceModels.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthResourceModels.cpp new file mode 100644 index 00000000..f5d1cc28 --- /dev/null +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthResourceModels.cpp @@ -0,0 +1,116 @@ +// SPDX-FileCopyrightText: 2023 flowln <flowlnlnln@gmail.com> +// +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - 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/>. + */ + +#include "ModrinthResourceModels.h" + +#include "modplatform/modrinth/ModrinthAPI.h" +#include "modplatform/modrinth/ModrinthPackIndex.h" + +namespace ResourceDownload { + +ModrinthModModel::ModrinthModModel(BaseInstance const& base) : ModModel(base, new ModrinthAPI) {} + +void ModrinthModModel::loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) +{ + ::Modrinth::loadIndexedPack(m, obj); +} + +void ModrinthModModel::loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj) +{ + ::Modrinth::loadExtraPackData(m, obj); +} + +void ModrinthModModel::loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) +{ + ::Modrinth::loadIndexedPackVersions(m, arr, APPLICATION->network(), &m_base_instance); +} + +auto ModrinthModModel::documentToArray(QJsonDocument& obj) const -> QJsonArray +{ + return obj.object().value("hits").toArray(); +} + +ModrinthResourcePackModel::ModrinthResourcePackModel(const BaseInstance& base) : ResourcePackResourceModel(base, new ModrinthAPI){} + +void ModrinthResourcePackModel::loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) +{ + ::Modrinth::loadIndexedPack(m, obj); +} + +void ModrinthResourcePackModel::loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj) +{ + ::Modrinth::loadExtraPackData(m, obj); +} + +void ModrinthResourcePackModel::loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) +{ + ::Modrinth::loadIndexedPackVersions(m, arr, APPLICATION->network(), &m_base_instance); +} + +auto ModrinthResourcePackModel::documentToArray(QJsonDocument& obj) const -> QJsonArray +{ + return obj.object().value("hits").toArray(); +} + +ModrinthTexturePackModel::ModrinthTexturePackModel(const BaseInstance& base) : TexturePackResourceModel(base, new ModrinthAPI){} + +void ModrinthTexturePackModel::loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) +{ + ::Modrinth::loadIndexedPack(m, obj); +} + +void ModrinthTexturePackModel::loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj) +{ + ::Modrinth::loadExtraPackData(m, obj); +} + +void ModrinthTexturePackModel::loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) +{ + ::Modrinth::loadIndexedPackVersions(m, arr, APPLICATION->network(), &m_base_instance); +} + +auto ModrinthTexturePackModel::documentToArray(QJsonDocument& obj) const -> QJsonArray +{ + return obj.object().value("hits").toArray(); +} + +ModrinthShaderPackModel::ModrinthShaderPackModel(const BaseInstance& base) : ShaderPackResourceModel(base, new ModrinthAPI){} + +void ModrinthShaderPackModel::loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) +{ + ::Modrinth::loadIndexedPack(m, obj); +} + +void ModrinthShaderPackModel::loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj) +{ + ::Modrinth::loadExtraPackData(m, obj); +} + +void ModrinthShaderPackModel::loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) +{ + ::Modrinth::loadIndexedPackVersions(m, arr, APPLICATION->network(), &m_base_instance); +} + +auto ModrinthShaderPackModel::documentToArray(QJsonDocument& obj) const -> QJsonArray +{ + return obj.object().value("hits").toArray(); +} + +} // namespace ResourceDownload diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthResourceModels.h b/launcher/ui/pages/modplatform/modrinth/ModrinthResourceModels.h new file mode 100644 index 00000000..b351b19b --- /dev/null +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthResourceModels.h @@ -0,0 +1,101 @@ +// SPDX-FileCopyrightText: 2023 flowln <flowlnlnln@gmail.com> +// +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - 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/>. + */ + +#pragma once + +#include "ui/pages/modplatform/ModModel.h" +#include "ui/pages/modplatform/ResourcePackModel.h" +#include "ui/pages/modplatform/modrinth/ModrinthResourcePages.h" + +namespace ResourceDownload { + +class ModrinthModModel : public ModModel { + Q_OBJECT + + public: + ModrinthModModel(const BaseInstance&); + ~ModrinthModModel() override = default; + + private: + [[nodiscard]] QString debugName() const override { return Modrinth::debugName() + " (Model)"; } + [[nodiscard]] QString metaEntryBase() const override { return Modrinth::metaEntryBase(); } + + void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) override; + void loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj) override; + void loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) override; + + auto documentToArray(QJsonDocument& obj) const -> QJsonArray override; +}; + +class ModrinthResourcePackModel : public ResourcePackResourceModel { + Q_OBJECT + + public: + ModrinthResourcePackModel(const BaseInstance&); + ~ModrinthResourcePackModel() override = default; + + private: + [[nodiscard]] QString debugName() const override { return Modrinth::debugName() + " (Model)"; } + [[nodiscard]] QString metaEntryBase() const override { return Modrinth::metaEntryBase(); } + + void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) override; + void loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj) override; + void loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) override; + + auto documentToArray(QJsonDocument& obj) const -> QJsonArray override; +}; + +class ModrinthTexturePackModel : public TexturePackResourceModel { + Q_OBJECT + + public: + ModrinthTexturePackModel(const BaseInstance&); + ~ModrinthTexturePackModel() override = default; + + private: + [[nodiscard]] QString debugName() const override { return Modrinth::debugName() + " (Model)"; } + [[nodiscard]] QString metaEntryBase() const override { return Modrinth::metaEntryBase(); } + + void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) override; + void loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj) override; + void loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) override; + + auto documentToArray(QJsonDocument& obj) const -> QJsonArray override; +}; + +class ModrinthShaderPackModel : public ShaderPackResourceModel { + Q_OBJECT + + public: + ModrinthShaderPackModel(const BaseInstance&); + ~ModrinthShaderPackModel() override = default; + + private: + [[nodiscard]] QString debugName() const override { return Modrinth::debugName() + " (Model)"; } + [[nodiscard]] QString metaEntryBase() const override { return Modrinth::metaEntryBase(); } + + void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) override; + void loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj) override; + void loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) override; + + auto documentToArray(QJsonDocument& obj) const -> QJsonArray override; +}; + +} // namespace ResourceDownload diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.cpp new file mode 100644 index 00000000..dd143700 --- /dev/null +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.cpp @@ -0,0 +1,147 @@ +// SPDX-FileCopyrightText: 2023 flowln <flowlnlnln@gmail.com> +// +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - 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 "ModrinthResourcePages.h" +#include "ui_ResourcePage.h" + +#include "modplatform/modrinth/ModrinthAPI.h" + +#include "ui/dialogs/ResourceDownloadDialog.h" + +#include "ui/pages/modplatform/modrinth/ModrinthResourceModels.h" + +namespace ResourceDownload { + +ModrinthModPage::ModrinthModPage(ModDownloadDialog* dialog, BaseInstance& instance) + : ModPage(dialog, instance) +{ + m_model = new ModrinthModModel(instance); + m_ui->packView->setModel(m_model); + + addSortings(); + + // sometimes Qt just ignores virtual slots and doesn't work as intended it seems, + // so it's best not to connect them in the parent's constructor... + connect(m_ui->sortByBox, SIGNAL(currentIndexChanged(int)), this, SLOT(triggerSearch())); + connect(m_ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &ModrinthModPage::onSelectionChanged); + connect(m_ui->versionSelectionBox, &QComboBox::currentTextChanged, this, &ModrinthModPage::onVersionSelectionChanged); + connect(m_ui->resourceSelectionButton, &QPushButton::clicked, this, &ModrinthModPage::onResourceSelected); + + m_ui->packDescription->setMetaEntry(metaEntryBase()); +} + +auto ModrinthModPage::validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, std::optional<ResourceAPI::ModLoaderTypes> loaders) const -> bool +{ + auto loaderCompatible = !loaders.has_value(); + + if (!loaderCompatible) { + auto loaderStrings = ModrinthAPI::getModLoaderStrings(loaders.value()); + for (auto remoteLoader : ver.loaders) + { + if (loaderStrings.contains(remoteLoader)) { + loaderCompatible = true; + break; + } + } + } + + return ver.mcVersion.contains(mineVer) && loaderCompatible; +} + +ModrinthResourcePackPage::ModrinthResourcePackPage(ResourcePackDownloadDialog* dialog, BaseInstance& instance) + : ResourcePackResourcePage(dialog, instance) +{ + m_model = new ModrinthResourcePackModel(instance); + m_ui->packView->setModel(m_model); + + addSortings(); + + // sometimes Qt just ignores virtual slots and doesn't work as intended it seems, + // so it's best not to connect them in the parent's constructor... + connect(m_ui->sortByBox, SIGNAL(currentIndexChanged(int)), this, SLOT(triggerSearch())); + connect(m_ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &ModrinthResourcePackPage::onSelectionChanged); + connect(m_ui->versionSelectionBox, &QComboBox::currentTextChanged, this, &ModrinthResourcePackPage::onVersionSelectionChanged); + connect(m_ui->resourceSelectionButton, &QPushButton::clicked, this, &ModrinthResourcePackPage::onResourceSelected); + + m_ui->packDescription->setMetaEntry(metaEntryBase()); +} + +ModrinthTexturePackPage::ModrinthTexturePackPage(TexturePackDownloadDialog* dialog, BaseInstance& instance) + : TexturePackResourcePage(dialog, instance) +{ + m_model = new ModrinthTexturePackModel(instance); + m_ui->packView->setModel(m_model); + + addSortings(); + + // sometimes Qt just ignores virtual slots and doesn't work as intended it seems, + // so it's best not to connect them in the parent's constructor... + connect(m_ui->sortByBox, SIGNAL(currentIndexChanged(int)), this, SLOT(triggerSearch())); + connect(m_ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &ModrinthTexturePackPage::onSelectionChanged); + connect(m_ui->versionSelectionBox, &QComboBox::currentTextChanged, this, &ModrinthTexturePackPage::onVersionSelectionChanged); + connect(m_ui->resourceSelectionButton, &QPushButton::clicked, this, &ModrinthTexturePackPage::onResourceSelected); + + m_ui->packDescription->setMetaEntry(metaEntryBase()); +} + +ModrinthShaderPackPage::ModrinthShaderPackPage(ShaderPackDownloadDialog* dialog, BaseInstance& instance) + : ShaderPackResourcePage(dialog, instance) +{ + m_model = new ModrinthShaderPackModel(instance); + m_ui->packView->setModel(m_model); + + addSortings(); + + // sometimes Qt just ignores virtual slots and doesn't work as intended it seems, + // so it's best not to connect them in the parent's constructor... + connect(m_ui->sortByBox, SIGNAL(currentIndexChanged(int)), this, SLOT(triggerSearch())); + connect(m_ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &ModrinthShaderPackPage::onSelectionChanged); + connect(m_ui->versionSelectionBox, &QComboBox::currentTextChanged, this, &ModrinthShaderPackPage::onVersionSelectionChanged); + connect(m_ui->resourceSelectionButton, &QPushButton::clicked, this, &ModrinthShaderPackPage::onResourceSelected); + + m_ui->packDescription->setMetaEntry(metaEntryBase()); +} + +// I don't know why, but doing this on the parent class makes it so that +// other mod providers start loading before being selected, at least with +// my Qt, so we need to implement this in every derived class... +auto ModrinthModPage::shouldDisplay() const -> bool { return true; } +auto ModrinthResourcePackPage::shouldDisplay() const -> bool { return true; } +auto ModrinthTexturePackPage::shouldDisplay() const -> bool { return true; } +auto ModrinthShaderPackPage::shouldDisplay() const -> bool { return true; } + +} // namespace ResourceDownload diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.h b/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.h new file mode 100644 index 00000000..f4eb5ce0 --- /dev/null +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.h @@ -0,0 +1,153 @@ +// SPDX-FileCopyrightText: 2023 flowln <flowlnlnln@gmail.com> +// +// SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0 +/* + * PolyMC - 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 "Application.h" + +#include "modplatform/ResourceAPI.h" + +#include "ui/pages/modplatform/ModPage.h" +#include "ui/pages/modplatform/ResourcePackPage.h" +#include "ui/pages/modplatform/TexturePackPage.h" +#include "ui/pages/modplatform/ShaderPackPage.h" + +namespace ResourceDownload { + +namespace Modrinth { +static inline QString displayName() { return "Modrinth"; } +static inline QIcon icon() { return APPLICATION->getThemedIcon("modrinth"); } +static inline QString id() { return "modrinth"; } +static inline QString debugName() { return "Modrinth"; } +static inline QString metaEntryBase() { return "ModrinthPacks"; } +} + +class ModrinthModPage : public ModPage { + Q_OBJECT + + public: + static ModrinthModPage* create(ModDownloadDialog* dialog, BaseInstance& instance) + { + return ModPage::create<ModrinthModPage>(dialog, instance); + } + + ModrinthModPage(ModDownloadDialog* dialog, BaseInstance& instance); + ~ModrinthModPage() override = default; + + [[nodiscard]] bool shouldDisplay() const override; + + [[nodiscard]] inline auto displayName() const -> QString override { return Modrinth::displayName(); } + [[nodiscard]] inline auto icon() const -> QIcon override { return Modrinth::icon(); } + [[nodiscard]] inline auto id() const -> QString override { return Modrinth::id(); } + [[nodiscard]] inline auto debugName() const -> QString override { return Modrinth::debugName(); } + [[nodiscard]] inline auto metaEntryBase() const -> QString override { return Modrinth::metaEntryBase(); } + + [[nodiscard]] inline auto helpPage() const -> QString override { return "Mod-platform"; } + + auto validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, std::optional<ResourceAPI::ModLoaderTypes> loaders = {}) const -> bool override; +}; + +class ModrinthResourcePackPage : public ResourcePackResourcePage { + Q_OBJECT + + public: + static ModrinthResourcePackPage* create(ResourcePackDownloadDialog* dialog, BaseInstance& instance) + { + return ResourcePackResourcePage::create<ModrinthResourcePackPage>(dialog, instance); + } + + ModrinthResourcePackPage(ResourcePackDownloadDialog* dialog, BaseInstance& instance); + ~ModrinthResourcePackPage() override = default; + + [[nodiscard]] bool shouldDisplay() const override; + + [[nodiscard]] inline auto displayName() const -> QString override { return Modrinth::displayName(); } + [[nodiscard]] inline auto icon() const -> QIcon override { return Modrinth::icon(); } + [[nodiscard]] inline auto id() const -> QString override { return Modrinth::id(); } + [[nodiscard]] inline auto debugName() const -> QString override { return Modrinth::debugName(); } + [[nodiscard]] inline auto metaEntryBase() const -> QString override { return Modrinth::metaEntryBase(); } + + [[nodiscard]] inline auto helpPage() const -> QString override { return ""; } +}; + +class ModrinthTexturePackPage : public TexturePackResourcePage { + Q_OBJECT + + public: + static ModrinthTexturePackPage* create(TexturePackDownloadDialog* dialog, BaseInstance& instance) + { + return TexturePackResourcePage::create<ModrinthTexturePackPage>(dialog, instance); + } + + ModrinthTexturePackPage(TexturePackDownloadDialog* dialog, BaseInstance& instance); + ~ModrinthTexturePackPage() override = default; + + [[nodiscard]] bool shouldDisplay() const override; + + [[nodiscard]] inline auto displayName() const -> QString override { return Modrinth::displayName(); } + [[nodiscard]] inline auto icon() const -> QIcon override { return Modrinth::icon(); } + [[nodiscard]] inline auto id() const -> QString override { return Modrinth::id(); } + [[nodiscard]] inline auto debugName() const -> QString override { return Modrinth::debugName(); } + [[nodiscard]] inline auto metaEntryBase() const -> QString override { return Modrinth::metaEntryBase(); } + + [[nodiscard]] inline auto helpPage() const -> QString override { return ""; } +}; + +class ModrinthShaderPackPage : public ShaderPackResourcePage { + Q_OBJECT + + public: + static ModrinthShaderPackPage* create(ShaderPackDownloadDialog* dialog, BaseInstance& instance) + { + return ShaderPackResourcePage::create<ModrinthShaderPackPage>(dialog, instance); + } + + ModrinthShaderPackPage(ShaderPackDownloadDialog* dialog, BaseInstance& instance); + ~ModrinthShaderPackPage() override = default; + + [[nodiscard]] bool shouldDisplay() const override; + + [[nodiscard]] inline auto displayName() const -> QString override { return Modrinth::displayName(); } + [[nodiscard]] inline auto icon() const -> QIcon override { return Modrinth::icon(); } + [[nodiscard]] inline auto id() const -> QString override { return Modrinth::id(); } + [[nodiscard]] inline auto debugName() const -> QString override { return Modrinth::debugName(); } + [[nodiscard]] inline auto metaEntryBase() const -> QString override { return Modrinth::metaEntryBase(); } + + [[nodiscard]] inline auto helpPage() const -> QString override { return ""; } +}; + +} // namespace ResourceDownload diff --git a/launcher/ui/pages/modplatform/technic/TechnicModel.cpp b/launcher/ui/pages/modplatform/technic/TechnicModel.cpp index b2af1ac0..50f0c72d 100644 --- a/launcher/ui/pages/modplatform/technic/TechnicModel.cpp +++ b/launcher/ui/pages/modplatform/technic/TechnicModel.cpp @@ -112,7 +112,7 @@ void Technic::ListModel::searchWithTerm(const QString& term) void Technic::ListModel::performSearch() { - NetJob *netJob = new NetJob("Technic::Search", APPLICATION->network()); + auto netJob = makeShared<NetJob>("Technic::Search", APPLICATION->network()); QString searchUrl = ""; if (currentSearchTerm.isEmpty()) { searchUrl = QString("%1trending?build=%2") @@ -137,8 +137,8 @@ void Technic::ListModel::performSearch() netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchUrl), &response)); jobPtr = netJob; jobPtr->start(); - QObject::connect(netJob, &NetJob::succeeded, this, &ListModel::searchRequestFinished); - QObject::connect(netJob, &NetJob::failed, this, &ListModel::searchRequestFailed); + QObject::connect(netJob.get(), &NetJob::succeeded, this, &ListModel::searchRequestFinished); + QObject::connect(netJob.get(), &NetJob::failed, this, &ListModel::searchRequestFailed); } void Technic::ListModel::searchRequestFinished() diff --git a/launcher/ui/pages/modplatform/technic/TechnicPage.cpp b/launcher/ui/pages/modplatform/technic/TechnicPage.cpp index b15af244..859da97e 100644 --- a/launcher/ui/pages/modplatform/technic/TechnicPage.cpp +++ b/launcher/ui/pages/modplatform/technic/TechnicPage.cpp @@ -141,10 +141,10 @@ void TechnicPage::suggestCurrent() return; } - NetJob *netJob = new NetJob(QString("Technic::PackMeta(%1)").arg(current.name), APPLICATION->network()); + auto netJob = makeShared<NetJob>(QString("Technic::PackMeta(%1)").arg(current.name), APPLICATION->network()); QString slug = current.slug; netJob->addNetAction(Net::Download::makeByteArray(QString("%1modpack/%2?build=%3").arg(BuildConfig.TECHNIC_API_BASE_URL, slug, BuildConfig.TECHNIC_API_BUILD), &response)); - QObject::connect(netJob, &NetJob::succeeded, this, [this, slug] + QObject::connect(netJob.get(), &NetJob::succeeded, this, [this, slug] { jobPtr.reset(); @@ -247,11 +247,11 @@ void TechnicPage::metadataLoaded() // version so we can display something quicker ui->versionSelectionBox->addItem(current.currentVersion); - auto* netJob = new NetJob(QString("Technic::SolderMeta(%1)").arg(current.name), APPLICATION->network()); + auto netJob = makeShared<NetJob>(QString("Technic::SolderMeta(%1)").arg(current.name), APPLICATION->network()); auto url = QString("%1/modpack/%2").arg(current.url, current.slug); netJob->addNetAction(Net::Download::makeByteArray(QUrl(url), &response)); - QObject::connect(netJob, &NetJob::succeeded, this, &TechnicPage::onSolderLoaded); + QObject::connect(netJob.get(), &NetJob::succeeded, this, &TechnicPage::onSolderLoaded); jobPtr = netJob; jobPtr->start(); diff --git a/launcher/ui/setupwizard/SetupWizard.cpp b/launcher/ui/setupwizard/SetupWizard.cpp index 3c8b5d39..3fd9bb23 100644 --- a/launcher/ui/setupwizard/SetupWizard.cpp +++ b/launcher/ui/setupwizard/SetupWizard.cpp @@ -13,7 +13,8 @@ SetupWizard::SetupWizard(QWidget *parent) : QWizard(parent) { setObjectName(QStringLiteral("SetupWizard")); - resize(615, 659); + resize(620, 660); + setMinimumSize(300, 400); // make it ugly everywhere to avoid variability in theming setWizardStyle(QWizard::ClassicStyle); setOptions(QWizard::NoCancelButton | QWizard::IndependentPages | QWizard::HaveCustomButton1); diff --git a/launcher/ui/setupwizard/ThemeWizardPage.cpp b/launcher/ui/setupwizard/ThemeWizardPage.cpp new file mode 100644 index 00000000..42826aba --- /dev/null +++ b/launcher/ui/setupwizard/ThemeWizardPage.cpp @@ -0,0 +1,70 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * 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 + * 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 "ThemeWizardPage.h" +#include "ui_ThemeWizardPage.h" + +#include "Application.h" +#include "ui/themes/ITheme.h" +#include "ui/themes/ThemeManager.h" +#include "ui/widgets/ThemeCustomizationWidget.h" +#include "ui_ThemeCustomizationWidget.h" + +ThemeWizardPage::ThemeWizardPage(QWidget* parent) : BaseWizardPage(parent), ui(new Ui::ThemeWizardPage) +{ + ui->setupUi(this); + + connect(ui->themeCustomizationWidget, &ThemeCustomizationWidget::currentIconThemeChanged, this, &ThemeWizardPage::updateIcons); + connect(ui->themeCustomizationWidget, &ThemeCustomizationWidget::currentCatChanged, this, &ThemeWizardPage::updateCat); + + updateIcons(); + updateCat(); +} + +ThemeWizardPage::~ThemeWizardPage() +{ + delete ui; +} + +void ThemeWizardPage::updateIcons() +{ + qDebug() << "Setting Icons"; + ui->previewIconButton0->setIcon(APPLICATION->getThemedIcon("new")); + ui->previewIconButton1->setIcon(APPLICATION->getThemedIcon("centralmods")); + ui->previewIconButton2->setIcon(APPLICATION->getThemedIcon("viewfolder")); + ui->previewIconButton3->setIcon(APPLICATION->getThemedIcon("launch")); + ui->previewIconButton4->setIcon(APPLICATION->getThemedIcon("copy")); + ui->previewIconButton5->setIcon(APPLICATION->getThemedIcon("export")); + ui->previewIconButton6->setIcon(APPLICATION->getThemedIcon("delete")); + ui->previewIconButton7->setIcon(APPLICATION->getThemedIcon("about")); + ui->previewIconButton8->setIcon(APPLICATION->getThemedIcon("settings")); + ui->previewIconButton9->setIcon(APPLICATION->getThemedIcon("cat")); + update(); + repaint(); + parentWidget()->update(); +} + +void ThemeWizardPage::updateCat() +{ + qDebug() << "Setting Cat"; + ui->catImagePreviewButton->setIcon(QIcon(QString(R"(:/backgrounds/%1)").arg(ThemeManager::getCatImage()))); +} + +void ThemeWizardPage::retranslate() +{ + ui->retranslateUi(this); +} diff --git a/launcher/ui/setupwizard/ThemeWizardPage.h b/launcher/ui/setupwizard/ThemeWizardPage.h new file mode 100644 index 00000000..61a3d0c0 --- /dev/null +++ b/launcher/ui/setupwizard/ThemeWizardPage.h @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * 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 + * 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 <QWidget> +#include "BaseWizardPage.h" + +namespace Ui { +class ThemeWizardPage; +} + +class ThemeWizardPage : public BaseWizardPage { + Q_OBJECT + + public: + explicit ThemeWizardPage(QWidget* parent = nullptr); + ~ThemeWizardPage(); + + bool validatePage() override { return true; }; + void retranslate() override; + + private slots: + void updateIcons(); + void updateCat(); + + private: + Ui::ThemeWizardPage* ui; +}; diff --git a/launcher/ui/setupwizard/ThemeWizardPage.ui b/launcher/ui/setupwizard/ThemeWizardPage.ui new file mode 100644 index 00000000..01394ea4 --- /dev/null +++ b/launcher/ui/setupwizard/ThemeWizardPage.ui @@ -0,0 +1,371 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>ThemeWizardPage</class> + <widget class="QWizardPage" name="ThemeWizardPage"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>510</width> + <height>552</height> + </rect> + </property> + <property name="windowTitle"> + <string>WizardPage</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout_2"> + <item> + <widget class="QLabel" name="label"> + <property name="text"> + <string>Select the Theme you wish to use</string> + </property> + </widget> + </item> + <item> + <widget class="ThemeCustomizationWidget" name="themeCustomizationWidget" native="true"> + <property name="minimumSize"> + <size> + <width>0</width> + <height>100</height> + </size> + </property> + </widget> + </item> + <item> + <widget class="QLabel" name="label_2"> + <property name="text"> + <string>Hint: The cat appears in the background and is not shown by default. It is only made visible when pressing the Cat button in the Toolbar.</string> + </property> + <property name="wordWrap"> + <bool>true</bool> + </property> + </widget> + </item> + <item> + <widget class="Line" name="line"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + </widget> + </item> + <item> + <widget class="QLabel" name="label_4"> + <property name="text"> + <string> Preview:</string> + </property> + </widget> + </item> + <item> + <layout class="QGridLayout" name="iconPreview"> + <item row="0" column="2"> + <widget class="QPushButton" name="previewIconButton2"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Maximum" vsizetype="Maximum"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="minimumSize"> + <size> + <width>30</width> + <height>30</height> + </size> + </property> + <property name="icon"> + <iconset theme="applications-engineering"> + <normaloff>.</normaloff>.</iconset> + </property> + <property name="checkable"> + <bool>false</bool> + </property> + <property name="flat"> + <bool>true</bool> + </property> + </widget> + </item> + <item row="0" column="5"> + <widget class="QPushButton" name="previewIconButton5"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Maximum" vsizetype="Maximum"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="minimumSize"> + <size> + <width>30</width> + <height>30</height> + </size> + </property> + <property name="icon"> + <iconset theme="applications-engineering"> + <normaloff>.</normaloff>.</iconset> + </property> + <property name="checkable"> + <bool>false</bool> + </property> + <property name="flat"> + <bool>true</bool> + </property> + </widget> + </item> + <item row="0" column="7"> + <widget class="QPushButton" name="previewIconButton7"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Maximum" vsizetype="Maximum"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="minimumSize"> + <size> + <width>30</width> + <height>30</height> + </size> + </property> + <property name="icon"> + <iconset theme="applications-engineering"> + <normaloff>.</normaloff>.</iconset> + </property> + <property name="checkable"> + <bool>false</bool> + </property> + <property name="flat"> + <bool>true</bool> + </property> + </widget> + </item> + <item row="0" column="4"> + <widget class="QPushButton" name="previewIconButton4"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Maximum" vsizetype="Maximum"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="minimumSize"> + <size> + <width>30</width> + <height>30</height> + </size> + </property> + <property name="icon"> + <iconset theme="applications-engineering"> + <normaloff>.</normaloff>.</iconset> + </property> + <property name="checkable"> + <bool>false</bool> + </property> + <property name="flat"> + <bool>true</bool> + </property> + </widget> + </item> + <item row="0" column="1"> + <widget class="QPushButton" name="previewIconButton1"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Maximum" vsizetype="Maximum"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="minimumSize"> + <size> + <width>30</width> + <height>30</height> + </size> + </property> + <property name="icon"> + <iconset theme="centralmods"> + <normaloff>.</normaloff>.</iconset> + </property> + <property name="checkable"> + <bool>false</bool> + </property> + <property name="flat"> + <bool>true</bool> + </property> + </widget> + </item> + <item row="0" column="0"> + <widget class="QPushButton" name="previewIconButton0"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Maximum" vsizetype="Maximum"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="minimumSize"> + <size> + <width>30</width> + <height>30</height> + </size> + </property> + <property name="icon"> + <iconset theme="applications-engineering"> + <normaloff>.</normaloff>.</iconset> + </property> + <property name="checkable"> + <bool>false</bool> + </property> + <property name="flat"> + <bool>true</bool> + </property> + </widget> + </item> + <item row="0" column="9"> + <widget class="QPushButton" name="previewIconButton9"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Maximum" vsizetype="Maximum"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="minimumSize"> + <size> + <width>30</width> + <height>30</height> + </size> + </property> + <property name="icon"> + <iconset theme="viewfolder"> + <normaloff>.</normaloff>.</iconset> + </property> + <property name="checkable"> + <bool>false</bool> + </property> + <property name="flat"> + <bool>true</bool> + </property> + </widget> + </item> + <item row="0" column="6"> + <widget class="QPushButton" name="previewIconButton6"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Maximum" vsizetype="Maximum"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="minimumSize"> + <size> + <width>30</width> + <height>30</height> + </size> + </property> + <property name="icon"> + <iconset theme="new"> + <normaloff>.</normaloff>.</iconset> + </property> + <property name="checkable"> + <bool>false</bool> + </property> + <property name="flat"> + <bool>true</bool> + </property> + </widget> + </item> + <item row="0" column="3"> + <widget class="QPushButton" name="previewIconButton3"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Maximum" vsizetype="Maximum"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="minimumSize"> + <size> + <width>30</width> + <height>30</height> + </size> + </property> + <property name="icon"> + <iconset theme="applications-engineering"> + <normaloff>.</normaloff>.</iconset> + </property> + <property name="checkable"> + <bool>false</bool> + </property> + <property name="flat"> + <bool>true</bool> + </property> + </widget> + </item> + <item row="0" column="8"> + <widget class="QPushButton" name="previewIconButton8"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Maximum" vsizetype="Maximum"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="minimumSize"> + <size> + <width>30</width> + <height>30</height> + </size> + </property> + <property name="icon"> + <iconset theme="applications-engineering"> + <normaloff>.</normaloff>.</iconset> + </property> + <property name="checkable"> + <bool>false</bool> + </property> + <property name="flat"> + <bool>true</bool> + </property> + </widget> + </item> + </layout> + </item> + <item> + <widget class="QPushButton" name="catImagePreviewButton"> + <property name="minimumSize"> + <size> + <width>0</width> + <height>256</height> + </size> + </property> + <property name="toolTip"> + <string>The cat appears in the background and does not serve a purpose, it is purely visual.</string> + </property> + <property name="text"> + <string/> + </property> + <property name="iconSize"> + <size> + <width>256</width> + <height>256</height> + </size> + </property> + <property name="flat"> + <bool>true</bool> + </property> + </widget> + </item> + <item> + <spacer name="verticalSpacer"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>193</height> + </size> + </property> + </spacer> + </item> + </layout> + </widget> + <customwidgets> + <customwidget> + <class>ThemeCustomizationWidget</class> + <extends>QWidget</extends> + <header>ui/widgets/ThemeCustomizationWidget.h</header> + </customwidget> + </customwidgets> + <resources/> + <connections/> +</ui> diff --git a/launcher/ui/themes/CustomTheme.cpp b/launcher/ui/themes/CustomTheme.cpp index 3ad61668..198e76ba 100644 --- a/launcher/ui/themes/CustomTheme.cpp +++ b/launcher/ui/themes/CustomTheme.cpp @@ -167,8 +167,6 @@ CustomTheme::CustomTheme(ITheme* baseTheme, QFileInfo& fileInfo, bool isManifest if (!FS::ensureFolderPathExists(path) || !FS::ensureFolderPathExists(pathResources)) { themeWarningLog() << "couldn't create folder for theme!"; - m_palette = baseTheme->colorScheme(); - m_styleSheet = baseTheme->appStyleSheet(); return; } @@ -177,18 +175,15 @@ CustomTheme::CustomTheme(ITheme* baseTheme, QFileInfo& fileInfo, bool isManifest bool jsonDataIncomplete = false; m_palette = baseTheme->colorScheme(); - if (!readThemeJson(themeFilePath, m_palette, m_fadeAmount, m_fadeColor, m_name, m_widgets, m_qssFilePath, jsonDataIncomplete)) { - themeDebugLog() << "Did not read theme json file correctly, writing new one to: " << themeFilePath; - m_name = "Custom"; - m_palette = baseTheme->colorScheme(); - m_fadeColor = baseTheme->fadeColor(); - m_fadeAmount = baseTheme->fadeAmount(); - m_widgets = baseTheme->qtTheme(); - m_qssFilePath = "themeStyle.css"; - } else { + if (readThemeJson(themeFilePath, m_palette, m_fadeAmount, m_fadeColor, m_name, m_widgets, m_qssFilePath, jsonDataIncomplete)) { + // If theme data was found, fade "Disabled" color of each role according to FadeAmount m_palette = fadeInactive(m_palette, m_fadeAmount, m_fadeColor); + } else { + themeDebugLog() << "Did not read theme json file correctly, not changing theme, keeping previous."; + return; } + // FIXME: This is kinda jank, it only actually checks if the qss file path is not present. It should actually check for any relevant missing data (e.g. name, colors) if (jsonDataIncomplete) { writeThemeJson(fileInfo.absoluteFilePath(), m_palette, m_fadeAmount, m_fadeColor, m_name, m_widgets, m_qssFilePath); } @@ -197,20 +192,14 @@ CustomTheme::CustomTheme(ITheme* baseTheme, QFileInfo& fileInfo, bool isManifest QFileInfo info(qssFilePath); if (info.isFile()) { try { - // TODO: validate css? + // TODO: validate qss? m_styleSheet = QString::fromUtf8(FS::read(qssFilePath)); } catch (const Exception& e) { - themeWarningLog() << "Couldn't load css:" << e.cause() << "from" << qssFilePath; - m_styleSheet = baseTheme->appStyleSheet(); + themeWarningLog() << "Couldn't load qss:" << e.cause() << "from" << qssFilePath; + return; } } else { - themeDebugLog() << "No theme css present."; - m_styleSheet = baseTheme->appStyleSheet(); - try { - FS::write(qssFilePath, m_styleSheet.toUtf8()); - } catch (const Exception& e) { - themeWarningLog() << "Couldn't write css:" << e.cause() << "to" << qssFilePath; - } + themeDebugLog() << "No theme qss present."; } } else { m_id = fileInfo.fileName(); diff --git a/launcher/ui/themes/ITheme.cpp b/launcher/ui/themes/ITheme.cpp index 8bfc466d..8f0757e1 100644 --- a/launcher/ui/themes/ITheme.cpp +++ b/launcher/ui/themes/ITheme.cpp @@ -1,3 +1,37 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * 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 + * 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 "ITheme.h" #include "rainbow.h" #include <QStyleFactory> @@ -11,9 +45,7 @@ void ITheme::apply(bool) if (hasColorScheme()) { QApplication::setPalette(colorScheme()); } - if (hasStyleSheet()) - APPLICATION->setStyleSheet(appStyleSheet()); - + APPLICATION->setStyleSheet(appStyleSheet()); QDir::setSearchPaths("theme", searchPaths()); } diff --git a/launcher/ui/themes/ITheme.h b/launcher/ui/themes/ITheme.h index c2347cf6..a0a638bd 100644 --- a/launcher/ui/themes/ITheme.h +++ b/launcher/ui/themes/ITheme.h @@ -1,12 +1,45 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * 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 + * 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 <QString> #include <QPalette> +#include <QString> class QStyle; -class ITheme -{ -public: +class ITheme { + public: virtual ~ITheme() {} virtual void apply(bool initial); virtual QString id() = 0; @@ -18,10 +51,7 @@ public: virtual QPalette colorScheme() = 0; virtual QColor fadeColor() = 0; virtual double fadeAmount() = 0; - virtual QStringList searchPaths() - { - return {}; - } + virtual QStringList searchPaths() { return {}; } static QPalette fadeInactive(QPalette in, qreal bias, QColor color); }; diff --git a/launcher/ui/themes/SystemTheme.cpp b/launcher/ui/themes/SystemTheme.cpp index a63d1741..a95bc875 100644 --- a/launcher/ui/themes/SystemTheme.cpp +++ b/launcher/ui/themes/SystemTheme.cpp @@ -34,24 +34,22 @@ */ #include "SystemTheme.h" #include <QApplication> +#include <QDebug> #include <QStyle> #include <QStyleFactory> -#include <QDebug> #include "ThemeManager.h" SystemTheme::SystemTheme() { themeDebugLog() << "Determining System Theme..."; - const auto & style = QApplication::style(); + const auto& style = QApplication::style(); systemPalette = style->standardPalette(); QString lowerThemeName = style->objectName(); themeDebugLog() << "System theme seems to be:" << lowerThemeName; QStringList styles = QStyleFactory::keys(); - for(auto &st: styles) - { + for (auto& st : styles) { themeDebugLog() << "Considering theme from theme factory:" << st.toLower(); - if(st.toLower() == lowerThemeName) - { + if (st.toLower() == lowerThemeName) { systemTheme = st; themeDebugLog() << "System theme has been determined to be:" << systemTheme; return; @@ -64,11 +62,10 @@ SystemTheme::SystemTheme() void SystemTheme::apply(bool initial) { - // if we are applying the system theme as the first theme, just don't touch anything. it's for the better... - if(initial) - { - return; - } + // See https://github.com/MultiMC/Launcher/issues/1790 + // or https://github.com/PrismLauncher/PrismLauncher/issues/490 + if (initial) + return; ITheme::apply(initial); } @@ -104,7 +101,7 @@ double SystemTheme::fadeAmount() QColor SystemTheme::fadeColor() { - return QColor(128,128,128); + return QColor(128, 128, 128); } bool SystemTheme::hasStyleSheet() diff --git a/launcher/ui/themes/SystemTheme.h b/launcher/ui/themes/SystemTheme.h index fe450600..05f31233 100644 --- a/launcher/ui/themes/SystemTheme.h +++ b/launcher/ui/themes/SystemTheme.h @@ -1,10 +1,43 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * 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 + * 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 "ITheme.h" -class SystemTheme: public ITheme -{ -public: +class SystemTheme : public ITheme { + public: SystemTheme(); virtual ~SystemTheme() {} void apply(bool initial) override; @@ -18,7 +51,8 @@ public: QPalette colorScheme() override; double fadeAmount() override; QColor fadeColor() override; -private: + + private: QPalette systemPalette; QString systemTheme; }; diff --git a/launcher/ui/themes/ThemeManager.cpp b/launcher/ui/themes/ThemeManager.cpp index 01a38a86..94ac8a24 100644 --- a/launcher/ui/themes/ThemeManager.cpp +++ b/launcher/ui/themes/ThemeManager.cpp @@ -1,155 +1,155 @@ -// SPDX-License-Identifier: GPL-3.0-only
-/*
- * Prism Launcher - Minecraft Launcher
- * 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
- * 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 "ThemeManager.h"
-
-#include <QApplication>
-#include <QDir>
-#include <QDirIterator>
-#include <QIcon>
-#include "ui/themes/BrightTheme.h"
-#include "ui/themes/CustomTheme.h"
-#include "ui/themes/DarkTheme.h"
-#include "ui/themes/SystemTheme.h"
-
-#include "Application.h"
-
-#ifdef Q_OS_WIN
-#include <windows.h>
-// this is needed for versionhelpers.h, it is also included in WinDarkmode, but we can't rely on that.
-// Ultimately this should be included in versionhelpers, but that is outside of the project.
-#include "ui/WinDarkmode.h"
-#include <versionhelpers.h>
-#endif
-
-ThemeManager::ThemeManager(MainWindow* mainWindow)
-{
- m_mainWindow = mainWindow;
- InitializeThemes();
-}
-
-/// @brief Adds the Theme to the list of themes
-/// @param theme The Theme to add
-/// @return Theme ID
-QString ThemeManager::AddTheme(std::unique_ptr<ITheme> theme)
-{
- QString id = theme->id();
- m_themes.emplace(id, std::move(theme));
- return id;
-}
-
-/// @brief Gets the Theme from the List via ID
-/// @param themeId Theme ID of theme to fetch
-/// @return Theme at themeId
-ITheme* ThemeManager::GetTheme(QString themeId)
-{
- return m_themes[themeId].get();
-}
-
-void ThemeManager::InitializeThemes()
-{
- // 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);
- themeDebugLog() << "<> Icon themes initialized.";
- }
-
- // Initialize widget themes
- {
- themeDebugLog() << "<> Initializing Widget Themes";
- themeDebugLog() << "Loading Built-in Theme:" << AddTheme(std::make_unique<SystemTheme>());
- auto darkThemeId = AddTheme(std::make_unique<DarkTheme>());
- themeDebugLog() << "Loading Built-in Theme:" << darkThemeId;
- themeDebugLog() << "Loading Built-in Theme:" << AddTheme(std::make_unique<BrightTheme>());
-
- // TODO: need some way to differentiate same name themes in different subdirectories (maybe smaller grey text next to theme name in
- // dropdown?)
- QString themeFolder = QDir("./themes/").absoluteFilePath("");
- themeDebugLog() << "Theme Folder Path: " << themeFolder;
-
- QDirIterator directoryIterator(themeFolder, QDir::Dirs | QDir::NoDotAndDotDot, QDirIterator::Subdirectories);
- while (directoryIterator.hasNext()) {
- QDir dir(directoryIterator.next());
- QFileInfo themeJson(dir.absoluteFilePath("theme.json"));
- if (themeJson.exists()) {
- // Load "theme.json" based themes
- themeDebugLog() << "Loading JSON Theme from:" << themeJson.absoluteFilePath();
- AddTheme(std::make_unique<CustomTheme>(GetTheme(darkThemeId), themeJson, true));
- } else {
- // Load pure QSS Themes
- QDirIterator stylesheetFileIterator(dir.absoluteFilePath(""), { "*.qss", "*.css" }, QDir::Files);
- while (stylesheetFileIterator.hasNext()) {
- QFile customThemeFile(stylesheetFileIterator.next());
- QFileInfo customThemeFileInfo(customThemeFile);
- themeDebugLog() << "Loading QSS Theme from:" << customThemeFileInfo.absoluteFilePath();
- AddTheme(std::make_unique<CustomTheme>(GetTheme(darkThemeId), customThemeFileInfo, false));
- }
- }
- }
-
- themeDebugLog() << "<> Widget themes initialized.";
- }
-}
-
-QList<ITheme*> ThemeManager::getValidApplicationThemes()
-{
- QList<ITheme*> ret;
- ret.reserve(m_themes.size());
- for (auto&& [id, theme] : m_themes) {
- ret.append(theme.get());
- }
- return ret;
-}
-
-void ThemeManager::setIconTheme(const QString& name)
-{
- QIcon::setThemeName(name);
-}
-
-void ThemeManager::applyCurrentlySelectedTheme()
-{
- setIconTheme(APPLICATION->settings()->get("IconTheme").toString());
- themeDebugLog() << "<> Icon theme set.";
- setApplicationTheme(APPLICATION->settings()->get("ApplicationTheme").toString(), true);
- themeDebugLog() << "<> Application theme set.";
-}
-
-void ThemeManager::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;
- themeDebugLog() << "applying theme" << theme->name();
- theme->apply(initial);
-#ifdef Q_OS_WIN
- if (m_mainWindow && IsWindows10OrGreater()) {
- if (QString::compare(theme->id(), "dark") == 0) {
- WinDarkmode::setDarkWinTitlebar(m_mainWindow->winId(), true);
- } else {
- WinDarkmode::setDarkWinTitlebar(m_mainWindow->winId(), false);
- }
- }
-#endif
- } else {
- themeWarningLog() << "Tried to set invalid theme:" << name;
- }
-}
+// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * 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 + * 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 "ThemeManager.h" + +#include <QApplication> +#include <QDir> +#include <QDirIterator> +#include <QIcon> +#include "ui/themes/BrightTheme.h" +#include "ui/themes/CustomTheme.h" +#include "ui/themes/DarkTheme.h" +#include "ui/themes/SystemTheme.h" + +#include "Application.h" + +ThemeManager::ThemeManager(MainWindow* mainWindow) +{ + m_mainWindow = mainWindow; + initializeThemes(); +} + +/// @brief Adds the Theme to the list of themes +/// @param theme The Theme to add +/// @return Theme ID +QString ThemeManager::addTheme(std::unique_ptr<ITheme> theme) +{ + QString id = theme->id(); + m_themes.emplace(id, std::move(theme)); + return id; +} + +/// @brief Gets the Theme from the List via ID +/// @param themeId Theme ID of theme to fetch +/// @return Theme at themeId +ITheme* ThemeManager::getTheme(QString themeId) +{ + return m_themes[themeId].get(); +} + +void ThemeManager::initializeThemes() +{ + // 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); + themeDebugLog() << "<> Icon themes initialized."; + } + + // Initialize widget themes + { + themeDebugLog() << "<> Initializing Widget Themes"; + themeDebugLog() << "Loading Built-in Theme:" << addTheme(std::make_unique<SystemTheme>()); + auto darkThemeId = addTheme(std::make_unique<DarkTheme>()); + themeDebugLog() << "Loading Built-in Theme:" << darkThemeId; + themeDebugLog() << "Loading Built-in Theme:" << addTheme(std::make_unique<BrightTheme>()); + + // TODO: need some way to differentiate same name themes in different subdirectories (maybe smaller grey text next to theme name in + // dropdown?) + QString themeFolder = QDir("./themes/").absoluteFilePath(""); + themeDebugLog() << "Theme Folder Path: " << themeFolder; + + QDirIterator directoryIterator(themeFolder, QDir::Dirs | QDir::NoDotAndDotDot, QDirIterator::Subdirectories); + while (directoryIterator.hasNext()) { + QDir dir(directoryIterator.next()); + QFileInfo themeJson(dir.absoluteFilePath("theme.json")); + if (themeJson.exists()) { + // Load "theme.json" based themes + themeDebugLog() << "Loading JSON Theme from:" << themeJson.absoluteFilePath(); + addTheme(std::make_unique<CustomTheme>(getTheme(darkThemeId), themeJson, true)); + } else { + // Load pure QSS Themes + QDirIterator stylesheetFileIterator(dir.absoluteFilePath(""), { "*.qss", "*.css" }, QDir::Files); + while (stylesheetFileIterator.hasNext()) { + QFile customThemeFile(stylesheetFileIterator.next()); + QFileInfo customThemeFileInfo(customThemeFile); + themeDebugLog() << "Loading QSS Theme from:" << customThemeFileInfo.absoluteFilePath(); + addTheme(std::make_unique<CustomTheme>(getTheme(darkThemeId), customThemeFileInfo, false)); + } + } + } + + themeDebugLog() << "<> Widget themes initialized."; + } +} + +QList<ITheme*> ThemeManager::getValidApplicationThemes() +{ + QList<ITheme*> ret; + ret.reserve(m_themes.size()); + for (auto&& [id, theme] : m_themes) { + ret.append(theme.get()); + } + return ret; +} + +void ThemeManager::setIconTheme(const QString& name) +{ + QIcon::setThemeName(name); +} + +void ThemeManager::applyCurrentlySelectedTheme(bool initial) +{ + setIconTheme(APPLICATION->settings()->get("IconTheme").toString()); + themeDebugLog() << "<> Icon theme set."; + setApplicationTheme(APPLICATION->settings()->get("ApplicationTheme").toString(), initial); + themeDebugLog() << "<> Application theme set."; +} + +void ThemeManager::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; + themeDebugLog() << "applying theme" << theme->name(); + theme->apply(initial); + } else { + themeWarningLog() << "Tried to set invalid theme:" << name; + } +} + +QString ThemeManager::getCatImage(QString catName) +{ + QDateTime now = QDateTime::currentDateTime(); + QDateTime birthday(QDate(now.date().year(), 11, 30), QTime(0, 0)); + QDateTime xmas(QDate(now.date().year(), 12, 25), QTime(0, 0)); + QDateTime halloween(QDate(now.date().year(), 10, 31), QTime(0, 0)); + QString cat = !catName.isEmpty() ? catName : APPLICATION->settings()->get("BackgroundCat").toString(); + if (std::abs(now.daysTo(xmas)) <= 4) { + cat += "-xmas"; + } else if (std::abs(now.daysTo(halloween)) <= 4) { + cat += "-spooky"; + } else if (std::abs(now.daysTo(birthday)) <= 12) { + cat += "-bday"; + } + return cat; +} diff --git a/launcher/ui/themes/ThemeManager.h b/launcher/ui/themes/ThemeManager.h index b85cb742..87f36d9c 100644 --- a/launcher/ui/themes/ThemeManager.h +++ b/launcher/ui/themes/ThemeManager.h @@ -1,52 +1,57 @@ -// SPDX-License-Identifier: GPL-3.0-only
-/*
- * Prism Launcher - Minecraft Launcher
- * 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
- * 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 <QString>
-
-#include "ui/MainWindow.h"
-#include "ui/themes/ITheme.h"
-
-inline auto themeDebugLog()
-{
- return qDebug() << "[Theme]";
-}
-inline auto themeWarningLog()
-{
- return qWarning() << "[Theme]";
-}
-
-class ThemeManager {
- public:
- ThemeManager(MainWindow* mainWindow);
-
- // maybe make private? Or put in ctor?
- void InitializeThemes();
-
- QList<ITheme*> getValidApplicationThemes();
- void setIconTheme(const QString& name);
- void applyCurrentlySelectedTheme();
- void setApplicationTheme(const QString& name, bool initial);
-
- private:
- std::map<QString, std::unique_ptr<ITheme>> m_themes;
- MainWindow* m_mainWindow;
-
- QString AddTheme(std::unique_ptr<ITheme> theme);
- ITheme* GetTheme(QString themeId);
-};
+// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * 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 + * 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 <QString> + +#include "ui/MainWindow.h" +#include "ui/themes/ITheme.h" + +inline auto themeDebugLog() +{ + return qDebug() << "[Theme]"; +} +inline auto themeWarningLog() +{ + return qWarning() << "[Theme]"; +} + +class ThemeManager { + public: + ThemeManager(MainWindow* mainWindow); + + QList<ITheme*> getValidApplicationThemes(); + void setIconTheme(const QString& name); + void applyCurrentlySelectedTheme(bool initial = false); + void setApplicationTheme(const QString& name, bool initial = false); + + /// <summary> + /// Returns the cat based on selected cat and with events (Birthday, XMas, etc.) + /// </summary> + /// <param name="catName">Optional, if you need a specific cat.</param> + /// <returns></returns> + static QString getCatImage(QString catName = ""); + + private: + std::map<QString, std::unique_ptr<ITheme>> m_themes; + MainWindow* m_mainWindow; + + void initializeThemes(); + QString addTheme(std::unique_ptr<ITheme> theme); + ITheme* getTheme(QString themeId); +}; diff --git a/launcher/ui/widgets/ModListView.cpp b/launcher/ui/widgets/ModListView.cpp index d1860f57..09b03a76 100644 --- a/launcher/ui/widgets/ModListView.cpp +++ b/launcher/ui/widgets/ModListView.cpp @@ -14,6 +14,9 @@ */ #include "ModListView.h" + +#include "minecraft/mod/ModFolderModel.h" + #include <QHeaderView> #include <QMouseEvent> #include <QPainter> @@ -62,4 +65,17 @@ void ModListView::setModel ( QAbstractItemModel* model ) for(int i = 1; i < head->count(); i++) head->setSectionResizeMode(i, QHeaderView::ResizeToContents); } + + auto real_model = model; + if (auto proxy_model = dynamic_cast<QSortFilterProxyModel*>(model); proxy_model) + real_model = proxy_model->sourceModel(); + + if (auto mod_model = dynamic_cast<ModFolderModel*>(real_model); mod_model) { + connect(mod_model, &ModFolderModel::updateFinished, this, [this, mod_model]{ + auto mods = mod_model->allMods(); + // Hide the 'Provider' column if no mod has a defined provider! + setColumnHidden(ModFolderModel::Columns::ProviderColumn, + std::none_of(mods.constBegin(), mods.constEnd(), [](auto const mod){ return mod->provider().has_value(); })); + }); + } } diff --git a/launcher/ui/widgets/PageContainer.cpp b/launcher/ui/widgets/PageContainer.cpp index 0a06a351..b9b17b42 100644 --- a/launcher/ui/widgets/PageContainer.cpp +++ b/launcher/ui/widgets/PageContainer.cpp @@ -87,7 +87,9 @@ PageContainer::PageContainer(BasePageProvider *pageProvider, QString defaultId, auto pages = pageProvider->getPages(); for (auto page : pages) { - page->stackIndex = m_pageStack->addWidget(dynamic_cast<QWidget *>(page)); + auto widget = dynamic_cast<QWidget *>(page); + widget->setParent(this); + page->stackIndex = m_pageStack->addWidget(widget); page->listIndex = counter; page->setParentContainer(this); counter++; diff --git a/launcher/ui/widgets/ProgressWidget.cpp b/launcher/ui/widgets/ProgressWidget.cpp index b60d9a7a..9181de7f 100644 --- a/launcher/ui/widgets/ProgressWidget.cpp +++ b/launcher/ui/widgets/ProgressWidget.cpp @@ -39,7 +39,7 @@ void ProgressWidget::progressFormat(QString format) m_bar->setFormat(format); } -void ProgressWidget::watch(Task* task) +void ProgressWidget::watch(const Task* task) { if (!task) return; @@ -51,17 +51,21 @@ void ProgressWidget::watch(Task* task) connect(m_task, &Task::finished, this, &ProgressWidget::handleTaskFinish); connect(m_task, &Task::status, this, &ProgressWidget::handleTaskStatus); + // TODO: should we connect &Task::details connect(m_task, &Task::progress, this, &ProgressWidget::handleTaskProgress); connect(m_task, &Task::destroyed, this, &ProgressWidget::taskDestroyed); - show(); + if (m_task->isRunning()) + show(); + else + connect(m_task, &Task::started, this, &ProgressWidget::show); } -void ProgressWidget::start(Task* task) +void ProgressWidget::start(const Task* task) { watch(task); if (!m_task->isRunning()) - QMetaObject::invokeMethod(m_task, "start", Qt::QueuedConnection); + QMetaObject::invokeMethod(const_cast<Task*>(m_task), "start", Qt::QueuedConnection); } bool ProgressWidget::exec(std::shared_ptr<Task> task) diff --git a/launcher/ui/widgets/ProgressWidget.h b/launcher/ui/widgets/ProgressWidget.h index 4d9097b8..b0458f33 100644 --- a/launcher/ui/widgets/ProgressWidget.h +++ b/launcher/ui/widgets/ProgressWidget.h @@ -27,10 +27,10 @@ class ProgressWidget : public QWidget { public slots: /** Watch the progress of a task. */ - void watch(Task* task); + void watch(const Task* task); /** Watch the progress of a task, and start it if needed */ - void start(Task* task); + void start(const Task* task); /** Blocking way of waiting for a task to finish. */ bool exec(std::shared_ptr<Task> task); @@ -50,7 +50,7 @@ class ProgressWidget : public QWidget { private: QLabel* m_label = nullptr; QProgressBar* m_bar = nullptr; - Task* m_task = nullptr; + const Task* m_task = nullptr; bool m_hide_if_inactive = false; }; diff --git a/launcher/ui/widgets/SubTaskProgressBar.cpp b/launcher/ui/widgets/SubTaskProgressBar.cpp new file mode 100644 index 00000000..84ea5f20 --- /dev/null +++ b/launcher/ui/widgets/SubTaskProgressBar.cpp @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PrismLaucher - Minecraft Launcher + * Copyright (C) 2022 Rachel Powers <508861+Ryex@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 "SubTaskProgressBar.h" +#include "ui_SubTaskProgressBar.h" + +unique_qobject_ptr<SubTaskProgressBar> SubTaskProgressBar::create(QWidget* parent) +{ + auto progress_bar = new SubTaskProgressBar(parent); + return unique_qobject_ptr<SubTaskProgressBar>(progress_bar); +} + +SubTaskProgressBar::SubTaskProgressBar(QWidget* parent) + : ui(new Ui::SubTaskProgressBar) +{ + ui->setupUi(this); +} +SubTaskProgressBar::~SubTaskProgressBar() +{ + delete ui; +} + +void SubTaskProgressBar::setRange(int min, int max) +{ + ui->progressBar->setRange(min, max); +} + +void SubTaskProgressBar::setValue(int value) +{ + ui->progressBar->setValue(value); +} + +void SubTaskProgressBar::setStatus(QString status) +{ + ui->statusLabel->setText(status); +} + +void SubTaskProgressBar::setDetails(QString details) +{ + ui->statusDetailsLabel->setText(details); +} + diff --git a/launcher/ui/widgets/SubTaskProgressBar.h b/launcher/ui/widgets/SubTaskProgressBar.h new file mode 100644 index 00000000..8f8aeea2 --- /dev/null +++ b/launcher/ui/widgets/SubTaskProgressBar.h @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PrismLaucher - Minecraft Launcher + * Copyright (C) 2022 Rachel Powers <508861+Ryex@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 <QWidget> +#include "QObjectPtr.h" + +namespace Ui { +class SubTaskProgressBar; +} + +class SubTaskProgressBar : public QWidget +{ + Q_OBJECT + +public: + static unique_qobject_ptr<SubTaskProgressBar> create(QWidget* parent = nullptr); + + SubTaskProgressBar(QWidget* parent = nullptr); + ~SubTaskProgressBar(); + + void setRange(int min, int max); + void setValue(int value); + void setStatus(QString status); + void setDetails(QString details); + + + +private: + Ui::SubTaskProgressBar* ui; + +}; diff --git a/launcher/ui/widgets/SubTaskProgressBar.ui b/launcher/ui/widgets/SubTaskProgressBar.ui new file mode 100644 index 00000000..5431eab6 --- /dev/null +++ b/launcher/ui/widgets/SubTaskProgressBar.ui @@ -0,0 +1,94 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>SubTaskProgressBar</class> + <widget class="QWidget" name="SubTaskProgressBar"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>312</width> + <height>86</height> + </rect> + </property> + <property name="sizePolicy"> + <sizepolicy hsizetype="MinimumExpanding" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="windowTitle"> + <string>Form</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout" stretch="0,0"> + <property name="spacing"> + <number>0</number> + </property> + <item> + <layout class="QHBoxLayout" name="horizontalLayout" stretch="1,0"> + <property name="spacing"> + <number>8</number> + </property> + <item> + <widget class="QLabel" name="statusLabel"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Preferred" vsizetype="Preferred"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="font"> + <font> + <pointsize>8</pointsize> + </font> + </property> + <property name="text"> + <string>Sub Task Status...</string> + </property> + <property name="wordWrap"> + <bool>true</bool> + </property> + </widget> + </item> + <item> + <widget class="QLabel" name="statusDetailsLabel"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Preferred" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="font"> + <font> + <pointsize>8</pointsize> + </font> + </property> + <property name="text"> + <string>Status Details</string> + </property> + <property name="alignment"> + <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set> + </property> + </widget> + </item> + </layout> + </item> + <item> + <widget class="QProgressBar" name="progressBar"> + <property name="font"> + <font> + <pointsize>8</pointsize> + </font> + </property> + <property name="value"> + <number>24</number> + </property> + <property name="textVisible"> + <bool>true</bool> + </property> + </widget> + </item> + </layout> + </widget> + <resources/> + <connections/> +</ui> diff --git a/launcher/ui/widgets/ThemeCustomizationWidget.cpp b/launcher/ui/widgets/ThemeCustomizationWidget.cpp new file mode 100644 index 00000000..dcf13303 --- /dev/null +++ b/launcher/ui/widgets/ThemeCustomizationWidget.cpp @@ -0,0 +1,150 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * 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 + * 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 "ThemeCustomizationWidget.h" +#include "ui_ThemeCustomizationWidget.h" + +#include "Application.h" +#include "ui/themes/ITheme.h" +#include "ui/themes/ThemeManager.h" + +ThemeCustomizationWidget::ThemeCustomizationWidget(QWidget *parent) : QWidget(parent), ui(new Ui::ThemeCustomizationWidget) +{ + ui->setupUi(this); + loadSettings(); + + connect(ui->iconsComboBox, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &ThemeCustomizationWidget::applyIconTheme); + connect(ui->widgetStyleComboBox, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &ThemeCustomizationWidget::applyWidgetTheme); + connect(ui->backgroundCatComboBox, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &ThemeCustomizationWidget::applyCatTheme); +} + +ThemeCustomizationWidget::~ThemeCustomizationWidget() +{ + delete ui; +} + +/// <summary> +/// The layout was not quite right, so currently this just disables the UI elements, which should be hidden instead +/// TODO FIXME +/// +/// Original Method One: +/// ui->iconsComboBox->setVisible(features& ThemeFields::ICONS); +/// ui->iconsLabel->setVisible(features& ThemeFields::ICONS); +/// ui->widgetStyleComboBox->setVisible(features& ThemeFields::WIDGETS); +/// ui->widgetThemeLabel->setVisible(features& ThemeFields::WIDGETS); +/// ui->backgroundCatComboBox->setVisible(features& ThemeFields::CAT); +/// ui->backgroundCatLabel->setVisible(features& ThemeFields::CAT); +/// +/// original Method Two: +/// if (!(features & ThemeFields::ICONS)) { +/// ui->formLayout->setRowVisible(0, false); +/// } +/// if (!(features & ThemeFields::WIDGETS)) { +/// ui->formLayout->setRowVisible(1, false); +/// } +/// if (!(features & ThemeFields::CAT)) { +/// ui->formLayout->setRowVisible(2, false); +/// } +/// </summary> +/// <param name="features"></param> +void ThemeCustomizationWidget::showFeatures(ThemeFields features) { + ui->iconsComboBox->setEnabled(features & ThemeFields::ICONS); + ui->iconsLabel->setEnabled(features & ThemeFields::ICONS); + ui->widgetStyleComboBox->setEnabled(features & ThemeFields::WIDGETS); + ui->widgetThemeLabel->setEnabled(features & ThemeFields::WIDGETS); + ui->backgroundCatComboBox->setEnabled(features & ThemeFields::CAT); + ui->backgroundCatLabel->setEnabled(features & ThemeFields::CAT); +} + +void ThemeCustomizationWidget::applyIconTheme(int index) { + auto settings = APPLICATION->settings(); + auto originalIconTheme = settings->get("IconTheme").toString(); + auto& newIconTheme = m_iconThemeOptions[index].first; + settings->set("IconTheme", newIconTheme); + + if (originalIconTheme != newIconTheme) { + APPLICATION->applyCurrentlySelectedTheme(); + } + + emit currentIconThemeChanged(index); +} + +void ThemeCustomizationWidget::applyWidgetTheme(int index) { + auto settings = APPLICATION->settings(); + auto originalAppTheme = settings->get("ApplicationTheme").toString(); + auto newAppTheme = ui->widgetStyleComboBox->currentData().toString(); + if (originalAppTheme != newAppTheme) { + settings->set("ApplicationTheme", newAppTheme); + APPLICATION->applyCurrentlySelectedTheme(); + } + + emit currentWidgetThemeChanged(index); +} + +void ThemeCustomizationWidget::applyCatTheme(int index) { + auto settings = APPLICATION->settings(); + settings->set("BackgroundCat", m_catOptions[index].first); + + emit currentCatChanged(index); +} + +void ThemeCustomizationWidget::applySettings() +{ + applyIconTheme(ui->iconsComboBox->currentIndex()); + applyWidgetTheme(ui->widgetStyleComboBox->currentIndex()); + applyCatTheme(ui->backgroundCatComboBox->currentIndex()); +} +void ThemeCustomizationWidget::loadSettings() +{ + auto settings = APPLICATION->settings(); + + auto iconTheme = settings->get("IconTheme").toString(); + for (auto& iconThemeFromList : m_iconThemeOptions) { + QIcon iconForComboBox = QIcon(QString(":/icons/%1/scalable/settings").arg(iconThemeFromList.first)); + ui->iconsComboBox->addItem(iconForComboBox, iconThemeFromList.second); + if (iconTheme == iconThemeFromList.first) { + ui->iconsComboBox->setCurrentIndex(ui->iconsComboBox->count() - 1); + } + } + + { + auto currentTheme = settings->get("ApplicationTheme").toString(); + auto themes = APPLICATION->getValidApplicationThemes(); + int idx = 0; + for (auto& theme : themes) { + ui->widgetStyleComboBox->addItem(theme->name(), theme->id()); + if (currentTheme == theme->id()) { + ui->widgetStyleComboBox->setCurrentIndex(idx); + } + idx++; + } + } + + auto cat = settings->get("BackgroundCat").toString(); + for (auto& catFromList : m_catOptions) { + QIcon catIcon = QIcon(QString(":/backgrounds/%1").arg(ThemeManager::getCatImage(catFromList.first))); + ui->backgroundCatComboBox->addItem(catIcon, catFromList.second); + if (cat == catFromList.first) { + ui->backgroundCatComboBox->setCurrentIndex(ui->backgroundCatComboBox->count() - 1); + } + } +} + +void ThemeCustomizationWidget::retranslate() +{ + ui->retranslateUi(this); +} diff --git a/launcher/ui/widgets/ThemeCustomizationWidget.h b/launcher/ui/widgets/ThemeCustomizationWidget.h new file mode 100644 index 00000000..d955a266 --- /dev/null +++ b/launcher/ui/widgets/ThemeCustomizationWidget.h @@ -0,0 +1,77 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * 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 + * 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 <QWidget> +#include "translations/TranslationsModel.h" + +enum ThemeFields { NONE = 0b0000, ICONS = 0b0001, WIDGETS = 0b0010, CAT = 0b0100 }; + +namespace Ui { +class ThemeCustomizationWidget; +} + +class ThemeCustomizationWidget : public QWidget { + Q_OBJECT + + public: + explicit ThemeCustomizationWidget(QWidget* parent = nullptr); + ~ThemeCustomizationWidget(); + + void showFeatures(ThemeFields features); + + void applySettings(); + + void loadSettings(); + void retranslate(); + + private slots: + void applyIconTheme(int index); + void applyWidgetTheme(int index); + void applyCatTheme(int index); + + signals: + int currentIconThemeChanged(int index); + int currentWidgetThemeChanged(int index); + int currentCatChanged(int index); + + private: + Ui::ThemeCustomizationWidget* ui; + + //TODO finish implementing + QList<std::pair<QString, QString>> m_iconThemeOptions{ + { "pe_colored", QObject::tr("Simple (Colored Icons)") }, + { "pe_light", QObject::tr("Simple (Light Icons)") }, + { "pe_dark", QObject::tr("Simple (Dark Icons)") }, + { "pe_blue", QObject::tr("Simple (Blue Icons)") }, + { "breeze_light", QObject::tr("Breeze Light") }, + { "breeze_dark", QObject::tr("Breeze Dark") }, + { "OSX", QObject::tr("OSX") }, + { "iOS", QObject::tr("iOS") }, + { "flat", QObject::tr("Flat") }, + { "flat_white", QObject::tr("Flat (White)") }, + { "multimc", QObject::tr("Legacy") }, + { "custom", QObject::tr("Custom") } + }; + QList<std::pair<QString, QString>> m_catOptions{ + { "kitteh", QObject::tr("Background Cat (from MultiMC)") }, + { "rory", QObject::tr("Rory ID 11 (drawn by Ashtaka)") }, + { "rory-flat", QObject::tr("Rory ID 11 (flat edition, drawn by Ashtaka)") }, + { "teawie", QObject::tr("Teawie (drawn by SympathyTea)") } + }; +}; diff --git a/launcher/ui/widgets/ThemeCustomizationWidget.ui b/launcher/ui/widgets/ThemeCustomizationWidget.ui new file mode 100644 index 00000000..f216a610 --- /dev/null +++ b/launcher/ui/widgets/ThemeCustomizationWidget.ui @@ -0,0 +1,132 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>ThemeCustomizationWidget</class> + <widget class="QWidget" name="ThemeCustomizationWidget"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>400</width> + <height>191</height> + </rect> + </property> + <property name="windowTitle"> + <string notr="true">Form</string> + </property> + <layout class="QFormLayout" name="formLayout"> + <property name="sizeConstraint"> + <enum>QLayout::SetMinimumSize</enum> + </property> + <property name="leftMargin"> + <number>0</number> + </property> + <property name="topMargin"> + <number>0</number> + </property> + <property name="rightMargin"> + <number>0</number> + </property> + <property name="bottomMargin"> + <number>0</number> + </property> + <item row="0" column="0"> + <widget class="QLabel" name="iconsLabel"> + <property name="text"> + <string>&Icons</string> + </property> + <property name="buddy"> + <cstring>iconsComboBox</cstring> + </property> + </widget> + </item> + <item row="0" column="1"> + <widget class="QComboBox" name="iconsComboBox"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="focusPolicy"> + <enum>Qt::StrongFocus</enum> + </property> + </widget> + </item> + <item row="1" column="0"> + <widget class="QLabel" name="widgetThemeLabel"> + <property name="text"> + <string>&Colors</string> + </property> + <property name="buddy"> + <cstring>widgetStyleComboBox</cstring> + </property> + </widget> + </item> + <item row="1" column="1"> + <widget class="QComboBox" name="widgetStyleComboBox"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="focusPolicy"> + <enum>Qt::StrongFocus</enum> + </property> + </widget> + </item> + <item row="2" column="0"> + <widget class="QLabel" name="backgroundCatLabel"> + <property name="toolTip"> + <string>The cat appears in the background and is not shown by default. It is only made visible when pressing the Cat button in the Toolbar.</string> + </property> + <property name="text"> + <string>C&at</string> + </property> + <property name="buddy"> + <cstring>backgroundCatComboBox</cstring> + </property> + </widget> + </item> + <item row="2" column="1"> + <layout class="QHBoxLayout" name="horizontalLayout"> + <item> + <widget class="QComboBox" name="backgroundCatComboBox"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="focusPolicy"> + <enum>Qt::StrongFocus</enum> + </property> + <property name="toolTip"> + <string>The cat appears in the background and is not shown by default. It is only made visible when pressing the Cat button in the Toolbar.</string> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="catInfoLabel"> + <property name="toolTip"> + <string>The cat appears in the background and is not shown by default. It is only made visible when pressing the Cat button in the Toolbar.</string> + </property> + <property name="text"> + <string/> + </property> + <property name="icon"> + <iconset theme="about"> + <normaloff>.</normaloff>.</iconset> + </property> + <property name="flat"> + <bool>true</bool> + </property> + </widget> + </item> + </layout> + </item> + </layout> + </widget> + <resources/> + <connections/> +</ui> diff --git a/launcher/ui/widgets/WideBar.cpp b/launcher/ui/widgets/WideBar.cpp index 428be563..ac34e3aa 100644 --- a/launcher/ui/widgets/WideBar.cpp +++ b/launcher/ui/widgets/WideBar.cpp @@ -7,12 +7,20 @@ class ActionButton : public QToolButton { Q_OBJECT public: - ActionButton(QAction* action, QWidget* parent = nullptr) : QToolButton(parent), m_action(action) + ActionButton(QAction* action, QWidget* parent = nullptr, bool use_default_action = false) : QToolButton(parent), + m_action(action), m_use_default_action(use_default_action) { setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); - + setToolButtonStyle(Qt::ToolButtonTextBesideIcon); + // workaround for breeze and breeze forks + setProperty("_kde_toolButton_alignment", Qt::AlignLeft); + + if (m_use_default_action) { + setDefaultAction(action); + } else { + connect(this, &ActionButton::clicked, action, &QAction::trigger); + } connect(action, &QAction::changed, this, &ActionButton::actionChanged); - connect(this, &ActionButton::clicked, action, &QAction::trigger); actionChanged(); }; @@ -20,17 +28,24 @@ class ActionButton : public QToolButton { void actionChanged() { setEnabled(m_action->isEnabled()); - setChecked(m_action->isChecked()); - setCheckable(m_action->isCheckable()); - setText(m_action->text()); - setIcon(m_action->icon()); - setToolTip(m_action->toolTip()); - setHidden(!m_action->isVisible()); + // better pop up mode + if (m_action->menu()) { + setPopupMode(QToolButton::MenuButtonPopup); + } + if (!m_use_default_action) { + setChecked(m_action->isChecked()); + setCheckable(m_action->isCheckable()); + setText(m_action->text()); + setIcon(m_action->icon()); + setToolTip(m_action->toolTip()); + setHidden(!m_action->isVisible()); + } setFocusPolicy(Qt::NoFocus); } private: QAction* m_action; + bool m_use_default_action; }; WideBar::WideBar(const QString& title, QWidget* parent) : QToolBar(title, parent) @@ -54,7 +69,7 @@ WideBar::WideBar(QWidget* parent) : QToolBar(parent) void WideBar::addAction(QAction* action) { BarEntry entry; - entry.bar_action = addWidget(new ActionButton(action, this)); + entry.bar_action = addWidget(new ActionButton(action, this, m_use_default_action)); entry.menu_action = action; entry.type = BarEntry::Type::Action; @@ -86,7 +101,7 @@ void WideBar::insertActionBefore(QAction* before, QAction* action) return; BarEntry entry; - entry.bar_action = insertWidget(iter->bar_action, new ActionButton(action, this)); + entry.bar_action = insertWidget(iter->bar_action, new ActionButton(action, this, m_use_default_action)); entry.menu_action = action; entry.type = BarEntry::Type::Action; @@ -102,7 +117,7 @@ void WideBar::insertActionAfter(QAction* after, QAction* action) return; BarEntry entry; - entry.bar_action = insertWidget((iter + 1)->bar_action, new ActionButton(action, this)); + entry.bar_action = insertWidget((iter + 1)->bar_action, new ActionButton(action, this, m_use_default_action)); entry.menu_action = action; entry.type = BarEntry::Type::Action; @@ -111,6 +126,15 @@ void WideBar::insertActionAfter(QAction* after, QAction* action) m_menu_state = MenuState::Dirty; } +void WideBar::insertWidgetBefore(QAction* before, QWidget* widget) +{ + auto iter = getMatching(before); + if (iter == m_entries.end()) + return; + + insertWidget(iter->bar_action, widget); +} + void WideBar::insertSpacer(QAction* action) { auto iter = getMatching(action); @@ -133,7 +157,7 @@ void WideBar::insertSeparator(QAction* before) return; BarEntry entry; - entry.bar_action = QToolBar::insertSeparator(before); + entry.bar_action = QToolBar::insertSeparator(iter->bar_action); entry.type = BarEntry::Type::Separator; m_entries.insert(iter, entry); @@ -180,6 +204,10 @@ void WideBar::showVisibilityMenu(QPoint const& position) m_bar_menu->clear(); + m_bar_menu->addActions(m_context_menu_actions); + + m_bar_menu->addSeparator()->setText(tr("Customize toolbar actions")); + for (auto& entry : m_entries) { if (entry.type != BarEntry::Type::Action) continue; @@ -206,6 +234,10 @@ void WideBar::showVisibilityMenu(QPoint const& position) m_bar_menu->popup(mapToGlobal(position)); } +void WideBar::addContextMenuAction(QAction* action) { + m_context_menu_actions.append(action); +} + [[nodiscard]] QByteArray WideBar::getVisibilityState() const { QByteArray state; diff --git a/launcher/ui/widgets/WideBar.h b/launcher/ui/widgets/WideBar.h index a0a7896c..c47f3a59 100644 --- a/launcher/ui/widgets/WideBar.h +++ b/launcher/ui/widgets/WideBar.h @@ -9,6 +9,9 @@ class WideBar : public QToolBar { Q_OBJECT + // Why: so we can enable / disable alt shortcuts in toolbuttons + // with toolbuttons using setDefaultAction, theres no alt shortcuts + Q_PROPERTY(bool useDefaultAction MEMBER m_use_default_action) public: explicit WideBar(const QString& title, QWidget* parent = nullptr); @@ -22,10 +25,13 @@ class WideBar : public QToolBar { void insertSeparator(QAction* before); void insertActionBefore(QAction* before, QAction* action); void insertActionAfter(QAction* after, QAction* action); + void insertWidgetBefore(QAction* before, QWidget* widget); QMenu* createContextMenu(QWidget* parent = nullptr, const QString& title = QString()); void showVisibilityMenu(const QPoint&); + void addContextMenuAction(QAction* action); + // Ideally we would use a QBitArray for this, but it doesn't support string conversion, // so using it in settings is very messy. @@ -48,6 +54,10 @@ class WideBar : public QToolBar { private: QList<BarEntry> m_entries; + QList<QAction*> m_context_menu_actions; + + bool m_use_default_action = false; + // Menu to toggle visibility from buttons in the bar std::unique_ptr<QMenu> m_bar_menu = nullptr; enum class MenuState { Fresh, Dirty } m_menu_state = MenuState::Dirty; |