aboutsummaryrefslogtreecommitdiff
path: root/launcher/ui
diff options
context:
space:
mode:
Diffstat (limited to 'launcher/ui')
-rw-r--r--launcher/ui/GuiUtil.cpp1
-rw-r--r--launcher/ui/MainWindow.cpp1118
-rw-r--r--launcher/ui/MainWindow.h40
-rw-r--r--launcher/ui/MainWindow.ui690
-rw-r--r--launcher/ui/dialogs/AboutDialog.cpp6
-rw-r--r--launcher/ui/dialogs/BlockedModsDialog.cpp8
-rw-r--r--launcher/ui/dialogs/ChooseProviderDialog.cpp6
-rw-r--r--launcher/ui/dialogs/ChooseProviderDialog.h6
-rw-r--r--launcher/ui/dialogs/ExportInstanceDialog.cpp1
-rw-r--r--launcher/ui/dialogs/ImportResourceDialog.cpp (renamed from launcher/ui/dialogs/ImportResourcePackDialog.cpp)19
-rw-r--r--launcher/ui/dialogs/ImportResourceDialog.h30
-rw-r--r--launcher/ui/dialogs/ImportResourceDialog.ui (renamed from launcher/ui/dialogs/ImportResourcePackDialog.ui)17
-rw-r--r--launcher/ui/dialogs/ImportResourcePackDialog.h27
-rw-r--r--launcher/ui/dialogs/ModDownloadDialog.cpp202
-rw-r--r--launcher/ui/dialogs/ModDownloadDialog.h78
-rw-r--r--launcher/ui/dialogs/ModUpdateDialog.cpp77
-rw-r--r--launcher/ui/dialogs/ModUpdateDialog.h14
-rw-r--r--launcher/ui/dialogs/ResourceDownloadDialog.cpp232
-rw-r--r--launcher/ui/dialogs/ResourceDownloadDialog.h111
-rw-r--r--launcher/ui/dialogs/ReviewMessageBox.cpp28
-rw-r--r--launcher/ui/dialogs/ReviewMessageBox.h11
-rw-r--r--launcher/ui/dialogs/ReviewMessageBox.ui14
-rw-r--r--launcher/ui/dialogs/UpdateDialog.cpp217
-rw-r--r--launcher/ui/dialogs/UpdateDialog.h67
-rw-r--r--launcher/ui/dialogs/UpdateDialog.ui91
-rw-r--r--launcher/ui/pages/global/LauncherPage.cpp122
-rw-r--r--launcher/ui/pages/global/LauncherPage.h12
-rw-r--r--launcher/ui/pages/global/LauncherPage.ui28
-rw-r--r--launcher/ui/pages/instance/InstanceSettingsPage.cpp76
-rw-r--r--launcher/ui/pages/instance/InstanceSettingsPage.h14
-rw-r--r--launcher/ui/pages/instance/InstanceSettingsPage.ui42
-rw-r--r--launcher/ui/pages/instance/LogPage.cpp2
-rw-r--r--launcher/ui/pages/instance/ManagedPackPage.cpp8
-rw-r--r--launcher/ui/pages/instance/ManagedPackPage.h4
-rw-r--r--launcher/ui/pages/instance/ModFolderPage.cpp8
-rw-r--r--launcher/ui/pages/instance/ResourcePackPage.h1
-rw-r--r--launcher/ui/pages/instance/ScreenshotsPage.h1
-rw-r--r--launcher/ui/pages/instance/ServersPage.cpp1
-rw-r--r--launcher/ui/pages/instance/VersionPage.cpp2
-rw-r--r--launcher/ui/pages/instance/VersionPage.h2
-rw-r--r--launcher/ui/pages/instance/WorldListPage.cpp2
-rw-r--r--launcher/ui/pages/modplatform/ModModel.cpp348
-rw-r--r--launcher/ui/pages/modplatform/ModModel.h94
-rw-r--r--launcher/ui/pages/modplatform/ModPage.cpp366
-rw-r--r--launcher/ui/pages/modplatform/ModPage.h97
-rw-r--r--launcher/ui/pages/modplatform/ResourceModel.cpp444
-rw-r--r--launcher/ui/pages/modplatform/ResourceModel.h147
-rw-r--r--launcher/ui/pages/modplatform/ResourcePage.cpp411
-rw-r--r--launcher/ui/pages/modplatform/ResourcePage.h111
-rw-r--r--launcher/ui/pages/modplatform/ResourcePage.ui (renamed from launcher/ui/pages/modplatform/ModPage.ui)18
-rw-r--r--launcher/ui/pages/modplatform/atlauncher/AtlListModel.cpp6
-rw-r--r--launcher/ui/pages/modplatform/flame/FlameModModel.cpp31
-rw-r--r--launcher/ui/pages/modplatform/flame/FlameModModel.h26
-rw-r--r--launcher/ui/pages/modplatform/flame/FlameModel.cpp6
-rw-r--r--launcher/ui/pages/modplatform/flame/FlameResourceModels.cpp37
-rw-r--r--launcher/ui/pages/modplatform/flame/FlameResourceModels.h30
-rw-r--r--launcher/ui/pages/modplatform/flame/FlameResourcePages.cpp (renamed from launcher/ui/pages/modplatform/flame/FlameModPage.cpp)44
-rw-r--r--launcher/ui/pages/modplatform/flame/FlameResourcePages.h (renamed from launcher/ui/pages/modplatform/flame/FlameModPage.h)43
-rw-r--r--launcher/ui/pages/modplatform/ftb/FtbListModel.cpp18
-rw-r--r--launcher/ui/pages/modplatform/ftb/FtbPage.cpp5
-rw-r--r--launcher/ui/pages/modplatform/legacy_ftb/ListModel.cpp2
-rw-r--r--launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp7
-rw-r--r--launcher/ui/pages/modplatform/modrinth/ModrinthModel.h1
-rw-r--r--launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp6
-rw-r--r--launcher/ui/pages/modplatform/modrinth/ModrinthResourceModels.cpp (renamed from launcher/ui/pages/modplatform/modrinth/ModrinthModModel.cpp)26
-rw-r--r--launcher/ui/pages/modplatform/modrinth/ModrinthResourceModels.h (renamed from launcher/ui/pages/modplatform/modrinth/ModrinthModModel.h)26
-rw-r--r--launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.cpp (renamed from launcher/ui/pages/modplatform/modrinth/ModrinthModPage.cpp)60
-rw-r--r--launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.h (renamed from launcher/ui/pages/modplatform/modrinth/ModrinthModPage.h)41
-rw-r--r--launcher/ui/pages/modplatform/technic/TechnicModel.cpp6
-rw-r--r--launcher/ui/pages/modplatform/technic/TechnicPage.cpp8
-rw-r--r--launcher/ui/widgets/ProgressWidget.cpp11
-rw-r--r--launcher/ui/widgets/ProgressWidget.h6
-rw-r--r--launcher/ui/widgets/WideBar.cpp58
-rw-r--r--launcher/ui/widgets/WideBar.h10
74 files changed, 3105 insertions, 2881 deletions
diff --git a/launcher/ui/GuiUtil.cpp b/launcher/ui/GuiUtil.cpp
index 29467c3c..930e088a 100644
--- a/launcher/ui/GuiUtil.cpp
+++ b/launcher/ui/GuiUtil.cpp
@@ -40,6 +40,7 @@
#include <QClipboard>
#include <QApplication>
#include <QFileDialog>
+#include <QStandardPaths>
#include "ui/dialogs/ProgressDialog.h"
#include "ui/dialogs/CustomMessageBox.h"
diff --git a/launcher/ui/MainWindow.cpp b/launcher/ui/MainWindow.cpp
index ab80fb80..6d21f5ed 100644
--- a/launcher/ui/MainWindow.cpp
+++ b/launcher/ui/MainWindow.cpp
@@ -43,6 +43,7 @@
#include "FileSystem.h"
#include "MainWindow.h"
+#include "ui_MainWindow.h"
#include <QVariant>
#include <QUrl>
@@ -86,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"
@@ -102,23 +102,19 @@
#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/WorldList.h"
-#include "UpdateController.h"
#include "KonamiCode.h"
#include "InstanceImportTask.h"
@@ -140,785 +136,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();
-
- instanceToolBar->addAction(actionLaunchInstance);
- instanceToolBar->addAction(actionKillInstance);
-
- instanceToolBar->addSeparator();
+ ui->instanceToolBar->insertSeparator(ui->actionLaunchInstance);
- instanceToolBar->addAction(actionEditInstance);
- instanceToolBar->addAction(actionChangeInstGroup);
-
- instanceToolBar->addAction(actionViewSelectedInstFolder);
-
- instanceToolBar->addAction(actionExportInstance);
- instanceToolBar->addAction(actionCopyInstance);
- instanceToolBar->addAction(actionDeleteInstance);
+ // 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(actionCreateInstanceShortcut); // TODO find better position for this
+ ui->instanceToolBar->setVisibilityState(instanceToolbarSetting->get().toByteArray());
- 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);
- }
- }
+ ui->instanceToolBar->addContextMenuAction(ui->newsToolBar->toggleViewAction());
+ ui->instanceToolBar->addContextMenuAction(ui->instanceToolBar->toggleViewAction());
+ ui->instanceToolBar->addContextMenuAction(ui->actionLockToolbars);
- 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
@@ -936,6 +259,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();
@@ -955,7 +279,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());
@@ -971,10 +295,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);
}
@@ -1011,25 +337,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...
@@ -1061,35 +375,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());
@@ -1117,10 +417,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) {
@@ -1130,6 +430,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()
@@ -1169,13 +483,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);
@@ -1231,8 +548,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);
@@ -1240,7 +555,6 @@ void MainWindow::updateToolsMenu()
ui->actionLaunchInstanceDemo->setDisabled(!m_selectedInstance || currentInstanceRunning);
QMenu *launchMenu = ui->actionLaunchInstance->menu();
- launchButton->setPopupMode(QToolButton::MenuButtonPopup);
if (launchMenu)
{
launchMenu->clear();
@@ -1249,7 +563,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"));
@@ -1357,8 +670,7 @@ void MainWindow::updateThemeMenu()
void MainWindow::repopulateAccountsMenu()
{
- accountMenu->clear();
- ui->profileMenu->clear();
+ ui->accountsMenu->clear();
auto accounts = APPLICATION->accounts();
MinecraftAccountPtr defaultAccount = accounts->defaultAccount();
@@ -1367,23 +679,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
{
@@ -1414,37 +720,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)
@@ -1488,20 +779,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)
@@ -1563,32 +854,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)
@@ -1613,40 +878,6 @@ 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);
@@ -1789,10 +1020,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");
@@ -1802,31 +1035,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;
+ }
}
}
@@ -1857,7 +1109,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);
}
}
@@ -1867,7 +1119,7 @@ void MainWindow::iconUpdated(QString icon)
{
auto icon = APPLICATION->icons()->getIcon(m_currentInstIcon);
ui->actionChangeInstIcon->setIcon(icon);
- ui->changeIconButton->setIcon(icon);
+ changeIconButton->setIcon(icon);
}
}
@@ -1876,7 +1128,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)
@@ -1962,8 +1214,7 @@ void MainWindow::checkForUpdates()
{
if(BuildConfig.UPDATER_ENABLED)
{
- auto updater = APPLICATION->updateChecker();
- updater->checkForUpdate(APPLICATION->settings()->get("UpdateChannel").toString(), true);
+ APPLICATION->triggerUpdateCheck();
}
else
{
@@ -2056,6 +1307,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);
@@ -2120,6 +1375,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();
}
@@ -2353,7 +1609,7 @@ void MainWindow::instanceChanged(const QModelIndex &current, 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());
@@ -2366,7 +1622,7 @@ void MainWindow::instanceChanged(const QModelIndex &current, 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());
@@ -2380,7 +1636,7 @@ void MainWindow::instanceChanged(const QModelIndex &current, const QModelIndex &
else
{
ui->instanceToolBar->setEnabled(false);
- ui->setInstanceActionsEnabled(false);
+ setInstanceActionsEnabled(false);
ui->actionLaunchInstance->setEnabled(false);
ui->actionLaunchInstanceOffline->setEnabled(false);
ui->actionLaunchInstanceDemo->setEnabled(false);
@@ -2413,9 +1669,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
@@ -2470,6 +1726,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>&amp;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>&amp;Edit</string>
+ </property>
+ <property name="toolTipsVisible">
+ <bool>true</bool>
+ </property>
+ <addaction name="actionUndoTrashInstance"/>
+ </widget>
+ <widget class="QMenu" name="viewMenu">
+ <property name="title">
+ <string>&amp;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&amp;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>&amp;Accounts</string>
+ </property>
+ </widget>
+ <widget class="QMenu" name="helpMenu">
+ <property name="title">
+ <string>&amp;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>&amp;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>&amp;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&amp;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>&amp;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&amp;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>&amp;Manage Accounts...</string>
+ </property>
+ </action>
+ <action name="actionLaunchInstance">
+ <property name="icon">
+ <iconset theme="launch">
+ <normaloff>.</normaloff>.</iconset>
+ </property>
+ <property name="text">
+ <string>&amp;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>&amp;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>&amp;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>&amp;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>&amp;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&amp;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&amp;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 &amp;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 &amp;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&amp;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 &amp;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>&amp;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 &amp;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 &amp;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>&amp;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>&amp;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&amp;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>&amp;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>&amp;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 &amp;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 &amp;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/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("&lt;<a href='%1'>%2</a>&gt;").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..eb427953 100644
--- a/launcher/ui/dialogs/BlockedModsDialog.cpp
+++ b/launcher/ui/dialogs/BlockedModsDialog.cpp
@@ -32,10 +32,14 @@
#include <QDebug>
#include <QDesktopServices>
#include <QDialogButtonBox>
+#include <QDir>
+#include <QDirIterator>
#include <QDragEnterEvent>
#include <QFileDialog>
#include <QFileInfo>
+#include <QMimeData>
#include <QPushButton>
+#include <QMimeData>
#include <QStandardPaths>
BlockedModsDialog::BlockedModsDialog(QWidget* parent, const QString& title, const QString& text, QList<BlockedMod>& mods)
@@ -88,7 +92,7 @@ void BlockedModsDialog::dropEvent(QDropEvent* e)
if (url.scheme().isEmpty()) { // ensure isLocalFile() works correctly
url.setScheme("file");
}
-
+
if (!url.isLocalFile()) { // can't drop external files here.
continue;
}
@@ -230,7 +234,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;
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/ExportInstanceDialog.cpp b/launcher/ui/dialogs/ExportInstanceDialog.cpp
index 88552b23..f13e36e8 100644
--- a/launcher/ui/dialogs/ExportInstanceDialog.cpp
+++ b/launcher/ui/dialogs/ExportInstanceDialog.cpp
@@ -44,6 +44,7 @@
#include <QSortFilterProxyModel>
#include <QDebug>
#include <QSaveFile>
+#include <QStack>
#include "StringUtils.h"
#include "SeparatorPrefixTree.h"
#include "Application.h"
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/ResourceDownloadDialog.cpp b/launcher/ui/dialogs/ResourceDownloadDialog.cpp
new file mode 100644
index 00000000..fa829bfb
--- /dev/null
+++ b/launcher/ui/dialogs/ResourceDownloadDialog.cpp
@@ -0,0 +1,232 @@
+// 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 "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);
+ 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;
+}
+
+} // namespace ResourceDownload
diff --git a/launcher/ui/dialogs/ResourceDownloadDialog.h b/launcher/ui/dialogs/ResourceDownloadDialog.h
new file mode 100644
index 00000000..19843532
--- /dev/null
+++ b/launcher/ui/dialogs/ResourceDownloadDialog.h
@@ -0,0 +1,111 @@
+// 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;
+
+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;
+};
+
+} // 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/LauncherPage.cpp b/launcher/ui/pages/global/LauncherPage.cpp
index 69a8e3df..a9f44c0b 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);
+ ui->updateSettingsBox->setHidden(!APPLICATION->updater());
- if (APPLICATION->updateChecker()->hasChannels())
- {
- refreshUpdateChannelList();
- }
- else
- {
- APPLICATION->updateChecker()->updateChanList(false);
- }
-
- 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()
@@ -198,94 +177,15 @@ void LauncherPage::on_metadataDisableBtn_clicked()
ui->metadataWarningLabel->setHidden(!ui->metadataDisableBtn->isChecked());
}
-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++)
- {
- 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;
- }
- }
-
- 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()
-{
- // 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;
- }
-}
-
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());
+ APPLICATION->updater()->setAutomaticallyChecksForUpdates(ui->autoUpdateCheckBox->isChecked());
}
- else
- {
- s->set("AutoUpdate", ui->autoUpdateCheckBox->isChecked());
- }
-
- s->set("UpdateChannel", m_currentUpdateChannel);
s->set("MenuBarInsteadOfToolBar", ui->preferMenuBarCheckBox->isChecked());
@@ -324,17 +224,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();
// Toolbar/menu bar settings (not applicable if native menu bar is present)
ui->toolsBox->setEnabled(!QMenuBar().isNativeMenuBar());
diff --git a/launcher/ui/pages/global/LauncherPage.h b/launcher/ui/pages/global/LauncherPage.h
index f38c922e..c60156c2 100644
--- a/launcher/ui/pages/global/LauncherPage.h
+++ b/launcher/ui/pages/global/LauncherPage.h
@@ -91,22 +91,10 @@ slots:
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 65f4a9d5..f084d970 100644
--- a/launcher/ui/pages/global/LauncherPage.ui
+++ b/launcher/ui/pages/global/LauncherPage.ui
@@ -58,33 +58,6 @@
</property>
</widget>
</item>
- <item>
- <widget class="QLabel" name="updateChannelLabel">
- <property name="text">
- <string>Up&amp;date Channel:</string>
- </property>
- <property name="buddy">
- <cstring>updateChannelComboBox</cstring>
- </property>
- </widget>
- </item>
- <item>
- <widget class="QComboBox" name="updateChannelComboBox">
- <property name="enabled">
- <bool>false</bool>
- </property>
- </widget>
- </item>
- <item>
- <widget class="QLabel" name="updateChannelDescLabel">
- <property name="text">
- <string>No channel selected.</string>
- </property>
- <property name="wordWrap">
- <bool>true</bool>
- </property>
- </widget>
- </item>
</layout>
</widget>
</item>
@@ -440,7 +413,6 @@
<tabstops>
<tabstop>tabWidget</tabstop>
<tabstop>autoUpdateCheckBox</tabstop>
- <tabstop>updateChannelComboBox</tabstop>
<tabstop>instDirTextBox</tabstop>
<tabstop>instDirBrowseBtn</tabstop>
<tabstop>modsDirTextBox</tabstop>
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 8f9e569e..639cd711 100644
--- a/launcher/ui/pages/instance/LogPage.cpp
+++ b/launcher/ui/pages/instance/LogPage.cpp
@@ -40,7 +40,7 @@
#include "Application.h"
-#include <QIcon>
+#include <QIdentityProxyModel>
#include <QScrollBar>
#include <QShortcut>
diff --git a/launcher/ui/pages/instance/ManagedPackPage.cpp b/launcher/ui/pages/instance/ManagedPackPage.cpp
index 4de80468..dc983d9a 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"
@@ -263,8 +262,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 627e71e5..d9069915 100644
--- a/launcher/ui/pages/instance/ModFolderPage.cpp
+++ b/launcher/ui/pages/instance/ModFolderPage.cpp
@@ -49,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"
@@ -59,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"
@@ -153,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) {
diff --git a/launcher/ui/pages/instance/ResourcePackPage.h b/launcher/ui/pages/instance/ResourcePackPage.h
index 9633e3b4..db8af0c5 100644
--- a/launcher/ui/pages/instance/ResourcePackPage.h
+++ b/launcher/ui/pages/instance/ResourcePackPage.h
@@ -73,3 +73,4 @@ public:
return true;
}
};
+
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 6f8591a1..4b1fa08a 100644
--- a/launcher/ui/pages/instance/ServersPage.cpp
+++ b/launcher/ui/pages/instance/ServersPage.cpp
@@ -50,6 +50,7 @@
#include <QFileSystemWatcher>
#include <QMenu>
+#include <QTimer>
static const int COLUMN_COUNT = 2; // 3 , TBD: latency and other nice things.
diff --git a/launcher/ui/pages/instance/VersionPage.cpp b/launcher/ui/pages/instance/VersionPage.cpp
index f92a7660..7fff3b93 100644
--- a/launcher/ui/pages/instance/VersionPage.cpp
+++ b/launcher/ui/pages/instance/VersionPage.cpp
@@ -678,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)
diff --git a/launcher/ui/pages/instance/VersionPage.h b/launcher/ui/pages/instance/VersionPage.h
index ca98dfd1..d0087714 100644
--- a/launcher/ui/pages/instance/VersionPage.h
+++ b/launcher/ui/pages/instance/VersionPage.h
@@ -104,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/WorldListPage.cpp b/launcher/ui/pages/instance/WorldListPage.cpp
index 0020c461..d4a395d9 100644
--- a/launcher/ui/pages/instance/WorldListPage.cpp
+++ b/launcher/ui/pages/instance/WorldListPage.cpp
@@ -45,9 +45,9 @@
#include <QKeyEvent>
#include <QClipboard>
#include <QMessageBox>
+#include <QSortFilterProxyModel>
#include <QTreeView>
#include <QInputDialog>
-#include <QProcess>
#include <Qt>
#include "tools/MCEditTool.h"
diff --git a/launcher/ui/pages/modplatform/ModModel.cpp b/launcher/ui/pages/modplatform/ModModel.cpp
index ed58eb32..3ffe6cb0 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 75be25b2..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();
+ m_ui->packView->clearSelection();
+ m_ui->packDescription->clear();
+ m_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)); }
-
- 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()
+void ModPage::updateVersionList()
{
- ui->retranslateUi(this);
-}
-
-void ModPage::updateModVersions(int prev_count)
-{
- 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,92 +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;
-
- // hoedown bug: it doesn't handle markdown surrounded by block tags (like center, div) so strip them
- current.extraData.body.remove(QRegularExpression("<[^>]*(?:center|div)\\W*>"));
-
- 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..c3b58cd6 100644
--- a/launcher/ui/pages/modplatform/ModPage.h
+++ b/launcher/ui/pages/modplatform/ModPage.h
@@ -1,105 +1,76 @@
+// 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;
+ ~ModPage() override = default;
- /* 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"); }
+ [[nodiscard]] QMap<QString, QString> urlHandlers() const override;
- void retranslate() override;
+ void addResourceToDialog(ModPlatform::IndexedPack&, ModPlatform::IndexedVersion&) override;
- void updateUi();
+ virtual auto validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, std::optional<ResourceAPI::ModLoaderTypes> loaders = {}) const -> bool = 0;
- 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; };
-
- 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..db7d26f8
--- /dev/null
+++ b/launcher/ui/pages/modplatform/ResourceModel.cpp
@@ -0,0 +1,444 @@
+// 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 "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] = 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 = ptr;
+ 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> 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 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..610b631c
--- /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 ddocumentToArray, 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> 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/ResourcePage.cpp b/launcher/ui/pages/modplatform/ResourcePage.cpp
new file mode 100644
index 00000000..bbd465bc
--- /dev/null
+++ b/launcher/ui/pages/modplatform/ResourcePage.cpp
@@ -0,0 +1,411 @@
+// 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;
+}
+
+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/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/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..de1f2122
--- /dev/null
+++ b/launcher/ui/pages/modplatform/flame/FlameResourceModels.cpp
@@ -0,0 +1,37 @@
+// 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");
+}
+
+} // 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..625a2a7d
--- /dev/null
+++ b/launcher/ui/pages/modplatform/flame/FlameResourceModels.h
@@ -0,0 +1,30 @@
+// 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/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;
+};
+
+} // namespace ResourceDownload
diff --git a/launcher/ui/pages/modplatform/flame/FlameModPage.cpp b/launcher/ui/pages/modplatform/flame/FlameResourcePages.cpp
index bad78c97..485431a7 100644
--- a/launcher/ui/pages/modplatform/flame/FlameModPage.cpp
+++ b/launcher/ui/pages/modplatform/flame/FlameResourcePages.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>
@@ -34,37 +36,33 @@
* limitations under the License.
*/
-#include "FlameModPage.h"
-#include "ui_ModPage.h"
+#include "FlameResourcePages.h"
+#include "ui_ResourcePage.h"
-#include "FlameModModel.h"
-#include "ui/dialogs/ModDownloadDialog.h"
+#include "FlameResourceModels.h"
+#include "ui/dialogs/ResourceDownloadDialog.h"
-FlameModPage::FlameModPage(ModDownloadDialog* dialog, BaseInstance* instance)
- : ModPage(dialog, instance, new FlameAPI())
+namespace ResourceDownload {
+
+FlameModPage::FlameModPage(ModDownloadDialog* dialog, BaseInstance& instance)
+ : ModPage(dialog, instance)
{
- listModel = new FlameMod::ListModel(this);
- ui->packView->setModel(listModel);
+ m_model = new FlameModModel(instance);
+ m_ui->packView->setModel(m_model);
- // 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"));
+ 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(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);
+ 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);
- ui->packDescription->setMetaEntry(metaEntryBase());
+ m_ui->packDescription->setMetaEntry(metaEntryBase());
}
-auto FlameModPage::validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, ModAPI::ModLoaderTypes loaders) const -> bool
+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();
@@ -95,3 +93,5 @@ void FlameModPage::openUrl(const QUrl& url)
ModPage::openUrl(url);
}
+
+} // namespace ResourceDownload
diff --git a/launcher/ui/pages/modplatform/flame/FlameModPage.h b/launcher/ui/pages/modplatform/flame/FlameResourcePages.h
index 58479ab9..b21a53ad 100644
--- a/launcher/ui/pages/modplatform/flame/FlameModPage.h
+++ b/launcher/ui/pages/modplatform/flame/FlameResourcePages.h
@@ -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>
@@ -36,35 +38,48 @@
#pragma once
-#include "modplatform/ModAPI.h"
+#include "Application.h"
+
+#include "modplatform/ResourceAPI.h"
+
#include "ui/pages/modplatform/ModPage.h"
-#include "modplatform/flame/FlameAPI.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)
+ static FlameModPage* create(ModDownloadDialog* dialog, BaseInstance& instance)
{
return ModPage::create<FlameModPage>(dialog, instance);
}
- FlameModPage(ModDownloadDialog* dialog, BaseInstance* 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"; }
+ [[nodiscard]] bool shouldDisplay() const override;
- inline auto debugName() const -> QString override { return "Flame"; }
- inline auto metaEntryBase() const -> QString override { return "FlameMods"; };
+ [[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(); }
- auto validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, ModAPI::ModLoaderTypes loaders = ModAPI::Unspecified) const -> bool override;
- bool optedOut(ModPlatform::IndexedVersion& ver) const override;
+ [[nodiscard]] inline auto helpPage() const -> QString override { return "Mod-platform"; }
- auto shouldDisplay() const -> bool override;
+ 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;
};
+
+} // namespace ResourceDownload
diff --git a/launcher/ui/pages/modplatform/ftb/FtbListModel.cpp b/launcher/ui/pages/modplatform/ftb/FtbListModel.cpp
index ce2b2b18..e8065415 100644
--- a/launcher/ui/pages/modplatform/ftb/FtbListModel.cpp
+++ b/launcher/ui/pages/modplatform/ftb/FtbListModel.cpp
@@ -109,14 +109,14 @@ void ListModel::request()
modpacks.clear();
endResetModel();
- auto *netJob = new NetJob("Ftb::Request", APPLICATION->network());
+ auto netJob = makeShared<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);
+ QObject::connect(netJob.get(), &NetJob::succeeded, this, &ListModel::requestFinished);
+ QObject::connect(netJob.get(), &NetJob::failed, this, &ListModel::requestFailed);
}
void ListModel::abortRequest()
@@ -158,14 +158,14 @@ void ListModel::requestFailed(QString reason)
void ListModel::requestPack()
{
- auto *netJob = new NetJob("Ftb::Search", APPLICATION->network());
+ auto netJob = makeShared<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);
+ QObject::connect(netJob.get(), &NetJob::succeeded, this, &ListModel::packRequestFinished);
+ QObject::connect(netJob.get(), &NetJob::failed, this, &ListModel::packRequestFailed);
}
void ListModel::packRequestFinished()
@@ -281,16 +281,16 @@ void ListModel::requestLogo(QString logo, QString url)
bool stale = entry->isStale();
- NetJob *job = new NetJob(QString("ModpacksCH Icon Download %1").arg(logo), APPLICATION->network());
+ auto job = makeShared<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]
+ QObject::connect(job.get(), &NetJob::finished, this, [this, logo, fullPath, stale]
{
logoLoaded(logo, stale);
});
- QObject::connect(job, &NetJob::failed, this, [this, logo]
+ QObject::connect(job.get(), &NetJob::failed, this, [this, logo]
{
logoFailed(logo);
});
diff --git a/launcher/ui/pages/modplatform/ftb/FtbPage.cpp b/launcher/ui/pages/modplatform/ftb/FtbPage.cpp
index b08f3bc4..7d59a6ae 100644
--- a/launcher/ui/pages/modplatform/ftb/FtbPage.cpp
+++ b/launcher/ui/pages/modplatform/ftb/FtbPage.cpp
@@ -43,7 +43,7 @@
#include "ui/dialogs/NewInstanceDialog.h"
#include "modplatform/modpacksch/FTBPackInstallTask.h"
-#include "HoeDown.h"
+#include "Markdown.h"
FtbPage::FtbPage(NewInstanceDialog* dialog, QWidget *parent)
: QWidget(parent), ui(new Ui::FtbPage), dialog(dialog)
@@ -175,8 +175,7 @@ void FtbPage::onSelectionChanged(QModelIndex first, QModelIndex second)
selected = filterModel->data(first, Qt::UserRole).value<ModpacksCH::Modpack>();
- HoeDown hoedown;
- QString output = hoedown.process(selected.description.toUtf8());
+ QString output = markdownToHTML(selected.description.toUtf8());
ui->packDescription->setHtml(output);
// reverse foreach, so that the newest versions are first
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/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/ModrinthModModel.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthResourceModels.cpp
index af92e63e..73d55133 100644
--- a/launcher/ui/pages/modplatform/modrinth/ModrinthModModel.cpp
+++ b/launcher/ui/pages/modplatform/modrinth/ModrinthResourceModels.cpp
@@ -1,3 +1,5 @@
+// SPDX-FileCopyrightText: 2023 flowln <flowlnlnln@gmail.com>
+//
// SPDX-License-Identifier: GPL-3.0-only
/*
* PolyMC - Minecraft Launcher
@@ -16,33 +18,33 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-#include "ModrinthModModel.h"
+#include "ModrinthResourceModels.h"
+#include "modplatform/modrinth/ModrinthAPI.h"
#include "modplatform/modrinth/ModrinthPackIndex.h"
-namespace Modrinth {
+namespace ResourceDownload {
-// NOLINTNEXTLINE(modernize-avoid-c-arrays)
-const char* ListModel::sorts[5]{ "relevance", "downloads", "follows", "updated", "newest" };
+ModrinthModModel::ModrinthModModel(BaseInstance const& base) : ModModel(base, new ModrinthAPI) {}
-void ListModel::loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj)
+void ModrinthModModel::loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj)
{
- Modrinth::loadIndexedPack(m, obj);
+ ::Modrinth::loadIndexedPack(m, obj);
}
-void ListModel::loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj)
+void ModrinthModModel::loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj)
{
- Modrinth::loadExtraPackData(m, obj);
+ ::Modrinth::loadExtraPackData(m, obj);
}
-void ListModel::loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr)
+void ModrinthModModel::loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr)
{
- Modrinth::loadIndexedPackVersions(m, arr, APPLICATION->network(), m_parent->m_instance);
+ ::Modrinth::loadIndexedPackVersions(m, arr, APPLICATION->network(), &m_base_instance);
}
-auto ListModel::documentToArray(QJsonDocument& obj) const -> QJsonArray
+auto ModrinthModModel::documentToArray(QJsonDocument& obj) const -> QJsonArray
{
return obj.object().value("hits").toArray();
}
-} // namespace Modrinth
+} // namespace ResourceDownload
diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthModModel.h b/launcher/ui/pages/modplatform/modrinth/ModrinthResourceModels.h
index 386897fd..56cab146 100644
--- a/launcher/ui/pages/modplatform/modrinth/ModrinthModModel.h
+++ b/launcher/ui/pages/modplatform/modrinth/ModrinthResourceModels.h
@@ -1,3 +1,5 @@
+// SPDX-FileCopyrightText: 2023 flowln <flowlnlnln@gmail.com>
+//
// SPDX-License-Identifier: GPL-3.0-only
/*
* PolyMC - Minecraft Launcher
@@ -18,27 +20,29 @@
#pragma once
-#include "ModrinthModPage.h"
+#include "ui/pages/modplatform/ModModel.h"
+#include "ui/pages/modplatform/modrinth/ModrinthResourcePages.h"
-namespace Modrinth {
+namespace ResourceDownload {
-class ListModel : public ModPlatform::ListModel {
+class ModrinthModPage;
+
+class ModrinthModModel : public ModModel {
Q_OBJECT
public:
- ListModel(ModrinthModPage* parent) : ModPlatform::ListModel(parent){};
- ~ListModel() override = default;
+ 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;
- // NOLINTNEXTLINE(modernize-avoid-c-arrays)
- static const char* sorts[5];
- inline auto getSorts() const -> const char** override { return sorts; };
+ auto documentToArray(QJsonDocument& obj) const -> QJsonArray override;
};
-} // namespace Modrinth
+} // namespace ResourceDownload
diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthModPage.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.cpp
index c531ea90..b82f800e 100644
--- a/launcher/ui/pages/modplatform/modrinth/ModrinthModPage.cpp
+++ b/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.cpp
@@ -1,3 +1,5 @@
+// SPDX-FileCopyrightText: 2023 flowln <flowlnlnln@gmail.com>
+//
// SPDX-License-Identifier: GPL-3.0-only
/*
* PolyMC - Minecraft Launcher
@@ -33,48 +35,50 @@
* limitations under the License.
*/
-#include "ModrinthModPage.h"
+#include "ModrinthResourcePages.h"
+#include "ui_ResourcePage.h"
+
#include "modplatform/modrinth/ModrinthAPI.h"
-#include "ui_ModPage.h"
-#include "ModrinthModModel.h"
-#include "ui/dialogs/ModDownloadDialog.h"
+#include "ui/dialogs/ResourceDownloadDialog.h"
+
+#include "ui/pages/modplatform/modrinth/ModrinthResourceModels.h"
+
+namespace ResourceDownload {
-ModrinthModPage::ModrinthModPage(ModDownloadDialog* dialog, BaseInstance* instance)
- : ModPage(dialog, instance, new ModrinthAPI())
+ModrinthModPage::ModrinthModPage(ModDownloadDialog* dialog, BaseInstance& instance)
+ : ModPage(dialog, instance)
{
- listModel = new Modrinth::ListModel(this);
- ui->packView->setModel(listModel);
+ m_model = new ModrinthModModel(instance);
+ m_ui->packView->setModel(m_model);
- // 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"));
+ 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(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);
+ 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);
- ui->packDescription->setMetaEntry(metaEntryBase());
+ m_ui->packDescription->setMetaEntry(metaEntryBase());
}
-auto ModrinthModPage::validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, ModAPI::ModLoaderTypes loaders) const -> bool
+auto ModrinthModPage::validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, std::optional<ResourceAPI::ModLoaderTypes> loaders) const -> bool
{
- auto loaderStrings = ModrinthAPI::getModLoaderStrings(loaders);
+ auto loaderCompatible = !loaders.has_value();
- auto loaderCompatible = false;
- for (auto remoteLoader : ver.loaders)
- {
- if (loaderStrings.contains(remoteLoader)) {
- loaderCompatible = true;
- break;
+ 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;
}
@@ -82,3 +86,5 @@ auto ModrinthModPage::validateVersion(ModPlatform::IndexedVersion& ver, QString
// 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; }
+
+} // namespace ResourceDownload
diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthModPage.h b/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.h
index 40d82e6f..be38eff1 100644
--- a/launcher/ui/pages/modplatform/modrinth/ModrinthModPage.h
+++ b/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.h
@@ -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
/*
* PolyMC - Minecraft Launcher
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
@@ -35,32 +37,45 @@
#pragma once
-#include "modplatform/ModAPI.h"
+#include "Application.h"
+
+#include "modplatform/ResourceAPI.h"
+
#include "ui/pages/modplatform/ModPage.h"
-#include "modplatform/modrinth/ModrinthAPI.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)
+ static ModrinthModPage* create(ModDownloadDialog* dialog, BaseInstance& instance)
{
return ModPage::create<ModrinthModPage>(dialog, instance);
}
- ModrinthModPage(ModDownloadDialog* dialog, BaseInstance* 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"; }
+ [[nodiscard]] bool shouldDisplay() const override;
- inline auto debugName() const -> QString override { return "Modrinth"; }
- inline auto metaEntryBase() const -> QString override { return "ModrinthPacks"; };
+ [[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(); }
- auto validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, ModAPI::ModLoaderTypes loaders = ModAPI::Unspecified) const -> bool override;
+ [[nodiscard]] inline auto helpPage() const -> QString override { return "Mod-platform"; }
- auto shouldDisplay() const -> bool override;
+ auto validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, std::optional<ResourceAPI::ModLoaderTypes> loaders = {}) const -> bool override;
};
+
+} // 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/widgets/ProgressWidget.cpp b/launcher/ui/widgets/ProgressWidget.cpp
index b60d9a7a..f736af08 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;
@@ -54,14 +54,17 @@ void ProgressWidget::watch(Task* task)
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/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;