diff --git a/cmake/MacOSXBundleInfo.plist.in b/cmake/MacOSXBundleInfo.plist.in
index eb40bacfd..1035efec3 100644
--- a/cmake/MacOSXBundleInfo.plist.in
+++ b/cmake/MacOSXBundleInfo.plist.in
@@ -85,6 +85,14 @@
curseforge
+
+ CFBundleURLName
+ Modrinth
+ CFBundleURLSchemes
+
+ modrinth
+
+
CFBundleURLName
${Launcher_Name}
diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt
index 235ba2975..a516c2ddf 100644
--- a/launcher/CMakeLists.txt
+++ b/launcher/CMakeLists.txt
@@ -561,6 +561,8 @@ set(MODRINTH_SOURCES
modplatform/modrinth/ModrinthInstanceCreationTask.h
modplatform/modrinth/ModrinthPackExportTask.cpp
modplatform/modrinth/ModrinthPackExportTask.h
+ modplatform/modrinth/ModrinthUrl.cpp
+ modplatform/modrinth/ModrinthUrl.h
)
set(PACKWIZ_SOURCES
diff --git a/launcher/modplatform/modrinth/ModrinthUrl.cpp b/launcher/modplatform/modrinth/ModrinthUrl.cpp
new file mode 100644
index 000000000..003aa6dd3
--- /dev/null
+++ b/launcher/modplatform/modrinth/ModrinthUrl.cpp
@@ -0,0 +1,45 @@
+// SPDX-License-Identifier: GPL-3.0-only
+/*
+ * Prism Launcher - Minecraft Launcher
+ *
+ * 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 .
+ */
+
+#include "ModrinthUrl.h"
+
+namespace Modrinth {
+
+auto parseModpackLink(const QUrl& url) -> std::optional
+{
+ if (url.scheme().compare("modrinth", Qt::CaseInsensitive) != 0) {
+ return std::nullopt;
+ }
+
+ if (url.host().compare("modpack", Qt::CaseInsensitive) != 0) {
+ return std::nullopt;
+ }
+
+ const auto segments = QUrl::fromPercentEncoding(url.path().toUtf8()).split('/', Qt::SkipEmptyParts);
+ if (segments.size() != 1) {
+ return std::nullopt;
+ }
+
+ const auto slug = segments.constFirst().trimmed();
+ if (slug.isEmpty()) {
+ return std::nullopt;
+ }
+
+ return ParsedModpackLink{ slug };
+}
+
+} // namespace Modrinth
diff --git a/launcher/modplatform/modrinth/ModrinthUrl.h b/launcher/modplatform/modrinth/ModrinthUrl.h
new file mode 100644
index 000000000..daf39fa45
--- /dev/null
+++ b/launcher/modplatform/modrinth/ModrinthUrl.h
@@ -0,0 +1,32 @@
+// SPDX-License-Identifier: GPL-3.0-only
+/*
+ * Prism Launcher - Minecraft Launcher
+ *
+ * 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 .
+ */
+
+#pragma once
+
+#include
+
+#include
+
+namespace Modrinth {
+
+struct ParsedModpackLink {
+ QString slug;
+};
+
+auto parseModpackLink(const QUrl& url) -> std::optional;
+
+} // namespace Modrinth
diff --git a/launcher/ui/MainWindow.cpp b/launcher/ui/MainWindow.cpp
index 1bdcd3f68..b057f5e9d 100644
--- a/launcher/ui/MainWindow.cpp
+++ b/launcher/ui/MainWindow.cpp
@@ -125,6 +125,7 @@
#include "modplatform/ModIndex.h"
#include "modplatform/flame/FlameAPI.h"
#include "modplatform/flame/FlameModIndex.h"
+#include "modplatform/modrinth/ModrinthUrl.h"
#include "KonamiCode.h"
@@ -896,7 +897,7 @@ void MainWindow::on_actionCopyInstance_triggered()
runModalTask(task.get());
}
-void MainWindow::addInstance(const QString& url, const QMap& extra_info)
+void MainWindow::addInstance(const QString& url, const QMap& extra_info, const QString& modrinthSlug)
{
QString groupName;
do {
@@ -917,6 +918,9 @@ void MainWindow::addInstance(const QString& url, const QMap& e
}
NewInstanceDialog newInstDlg(groupName, url, extra_info, this);
+ if (!modrinthSlug.isEmpty()) {
+ newInstDlg.openModrinthModpack(modrinthSlug);
+ }
if (!newInstDlg.exec())
return;
@@ -950,6 +954,19 @@ void MainWindow::processURLs(QList urls)
QMap extra_info;
QUrl local_url;
if (!url.isLocalFile()) { // download the remote resource and identify
+ if (url.scheme().compare("modrinth", Qt::CaseInsensitive) == 0) {
+ if (auto modpackLink = Modrinth::parseModpackLink(url)) {
+ addInstance(QString(), {}, modpackLink->slug);
+ } else {
+ CustomMessageBox::selectable(
+ this, tr("Error"),
+ tr("Unsupported Modrinth link.\n\nPrism Launcher currently only supports modpack links such as "
+ "modrinth://modpack/fabulously-optimized."),
+ QMessageBox::Critical)
+ ->show();
+ }
+ continue;
+ }
const bool isExternalURLImport = (url.host().toLower() == "import") || (url.path().startsWith("/import", Qt::CaseInsensitive));
diff --git a/launcher/ui/MainWindow.h b/launcher/ui/MainWindow.h
index 80860deef..776e6bf90 100644
--- a/launcher/ui/MainWindow.h
+++ b/launcher/ui/MainWindow.h
@@ -221,7 +221,8 @@ class MainWindow : public QMainWindow {
private:
void retranslateUi();
- void addInstance(const QString& url = QString(), const QMap& extra_info = {});
+ void addInstance(const QString& url = QString(), const QMap& extra_info = {},
+ const QString& modrinthSlug = QString());
void activateInstance(BaseInstance* instance);
void setCatBackground(bool enabled);
void updateInstanceToolIcon(QString new_icon);
diff --git a/launcher/ui/dialogs/NewInstanceDialog.cpp b/launcher/ui/dialogs/NewInstanceDialog.cpp
index 8cf094527..8e41500b2 100644
--- a/launcher/ui/dialogs/NewInstanceDialog.cpp
+++ b/launcher/ui/dialogs/NewInstanceDialog.cpp
@@ -250,6 +250,23 @@ void NewInstanceDialog::setSuggestedIcon(const QString& key)
ui->iconButton->setIcon(icon);
}
+void NewInstanceDialog::openModrinthModpack(const QString& slug)
+{
+ auto trimmedSlug = slug.trimmed();
+ if (trimmedSlug.isEmpty()) {
+ return;
+ }
+
+ auto* page = dynamic_cast(m_container->getPage("modrinth"));
+ if (!page) {
+ return;
+ }
+
+ m_searchTerm = trimmedSlug;
+ page->openProjectBySlug(trimmedSlug);
+ m_container->selectPage(page->id());
+}
+
InstanceTask* NewInstanceDialog::extractTask()
{
InstanceTask* extracted = creationTask.release();
diff --git a/launcher/ui/dialogs/NewInstanceDialog.h b/launcher/ui/dialogs/NewInstanceDialog.h
index e97c9f543..b08ed92b7 100644
--- a/launcher/ui/dialogs/NewInstanceDialog.h
+++ b/launcher/ui/dialogs/NewInstanceDialog.h
@@ -65,6 +65,7 @@ class NewInstanceDialog : public QDialog, public BasePageProvider {
void setSuggestedPack(const QString& name, QString version, InstanceTask* task = nullptr);
void setSuggestedIconFromFile(const QString& path, const QString& name);
void setSuggestedIcon(const QString& key);
+ void openModrinthModpack(const QString& slug);
InstanceTask* extractTask();
diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp
index 4798583bd..a92e6bb6b 100644
--- a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp
+++ b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp
@@ -56,6 +56,7 @@
#include
#include
#include
+#include
ModrinthPage::ModrinthPage(NewInstanceDialog* dialog, QWidget* parent)
: QWidget(parent), m_ui(new Ui::ModrinthPage), m_dialog(dialog), m_fetch_progress(this, false)
@@ -328,6 +329,44 @@ void ModrinthPage::suggestCurrent()
}
}
+void ModrinthPage::openProjectBySlug(QString slug)
+{
+ m_requestedProjectSlug = std::move(slug).trimmed();
+
+ if (m_requestedProjectSlug.isEmpty()) {
+ return;
+ }
+
+ setSearchTerm(m_requestedProjectSlug);
+}
+
+void ModrinthPage::selectRequestedProject()
+{
+ if (m_requestedProjectSlug.isEmpty()) {
+ return;
+ }
+
+ const auto requestedSlug = m_requestedProjectSlug;
+ m_requestedProjectSlug.clear();
+
+ for (int row = 0; row < m_model->rowCount({}); row++) {
+ const auto index = m_model->index(row, 0);
+ const auto pack = m_model->data(index, Qt::UserRole).value();
+
+ if (pack && pack->slug.compare(requestedSlug, Qt::CaseInsensitive) == 0) {
+ m_ui->packView->setCurrentIndex(index);
+ m_ui->packView->scrollTo(index);
+ return;
+ }
+ }
+
+ if (m_model->rowCount({}) == 1) {
+ const auto index = m_model->index(0, 0);
+ m_ui->packView->setCurrentIndex(index);
+ m_ui->packView->scrollTo(index);
+ }
+}
+
void ModrinthPage::triggerSearch()
{
m_ui->packView->selectionModel()->setCurrentIndex({}, QItemSelectionModel::SelectionFlag::ClearAndSelect);
@@ -335,8 +374,17 @@ void ModrinthPage::triggerSearch()
m_ui->packDescription->clear();
m_ui->versionSelectionBox->clear();
bool filterChanged = m_filterWidget->changed();
- m_model->searchWithTerm(m_ui->searchEdit->text(), m_ui->sortByBox->currentIndex(), m_filterWidget->getFilter(), filterChanged);
+ const auto searchTerm = m_requestedProjectSlug.isEmpty() ? m_ui->searchEdit->text() : "#" + m_requestedProjectSlug;
+ m_model->searchWithTerm(searchTerm, m_ui->sortByBox->currentIndex(), m_filterWidget->getFilter(), filterChanged);
m_fetch_progress.watch(m_model->activeSearchJob().get());
+
+ if (!m_requestedProjectSlug.isEmpty()) {
+ if (m_model->hasActiveSearchJob()) {
+ connect(m_model->activeSearchJob().get(), &Task::finished, this, [this] { selectRequestedProject(); });
+ } else {
+ selectRequestedProject();
+ }
+ }
}
void ModrinthPage::onVersionSelectionChanged(int index)
diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h
index 4ca41a3e0..264e92313 100644
--- a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h
+++ b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h
@@ -72,6 +72,7 @@ class ModrinthPage : public QWidget, public ModpackProviderBasePage {
ModPlatform::IndexedPack::Ptr getCurrent() { return m_current; }
void suggestCurrent();
+ void openProjectBySlug(QString slug);
void updateUI();
@@ -91,12 +92,15 @@ class ModrinthPage : public QWidget, public ModpackProviderBasePage {
void createFilterWidget();
private:
+ void selectRequestedProject();
+
Ui::ModrinthPage* m_ui;
NewInstanceDialog* m_dialog;
Modrinth::ModpackListModel* m_model;
ModPlatform::IndexedPack::Ptr m_current;
QString m_selectedVersion;
+ QString m_requestedProjectSlug;
ProgressWidget m_fetch_progress;
diff --git a/program_info/org.prismlauncher.PrismLauncher.desktop.in b/program_info/org.prismlauncher.PrismLauncher.desktop.in
index 416ca1b6e..98037a78e 100644
--- a/program_info/org.prismlauncher.PrismLauncher.desktop.in
+++ b/program_info/org.prismlauncher.PrismLauncher.desktop.in
@@ -10,4 +10,4 @@ Icon=@Launcher_AppID@
Categories=Game;ActionGame;AdventureGame;Simulation;PackageManager;
Keywords=game;minecraft;mc;
StartupWMClass=@Launcher_CommonName@
-MimeType=application/zip;application/x-modrinth-modpack+zip;x-scheme-handler/curseforge;x-scheme-handler/prismlauncher;x-scheme-handler/@Launcher_APP_BINARY_NAME@;
+MimeType=application/zip;application/x-modrinth-modpack+zip;x-scheme-handler/curseforge;x-scheme-handler/modrinth;x-scheme-handler/prismlauncher;x-scheme-handler/@Launcher_APP_BINARY_NAME@;
diff --git a/program_info/win_install.nsi.in b/program_info/win_install.nsi.in
index 83335ce9d..05431156f 100644
--- a/program_info/win_install.nsi.in
+++ b/program_info/win_install.nsi.in
@@ -394,6 +394,10 @@ Section "@Launcher_DisplayName@"
WriteRegStr HKCU Software\Classes\curseforge "URL Protocol" ""
WriteRegStr HKCU Software\Classes\curseforge\shell\open\command "" '"$INSTDIR\@Launcher_APP_BINARY_NAME@.exe" "%1"'
+ ; Write the URL Handler into registry for modrinth
+ WriteRegStr HKCU Software\Classes\modrinth "URL Protocol" ""
+ WriteRegStr HKCU Software\Classes\modrinth\shell\open\command "" '"$INSTDIR\@Launcher_APP_BINARY_NAME@.exe" "%1"'
+
; Write the URL Handler into registry for prismlauncher
WriteRegStr HKCU Software\Classes\@Launcher_APP_BINARY_NAME@ "URL Protocol" ""
WriteRegStr HKCU Software\Classes\@Launcher_APP_BINARY_NAME@\shell\open\command "" '"$INSTDIR\@Launcher_APP_BINARY_NAME@.exe" "%1"'
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
index 2165cd03d..d04fe2045 100644
--- a/tests/CMakeLists.txt
+++ b/tests/CMakeLists.txt
@@ -54,6 +54,9 @@ ecm_add_test(Index_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}:
ecm_add_test(Version_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test
TEST_NAME Version)
+ecm_add_test(ModrinthUrl_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test
+ TEST_NAME ModrinthUrl)
+
ecm_add_test(MetaComponentParse_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test
TEST_NAME MetaComponentParse)
diff --git a/tests/ModrinthUrl_test.cpp b/tests/ModrinthUrl_test.cpp
new file mode 100644
index 000000000..967fb1872
--- /dev/null
+++ b/tests/ModrinthUrl_test.cpp
@@ -0,0 +1,49 @@
+#include
+
+#include
+
+class ModrinthUrlTest : public QObject {
+ Q_OBJECT
+
+ private slots:
+ void parseModpackLink_data()
+ {
+ QTest::addColumn("link");
+ QTest::addColumn("slug");
+
+ QTest::newRow("official") << "modrinth://modpack/fabulously-optimized" << "fabulously-optimized";
+ QTest::newRow("trailing slash") << "modrinth://modpack/fabulously-optimized/" << "fabulously-optimized";
+ QTest::newRow("case insensitive host") << "modrinth://MODPACK/fo" << "fo";
+ }
+
+ void parseModpackLink()
+ {
+ QFETCH(QString, link);
+ QFETCH(QString, slug);
+
+ auto parsed = Modrinth::parseModpackLink(QUrl(link));
+ QVERIFY(parsed.has_value());
+ QCOMPARE(parsed->slug, slug);
+ }
+
+ void rejectInvalidLinks_data()
+ {
+ QTest::addColumn("link");
+
+ QTest::newRow("wrong scheme") << "https://modrinth.com/modpack/fabulously-optimized";
+ QTest::newRow("missing slug") << "modrinth://modpack/";
+ QTest::newRow("unsupported resource") << "modrinth://mod/sodium";
+ QTest::newRow("extra path") << "modrinth://modpack/fabulously-optimized/version/latest";
+ }
+
+ void rejectInvalidLinks()
+ {
+ QFETCH(QString, link);
+
+ QVERIFY(!Modrinth::parseModpackLink(QUrl(link)).has_value());
+ }
+};
+
+QTEST_GUILESS_MAIN(ModrinthUrlTest)
+
+#include "ModrinthUrl_test.moc"