diff options
Diffstat (limited to 'archived/projt-launcher/launcher/ui/dialogs')
87 files changed, 15558 insertions, 0 deletions
diff --git a/archived/projt-launcher/launcher/ui/dialogs/AboutDialog.cpp b/archived/projt-launcher/launcher/ui/dialogs/AboutDialog.cpp new file mode 100644 index 0000000000..48c77aa492 --- /dev/null +++ b/archived/projt-launcher/launcher/ui/dialogs/AboutDialog.cpp @@ -0,0 +1,197 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * 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, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * + * + * Prism Launcher - 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, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * 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 "AboutDialog.h" +#include <QIcon> +#include "Application.h" +#include "BuildConfig.h" +#include "Markdown.h" +#include "ScrollMessageBox.h" +#include "StringUtils.h" +#include "ui_AboutDialog.h" + +#include <net/NetJob.h> +#include <qobject.h> +#include <QPixmap> + +namespace +{ + QString getCreditsHtml() + { + QFile dataFile(":/documents/credits.html"); + if (!dataFile.open(QIODevice::ReadOnly)) + { + qWarning() << "Failed to open file '" << dataFile.fileName() << "' for reading!"; + return QString(); + } + + QString fileContent = QString::fromUtf8(dataFile.readAll()); + + return fileContent.arg(QObject::tr("%1 Developers").arg(BuildConfig.LAUNCHER_DISPLAYNAME), + QObject::tr("Prism Launcher Developers"), + QObject::tr("MultiMC Developers"), + QObject::tr("With special thanks to")); + } + + QString getLicenseHtml() + { + QFile dataFile(":/documents/COPYING"); + if (dataFile.open(QIODevice::ReadOnly)) + { + QString output = markdownToHTML(dataFile.readAll()); + dataFile.close(); + return output; + } + else + { + qWarning() << "Failed to open file '" << dataFile.fileName() << "' for reading!"; + return QString(); + } + } + + QString getManifestoHtml() + { + QFile dataFile(":/documents/manifesto.md"); + if (dataFile.open(QIODevice::ReadOnly)) + { + QString output = markdownToHTML(dataFile.readAll()); + dataFile.close(); + return StringUtils::htmlListPatch(output); + } + else + { + qWarning() << "Failed to open file '" << dataFile.fileName() << "' for reading!"; + return QString(); + } + } + +} // namespace + +AboutDialog::AboutDialog(QWidget* parent) : QDialog(parent), ui(new Ui::AboutDialog) +{ + ui->setupUi(this); + + QString launcherName = BuildConfig.LAUNCHER_DISPLAYNAME; + + setWindowTitle(tr("About %1").arg(launcherName)); + + QString chtml = getCreditsHtml(); + ui->creditsText->setHtml(StringUtils::htmlListPatch(chtml)); + + QString lhtml = getLicenseHtml(); + ui->licenseText->setHtml(StringUtils::htmlListPatch(lhtml)); + + ui->urlLabel->setOpenExternalLinks(true); + + ui->icon->setPixmap(APPLICATION->logo().pixmap(64)); + ui->title->setText(launcherName); + + ui->versionLabel->setText(BuildConfig.printableVersionString()); + + if (!BuildConfig.BUILD_PLATFORM.isEmpty()) + ui->platformLabel->setText(tr("Platform") + ": " + BuildConfig.BUILD_PLATFORM); + else + ui->platformLabel->setVisible(false); + + if (!BuildConfig.GIT_COMMIT.isEmpty()) + ui->commitLabel->setText(tr("Commit: %1").arg(BuildConfig.GIT_COMMIT)); + else + ui->commitLabel->setVisible(false); + + if (!BuildConfig.BUILD_DATE.isEmpty()) + ui->buildDateLabel->setText(tr("Build date: %1").arg(BuildConfig.BUILD_DATE)); + else + ui->buildDateLabel->setVisible(false); + + if (!BuildConfig.VERSION_CHANNEL.isEmpty()) + ui->channelLabel->setText(tr("Channel") + ": " + BuildConfig.VERSION_CHANNEL); + else + ui->channelLabel->setVisible(false); + + QString urlText("<html><head/><body><p><a href=\"%1\">%1</a></p></body></html>"); + ui->urlLabel->setText(urlText.arg(BuildConfig.LAUNCHER_GIT)); + + ui->copyLabel->setText(BuildConfig.LAUNCHER_COPYRIGHT); + ui->licenseBadgeTextLabel->setText(tr("This project is licensed under")); + ui->licenseBadgeLabel->setPixmap(QPixmap(QStringLiteral(":/gplv3-127x51.png"))); + + connect(ui->closeButton, &QPushButton::clicked, this, &AboutDialog::close); + + const QString manifestoHtml = getManifestoHtml(); + if (!manifestoHtml.isEmpty()) + { + connect(ui->aboutProjectTick, + &QPushButton::clicked, + this, + [this, manifestoHtml] + { + ScrollMessageBox dialog(this, tr("About Project Tick"), tr("Project Tick Overview"), manifestoHtml); + dialog.exec(); + }); + } + else + { + ui->aboutProjectTick->setEnabled(false); + } +} + +AboutDialog::~AboutDialog() +{ + delete ui; +} diff --git a/archived/projt-launcher/launcher/ui/dialogs/AboutDialog.h b/archived/projt-launcher/launcher/ui/dialogs/AboutDialog.h new file mode 100644 index 0000000000..4ccf8acc54 --- /dev/null +++ b/archived/projt-launcher/launcher/ui/dialogs/AboutDialog.h @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0 +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * 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, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * 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> + +namespace Ui +{ + class AboutDialog; +} + +class AboutDialog : public QDialog +{ + Q_OBJECT + + public: + explicit AboutDialog(QWidget* parent = 0); + ~AboutDialog(); + + private: + Ui::AboutDialog* ui; +}; diff --git a/archived/projt-launcher/launcher/ui/dialogs/AboutDialog.ui b/archived/projt-launcher/launcher/ui/dialogs/AboutDialog.ui new file mode 100644 index 0000000000..a1ed29cbfc --- /dev/null +++ b/archived/projt-launcher/launcher/ui/dialogs/AboutDialog.ui @@ -0,0 +1,390 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>AboutDialog</class> + <widget class="QDialog" name="AboutDialog"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>573</width> + <height>600</height> + </rect> + </property> + <property name="minimumSize"> + <size> + <width>450</width> + <height>400</height> + </size> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <layout class="QHBoxLayout" name="horizontalLayout"> + <item> + <spacer name="horizontalSpacer"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item> + <widget class="QLabel" name="icon"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Fixed" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="minimumSize"> + <size> + <width>64</width> + <height>64</height> + </size> + </property> + <property name="baseSize"> + <size> + <width>64</width> + <height>64</height> + </size> + </property> + <property name="text"> + <string/> + </property> + </widget> + </item> + <item> + <spacer name="horizontalSpacer_2"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + </layout> + </item> + <item> + <widget class="QLabel" name="title"> + <property name="font"> + <font> + <pointsize>15</pointsize> + </font> + </property> + <property name="text"> + <string notr="true">Launcher</string> + </property> + <property name="alignment"> + <set>Qt::AlignCenter</set> + </property> + </widget> + </item> + <item alignment="Qt::AlignHCenter"> + <widget class="QLabel" name="versionLabel"> + <property name="cursor"> + <cursorShape>IBeamCursor</cursorShape> + </property> + <property name="textInteractionFlags"> + <set>Qt::TextSelectableByMouse</set> + </property> + </widget> + </item> + <item> + <widget class="QTabWidget" name="tabWidget"> + <property name="currentIndex"> + <number>0</number> + </property> + <widget class="QWidget" name="aboutTab"> + <attribute name="title"> + <string>About</string> + </attribute> + <layout class="QVBoxLayout" name="verticalLayout_5"> + <item> + <widget class="QLabel" name="aboutLabel"> + <property name="enabled"> + <bool>true</bool> + </property> + <property name="text"> + <string><html><head/><body><p>A custom launcher that makes managing Minecraft easier by allowing you to have multiple instances of Minecraft at once.</p></body></html></string> + </property> + <property name="alignment"> + <set>Qt::AlignCenter</set> + </property> + <property name="wordWrap"> + <bool>true</bool> + </property> + </widget> + </item> + <item> + <widget class="QLabel" name="urlLabel"> + <property name="font"> + <font> + <pointsize>10</pointsize> + </font> + </property> + <property name="text"> + <string notr="true">GIT URL</string> + </property> + <property name="alignment"> + <set>Qt::AlignCenter</set> + </property> + <property name="textInteractionFlags"> + <set>Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse</set> + </property> + </widget> + </item> + <item> + <widget class="QLabel" name="copyLabel"> + <property name="font"> + <font> + <pointsize>8</pointsize> + <kerning>true</kerning> + </font> + </property> + <property name="text"> + <string notr="true">COPYRIGHT</string> + </property> + <property name="alignment"> + <set>Qt::AlignCenter</set> + </property> + </widget> + </item> + <item> + <widget class="QLabel" name="licenseBadgeTextLabel"> + <property name="font"> + <font> + <pointsize>9</pointsize> + </font> + </property> + <property name="text"> + <string>This project is licensed under</string> + </property> + <property name="alignment"> + <set>Qt::AlignCenter</set> + </property> + </widget> + </item> + <item alignment="Qt::AlignHCenter"> + <widget class="QLabel" name="licenseBadgeLabel"> + <property name="minimumSize"> + <size> + <width>127</width> + <height>51</height> + </size> + </property> + <property name="maximumSize"> + <size> + <width>127</width> + <height>51</height> + </size> + </property> + <property name="text"> + <string/> + </property> + <property name="alignment"> + <set>Qt::AlignCenter</set> + </property> + </widget> + </item> + <item> + <widget class="Line" name="line"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + </widget> + </item> + <item alignment="Qt::AlignHCenter"> + <widget class="QLabel" name="platformLabel"> + <property name="cursor"> + <cursorShape>IBeamCursor</cursorShape> + </property> + <property name="text"> + <string>Platform:</string> + </property> + <property name="alignment"> + <set>Qt::AlignCenter</set> + </property> + <property name="textInteractionFlags"> + <set>Qt::TextSelectableByMouse</set> + </property> + </widget> + </item> + <item alignment="Qt::AlignHCenter"> + <widget class="QLabel" name="buildDateLabel"> + <property name="cursor"> + <cursorShape>IBeamCursor</cursorShape> + </property> + <property name="text"> + <string>Build Date:</string> + </property> + <property name="alignment"> + <set>Qt::AlignCenter</set> + </property> + <property name="textInteractionFlags"> + <set>Qt::TextSelectableByMouse</set> + </property> + </widget> + </item> + <item alignment="Qt::AlignHCenter"> + <widget class="QLabel" name="commitLabel"> + <property name="cursor"> + <cursorShape>IBeamCursor</cursorShape> + </property> + <property name="text"> + <string>Commit:</string> + </property> + <property name="alignment"> + <set>Qt::AlignCenter</set> + </property> + <property name="textInteractionFlags"> + <set>Qt::TextSelectableByMouse</set> + </property> + </widget> + </item> + <item alignment="Qt::AlignHCenter"> + <widget class="QLabel" name="channelLabel"> + <property name="cursor"> + <cursorShape>IBeamCursor</cursorShape> + </property> + <property name="text"> + <string>Channel:</string> + </property> + <property name="alignment"> + <set>Qt::AlignCenter</set> + </property> + <property name="textInteractionFlags"> + <set>Qt::TextSelectableByMouse</set> + </property> + </widget> + </item> + <item alignment="Qt::AlignHCenter"> + <widget class="QLabel" name="qtPoweredLabel"> + <property name="font"> + <font> + <pointsize>10</pointsize> + </font> + </property> + <property name="text"> + <string>ProjT Launcher is powered by the Qt framework.</string> + </property> + <property name="alignment"> + <set>Qt::AlignCenter</set> + </property> + <property name="wordWrap"> + <bool>false</bool> + </property> + </widget> + </item> + <item> + <spacer name="verticalSpacer"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>212</height> + </size> + </property> + </spacer> + </item> + </layout> + </widget> + <widget class="QWidget" name="creditsTab"> + <attribute name="title"> + <string>Credits</string> + </attribute> + <layout class="QVBoxLayout" name="verticalLayout_2"> + <item> + <widget class="QTextBrowser" name="creditsText"> + <property name="openExternalLinks"> + <bool>true</bool> + </property> + </widget> + </item> + </layout> + </widget> + <widget class="QWidget" name="licenseTab"> + <attribute name="title"> + <string>License</string> + </attribute> + <layout class="QVBoxLayout" name="verticalLayout_3"> + <item> + <widget class="QTextEdit" name="licenseText"> + <property name="minimumSize"> + <size> + <width>0</width> + <height>0</height> + </size> + </property> + <property name="font"> + <font> + <family>DejaVu Sans Mono</family> + </font> + </property> + <property name="readOnly"> + <bool>true</bool> + </property> + <property name="textInteractionFlags"> + <set>Qt::TextBrowserInteraction</set> + </property> + </widget> + </item> + </layout> + </widget> + </widget> + </item> + <item> + <layout class="QHBoxLayout" name="horizontalLayout_2"> + <item> + <widget class="QPushButton" name="aboutProjectTick"> + <property name="autoFillBackground"> + <bool>false</bool> + </property> + <property name="text"> + <string>About Project Tick</string> + </property> + </widget> + </item> + <item> + <spacer name="horizontalSpacer_3"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item> + <widget class="QPushButton" name="closeButton"> + <property name="text"> + <string>Close</string> + </property> + </widget> + </item> + </layout> + </item> + </layout> + </widget> + <tabstops> + <tabstop>tabWidget</tabstop> + <tabstop>creditsText</tabstop> + <tabstop>licenseText</tabstop> + <tabstop>aboutProjectTick</tabstop> + <tabstop>closeButton</tabstop> + </tabstops> + <resources/> + <connections/> +</ui> diff --git a/archived/projt-launcher/launcher/ui/dialogs/BackupDialog.cpp b/archived/projt-launcher/launcher/ui/dialogs/BackupDialog.cpp new file mode 100644 index 0000000000..34bb087bdf --- /dev/null +++ b/archived/projt-launcher/launcher/ui/dialogs/BackupDialog.cpp @@ -0,0 +1,330 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * 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, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +#include "BackupDialog.h" +#include <QInputDialog> +#include <QMessageBox> +#include "ui_BackupDialog.h" + +BackupDialog::BackupDialog(InstancePtr instance, QWidget* parent) + : QDialog(parent), + ui(new Ui::BackupDialog), + m_instance(instance), + m_backupManager(new BackupManager(this)) +{ + ui->setupUi(this); + + setWindowTitle(tr("Manage Backups - %1").arg(instance->name())); + + // Connect signals + connect(m_backupManager, &BackupManager::backupCreated, this, &BackupDialog::onBackupCreated); + connect(m_backupManager, &BackupManager::backupRestored, this, &BackupDialog::onBackupRestored); + + // Load backups + refreshBackupList(); +} + +BackupDialog::~BackupDialog() +{ + delete ui; +} + +void BackupDialog::refreshBackupList() +{ + ui->backupList->clear(); + m_backups = m_backupManager->listBackups(m_instance); + + for (const InstanceBackup& backup : m_backups) + { + QString displayText = QString("%1 - %2 (%3)") + .arg(backup.name()) + .arg(backup.createdAt().toString("yyyy-MM-dd HH:mm")) + .arg(backup.displaySize()); + ui->backupList->addItem(displayText); + } + + updateButtons(); +} + +void BackupDialog::updateBackupDetails() +{ + int currentRow = ui->backupList->currentRow(); + if (currentRow < 0 || currentRow >= m_backups.size()) + { + ui->backupDetails->clear(); + return; + } + + const InstanceBackup& backup = m_backups[currentRow]; + + QString details; + details += tr("<b>Name:</b> %1<br>").arg(backup.name()); + details += tr("<b>Created:</b> %1<br>").arg(backup.createdAt().toString("yyyy-MM-dd HH:mm:ss")); + details += tr("<b>Size:</b> %1<br>").arg(backup.displaySize()); + + if (!backup.description().isEmpty()) + { + details += tr("<b>Description:</b> %1<br>").arg(backup.description()); + } + + if (!backup.includedPaths().isEmpty()) + { + details += tr("<b>Included:</b> %1").arg(backup.includedPaths().join(", ")); + } + + ui->backupDetails->setHtml(details); +} + +void BackupDialog::updateButtons() +{ + bool hasSelection = ui->backupList->currentRow() >= 0; + ui->restoreButton->setEnabled(hasSelection); + ui->deleteButton->setEnabled(hasSelection); +} + +void BackupDialog::on_createButton_clicked() +{ + bool ok; + QString backupName = + QInputDialog::getText(this, tr("Create Backup"), tr("Backup name:"), QLineEdit::Normal, QString(), &ok); + + if (!ok) + { + return; + } + + BackupOptions options = getSelectedOptions(); + + // Disable UI during backup + ui->createButton->setEnabled(false); + ui->restoreButton->setEnabled(false); + ui->deleteButton->setEnabled(false); + ui->createButton->setText(tr("Creating...")); + + // Connect signals for this operation + connect( + m_backupManager, + &BackupManager::backupCreated, + this, + [this](const QString&, const QString&) + { + ui->createButton->setEnabled(true); + ui->restoreButton->setEnabled(true); + ui->deleteButton->setEnabled(true); + ui->createButton->setText(tr("Create Backup")); + QMessageBox::information(this, tr("Success"), tr("Backup created successfully!")); + refreshBackupList(); + disconnect(m_backupManager, &BackupManager::backupCreated, this, nullptr); + disconnect(m_backupManager, &BackupManager::backupFailed, this, nullptr); + }, + Qt::SingleShotConnection); + + connect( + m_backupManager, + &BackupManager::backupFailed, + this, + [this](const QString&, const QString& error) + { + ui->createButton->setEnabled(true); + ui->restoreButton->setEnabled(true); + ui->deleteButton->setEnabled(true); + ui->createButton->setText(tr("Create Backup")); + QMessageBox::critical(this, tr("Error"), tr("Failed to create backup: %1").arg(error)); + disconnect(m_backupManager, &BackupManager::backupCreated, this, nullptr); + disconnect(m_backupManager, &BackupManager::backupFailed, this, nullptr); + }, + Qt::SingleShotConnection); + + m_backupManager->createBackupAsync(m_instance, backupName, options); +} + +void BackupDialog::on_restoreButton_clicked() +{ + int currentRow = ui->backupList->currentRow(); + if (currentRow < 0 || currentRow >= m_backups.size()) + { + return; + } + + const InstanceBackup& backup = m_backups[currentRow]; + + auto result = QMessageBox::question( + this, + tr("Restore Backup"), + tr("Are you sure you want to restore backup '%1'?\nThis will overwrite current instance data.") + .arg(backup.name()), + QMessageBox::Yes | QMessageBox::No, + QMessageBox::No); + + if (result != QMessageBox::Yes) + { + return; + } + + bool createSafetyBackup = QMessageBox::question(this, + tr("Safety Backup"), + tr("Create a safety backup before restoring?"), + QMessageBox::Yes | QMessageBox::No, + QMessageBox::Yes) + == QMessageBox::Yes; + + // Disable UI during restore + ui->createButton->setEnabled(false); + ui->restoreButton->setEnabled(false); + ui->deleteButton->setEnabled(false); + ui->restoreButton->setText(tr("Restoring...")); + + // Connect signals for this operation + connect( + m_backupManager, + &BackupManager::backupRestored, + this, + [this](const QString&, const QString&) + { + ui->createButton->setEnabled(true); + ui->restoreButton->setEnabled(true); + ui->deleteButton->setEnabled(true); + ui->restoreButton->setText(tr("Restore")); + QMessageBox::information(this, tr("Success"), tr("Backup restored successfully!")); + refreshBackupList(); + disconnect(m_backupManager, &BackupManager::backupRestored, this, nullptr); + disconnect(m_backupManager, &BackupManager::restoreFailed, this, nullptr); + }, + Qt::SingleShotConnection); + + connect( + m_backupManager, + &BackupManager::restoreFailed, + this, + [this](const QString&, const QString& error) + { + ui->createButton->setEnabled(true); + ui->restoreButton->setEnabled(true); + ui->deleteButton->setEnabled(true); + ui->restoreButton->setText(tr("Restore")); + QMessageBox::critical(this, tr("Error"), tr("Failed to restore backup: %1").arg(error)); + disconnect(m_backupManager, &BackupManager::backupRestored, this, nullptr); + disconnect(m_backupManager, &BackupManager::restoreFailed, this, nullptr); + }, + Qt::SingleShotConnection); + + m_backupManager->restoreBackupAsync(m_instance, backup, createSafetyBackup); +} + +void BackupDialog::on_deleteButton_clicked() +{ + int currentRow = ui->backupList->currentRow(); + if (currentRow < 0 || currentRow >= m_backups.size()) + { + return; + } + + const InstanceBackup& backup = m_backups[currentRow]; + + auto result = QMessageBox::question(this, + tr("Delete Backup"), + tr("Are you sure you want to delete backup '%1'?").arg(backup.name()), + QMessageBox::Yes | QMessageBox::No, + QMessageBox::No); + + if (result != QMessageBox::Yes) + { + return; + } + + if (m_backupManager->deleteBackup(backup)) + { + refreshBackupList(); + QMessageBox::information(this, tr("Success"), tr("Backup deleted successfully!")); + } + else + { + QMessageBox::critical(this, tr("Error"), tr("Failed to delete backup.")); + } +} + +void BackupDialog::on_refreshButton_clicked() +{ + refreshBackupList(); +} + +void BackupDialog::on_backupList_currentRowChanged(int) +{ + updateBackupDetails(); + updateButtons(); +} + +void BackupDialog::onBackupCreated(const QString& instanceId, const QString& backupName) +{ + if (instanceId == m_instance->id()) + { + refreshBackupList(); + } +} + +void BackupDialog::onBackupRestored(const QString& instanceId, const QString& backupName) +{ + if (instanceId == m_instance->id()) + { + refreshBackupList(); + } +} + +BackupOptions BackupDialog::getSelectedOptions() const +{ + BackupOptions options; + options.includeSaves = ui->includeSaves->isChecked(); + options.includeConfig = ui->includeConfig->isChecked(); + options.includeMods = ui->includeMods->isChecked(); + options.includeResourcePacks = ui->includeResourcePacks->isChecked(); + options.includeShaderPacks = ui->includeShaderPacks->isChecked(); + options.includeScreenshots = ui->includeScreenshots->isChecked(); + options.includeOptions = ui->includeOptions->isChecked(); + options.customPaths = m_customPaths; + return options; +} + +void BackupDialog::on_addCustomPathButton_clicked() +{ + QString path = QInputDialog::getText(this, + tr("Add Custom Path"), + tr("Enter relative path to include (e.g., \"logs\", \"crash-reports\"):"), + QLineEdit::Normal, + QString(), + nullptr); + + if (!path.isEmpty() && !m_customPaths.contains(path)) + { + m_customPaths.append(path); + ui->customPathsList->addItem(path); + } +} + +void BackupDialog::on_removeCustomPathButton_clicked() +{ + int currentRow = ui->customPathsList->currentRow(); + if (currentRow >= 0 && currentRow < m_customPaths.size()) + { + m_customPaths.removeAt(currentRow); + delete ui->customPathsList->takeItem(currentRow); + } +} diff --git a/archived/projt-launcher/launcher/ui/dialogs/BackupDialog.h b/archived/projt-launcher/launcher/ui/dialogs/BackupDialog.h new file mode 100644 index 0000000000..3c85d628a9 --- /dev/null +++ b/archived/projt-launcher/launcher/ui/dialogs/BackupDialog.h @@ -0,0 +1,63 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * 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, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#pragma once + +#include <QDialog> +#include "BaseInstance.h" +#include "minecraft/BackupManager.h" + +namespace Ui +{ + class BackupDialog; +} + +class BackupDialog : public QDialog +{ + Q_OBJECT + + public: + explicit BackupDialog(InstancePtr instance, QWidget* parent = nullptr); + ~BackupDialog(); + + private slots: + void on_createButton_clicked(); + void on_restoreButton_clicked(); + void on_deleteButton_clicked(); + void on_refreshButton_clicked(); + void on_backupList_currentRowChanged(int currentRow); + void on_addCustomPathButton_clicked(); + void on_removeCustomPathButton_clicked(); + void onBackupCreated(const QString& instanceId, const QString& backupName); + void onBackupRestored(const QString& instanceId, const QString& backupName); + + private: + void refreshBackupList(); + void updateBackupDetails(); + void updateButtons(); + BackupOptions getSelectedOptions() const; + + Ui::BackupDialog* ui; + InstancePtr m_instance; + BackupManager* m_backupManager; + QList<InstanceBackup> m_backups; + QStringList m_customPaths; +}; diff --git a/archived/projt-launcher/launcher/ui/dialogs/BackupDialog.ui b/archived/projt-launcher/launcher/ui/dialogs/BackupDialog.ui new file mode 100644 index 0000000000..d699588691 --- /dev/null +++ b/archived/projt-launcher/launcher/ui/dialogs/BackupDialog.ui @@ -0,0 +1,228 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>BackupDialog</class> + <widget class="QWidget" name="BackupDialog"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>700</width> + <height>500</height> + </rect> + </property> + <property name="windowTitle"> + <string>Manage Backups</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <widget class="QSplitter" name="splitter"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <widget class="QWidget" name="leftWidget" native="true"> + <layout class="QVBoxLayout" name="leftLayout"> + <item> + <widget class="QLabel" name="backupListLabel"> + <property name="text"> + <string>Available Backups:</string> + </property> + </widget> + </item> + <item> + <widget class="QListWidget" name="backupList"/> + </item> + <item> + <layout class="QHBoxLayout" name="buttonLayout"> + <item> + <widget class="QPushButton" name="createButton"> + <property name="text"> + <string>Create</string> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="restoreButton"> + <property name="text"> + <string>Restore</string> + </property> + <property name="enabled"> + <bool>false</bool> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="deleteButton"> + <property name="text"> + <string>Delete</string> + </property> + <property name="enabled"> + <bool>false</bool> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="refreshButton"> + <property name="text"> + <string>Refresh</string> + </property> + </widget> + </item> + </layout> + </item> + </layout> + </widget> + <widget class="QWidget" name="rightWidget" native="true"> + <layout class="QVBoxLayout" name="rightLayout"> + <item> + <widget class="QGroupBox" name="detailsGroup"> + <property name="title"> + <string>Backup Details</string> + </property> + <layout class="QVBoxLayout" name="detailsLayout"> + <item> + <widget class="QTextBrowser" name="backupDetails"> + <property name="openExternalLinks"> + <bool>false</bool> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item> + <widget class="QGroupBox" name="optionsGroup"> + <property name="title"> + <string>Backup Options</string> + </property> + <layout class="QVBoxLayout" name="optionsLayout"> + <item> + <widget class="QCheckBox" name="includeSaves"> + <property name="text"> + <string>Include Saves</string> + </property> + <property name="checked"> + <bool>true</bool> + </property> + </widget> + </item> + <item> + <widget class="QCheckBox" name="includeConfig"> + <property name="text"> + <string>Include Config</string> + </property> + <property name="checked"> + <bool>true</bool> + </property> + </widget> + </item> + <item> + <widget class="QCheckBox" name="includeMods"> + <property name="text"> + <string>Include Mods</string> + </property> + <property name="checked"> + <bool>false</bool> + </property> + </widget> + </item> + <item> + <widget class="QCheckBox" name="includeResourcePacks"> + <property name="text"> + <string>Include Resource Packs</string> + </property> + <property name="checked"> + <bool>false</bool> + </property> + </widget> + </item> + <item> + <widget class="QCheckBox" name="includeShaderPacks"> + <property name="text"> + <string>Include Shader Packs</string> + </property> + <property name="checked"> + <bool>false</bool> + </property> + </widget> + </item> + <item> + <widget class="QCheckBox" name="includeScreenshots"> + <property name="text"> + <string>Include Screenshots</string> + </property> + <property name="checked"> + <bool>false</bool> + </property> + </widget> + </item> + <item> + <widget class="QCheckBox" name="includeOptions"> + <property name="text"> + <string>Include Options (options.txt)</string> + </property> + <property name="checked"> + <bool>true</bool> + </property> + </widget> + </item> + <item> + <widget class="Line" name="separator"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + </widget> + </item> + <item> + <widget class="QLabel" name="customPathsLabel"> + <property name="text"> + <string>Custom Paths:</string> + </property> + </widget> + </item> + <item> + <widget class="QListWidget" name="customPathsList"> + <property name="maximumSize"> + <size> + <width>16777215</width> + <height>100</height> + </size> + </property> + </widget> + </item> + <item> + <layout class="QHBoxLayout" name="customPathsButtons"> + <item> + <widget class="QPushButton" name="addCustomPathButton"> + <property name="text"> + <string>Add...</string> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="removeCustomPathButton"> + <property name="text"> + <string>Remove</string> + </property> + </widget> + </item> + </layout> + </item> + <item> + <widget class="QLabel" name="estimatedSizeLabel"> + <property name="text"> + <string>Estimated Size: Calculating...</string> + </property> + </widget> + </item> + </layout> + </widget> + </item> + </layout> + </widget> + </widget> + </item> + </layout> + </widget> + <resources/> + <connections/> +</ui> diff --git a/archived/projt-launcher/launcher/ui/dialogs/BlockedModsDialog.cpp b/archived/projt-launcher/launcher/ui/dialogs/BlockedModsDialog.cpp new file mode 100644 index 0000000000..c154de7b2f --- /dev/null +++ b/archived/projt-launcher/launcher/ui/dialogs/BlockedModsDialog.cpp @@ -0,0 +1,530 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * 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, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * + * + * + * + * + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> + * Copyright (C) 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> + * Copyright (C) 2022 kumquat-ir <66188216+kumquat-ir@users.noreply.github.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * + * + * ======================================================================== */ + +#include "BlockedModsDialog.h" +#include "ui_BlockedModsDialog.h" + +#include "Application.h" +#include "modplatform/helpers/HashUtils.h" + +#include <QDebug> +#include <QDesktopServices> +#include <QDialogButtonBox> +#include <QDir> +#include <QDirIterator> +#include <QDragEnterEvent> +#include <QFileDialog> +#include <QFileInfo> +#include <QMimeData> +#include <QPushButton> +#include <QStandardPaths> +#include <QTimer> + +BlockedModsDialog::BlockedModsDialog(QWidget* parent, + const QString& title, + const QString& text, + QList<BlockedMod>& mods, + QString hash_type) + : QDialog(parent), + ui(new Ui::BlockedModsDialog), + m_mods(mods), + m_hashType(hash_type) +{ + m_hashingTask = shared_qobject_ptr<ConcurrentTask>( + new ConcurrentTask("MakeHashesTask", APPLICATION->settings()->get("NumberOfConcurrentTasks").toInt())); + connect(m_hashingTask.get(), &Task::finished, this, &BlockedModsDialog::hashTaskFinished); + + ui->setupUi(this); + + ui->buttonBox->button(QDialogButtonBox::Cancel)->setText(tr("Cancel")); + ui->buttonBox->button(QDialogButtonBox::Ok)->setText(tr("OK")); + connect(ui->openMissingButton, &QPushButton::clicked, this, [this]() { openAll(true); }); + connect(ui->downloadFolderButton, &QPushButton::clicked, this, &BlockedModsDialog::addDownloadFolder); + + connect(&m_watcher, &QFileSystemWatcher::directoryChanged, this, &BlockedModsDialog::directoryChanged); + + qDebug() << "[Blocked Mods Dialog] Mods List: " << mods; + + // defer setup of file system watchers until after the dialog is shown + // this allows OS (namely macOS) permission prompts to show after the relevant dialog appears + QTimer::singleShot(0, + this, + [this] + { + setupWatch(); + scanPaths(); + update(); + }); + + this->setWindowTitle(title); + ui->labelDescription->setText(text); + + // force all URL handling as external + connect(ui->textBrowserWatched, + &QTextBrowser::anchorClicked, + this, + [](const QUrl url) { QDesktopServices::openUrl(url); }); + + setAcceptDrops(true); + + update(); +} + +BlockedModsDialog::~BlockedModsDialog() +{ + delete ui; +} + +void BlockedModsDialog::dragEnterEvent(QDragEnterEvent* e) +{ + if (e->mimeData()->hasUrls()) + { + e->acceptProposedAction(); + } +} + +void BlockedModsDialog::dropEvent(QDropEvent* e) +{ + for (QUrl& url : e->mimeData()->urls()) + { + if (url.scheme().isEmpty()) + { // ensure isLocalFile() works correctly + url.setScheme("file"); + } + + if (!url.isLocalFile()) + { // can't drop external files here. + continue; + } + + QString filePath = url.toLocalFile(); + qDebug() << "[Blocked Mods Dialog] Dropped file:" << filePath; + addHashTask(filePath); + + // watch for changes + QFileInfo file = QFileInfo(filePath); + QString path = file.dir().absolutePath(); + qDebug() << "[Blocked Mods Dialog] Adding watch path:" << path; + m_watcher.addPath(path); + } + scanPaths(); + update(); +} + +void BlockedModsDialog::done(int r) +{ + QDialog::done(r); + disconnect(&m_watcher, &QFileSystemWatcher::directoryChanged, this, &BlockedModsDialog::directoryChanged); +} + +void BlockedModsDialog::openAll(bool missingOnly) +{ + for (auto& mod : m_mods) + { + if (!missingOnly || !mod.matched) + { + QDesktopServices::openUrl(mod.websiteUrl); + } + } +} + +void BlockedModsDialog::addDownloadFolder() +{ + QString dir = QFileDialog::getExistingDirectory(this, + tr("Select directory where you downloaded the mods"), + QStandardPaths::writableLocation(QStandardPaths::DownloadLocation), + QFileDialog::ShowDirsOnly); + qDebug() << "[Blocked Mods Dialog] Adding watch path:" << dir; + m_watcher.addPath(dir); + scanPath(dir, true); + update(); +} + +/// @brief update UI with current status of the blocked mod detection +void BlockedModsDialog::update() +{ + QString text; + QString span; + + for (auto& mod : m_mods) + { + if (mod.matched) + { + // ✔ -> html for HEAVY CHECK MARK : ✔ + span = QString(tr("<span style=\"color:green\"> ✔ Found at %1 </span>")).arg(mod.localPath); + } + else + { + // ✘ -> html for HEAVY BALLOT X : ✘ + span = QString(tr("<span style=\"color:red\"> ✘ Not Found </span>")); + } + text += QString(tr("%1: <a href='%2'>%2</a> <p>Hash: %3 %4</p> <br/>")) + .arg(mod.name, mod.websiteUrl, mod.hash, span); + } + + ui->textBrowserModsListing->setText(text); + + QString watching; + for (auto& dir : m_watcher.directories()) + { + QUrl fileURL = QUrl::fromLocalFile(dir); + watching += QString("<a href=\"%1\">%2</a><br/>").arg(fileURL.toString(), dir); + } + + ui->textBrowserWatched->setText(watching); + + if (allModsMatched()) + { + ui->labelModsFound->setText("<span style=\"color:green\">✔</span>" + tr("All mods found")); + ui->openMissingButton->setDisabled(true); + ui->buttonBox->button(QDialogButtonBox::Ok)->setText(tr("OK")); + } + else + { + ui->labelModsFound->setText(tr("Please download the missing mods.")); + ui->openMissingButton->setDisabled(false); + ui->buttonBox->button(QDialogButtonBox::Ok)->setText(tr("Skip")); + } +} + +/// @brief Signal fired when a watched directory has changed +/// @param path the path to the changed directory +void BlockedModsDialog::directoryChanged(QString path) +{ + qDebug() << "[Blocked Mods Dialog] Directory changed: " << path; + validateMatchedMods(); + scanPath(path, true); +} + +/// @brief add the user downloads folder and the global mods folder to the filesystem watcher +void BlockedModsDialog::setupWatch() +{ + const QString downloadsFolder = APPLICATION->settings()->get("DownloadsDir").toString(); + const QString modsFolder = APPLICATION->settings()->get("CentralModsDir").toString(); + const bool downloadsFolderWatchRecursive = APPLICATION->settings()->get("DownloadsDirWatchRecursive").toBool(); + watchPath(downloadsFolder, downloadsFolderWatchRecursive); + watchPath(modsFolder, true); +} + +void BlockedModsDialog::watchPath(QString path, bool watch_recursive) +{ + auto to_watch = QFileInfo(path); + if (!to_watch.isReadable()) + { + qWarning() << "[Blocked Mods Dialog] Failed to add Watch Path (unable to read):" << path; + return; + } + auto to_watch_path = to_watch.canonicalFilePath(); + if (m_watcher.directories().contains(to_watch_path)) + return; // don't watch the same path twice (no loops!) + + qDebug() << "[Blocked Mods Dialog] Adding Watch Path:" << path; + m_watcher.addPath(to_watch_path); + + if (!to_watch.isDir() || !watch_recursive) + return; + + QDirIterator it(to_watch_path, QDir::Filter::Dirs | QDir::Filter::NoDotAndDotDot, QDirIterator::NoIteratorFlags); + while (it.hasNext()) + { + QString watch_dir = QDir(it.next()).canonicalPath(); // resolve symlinks and relative paths + watchPath(watch_dir, watch_recursive); + } +} + +/// @brief scan all watched folder +void BlockedModsDialog::scanPaths() +{ + for (auto& dir : m_watcher.directories()) + { + scanPath(dir, false); + } + runHashTask(); +} + +/// @brief Scan the directory at path, skip paths that do not contain a file name +/// of a blocked mod we are looking for +/// @param path the directory to scan +void BlockedModsDialog::scanPath(QString path, bool start_task) +{ + QDir scan_dir(path); + QDirIterator scan_it(path, QDir::Filter::Files | QDir::Filter::Hidden, QDirIterator::NoIteratorFlags); + while (scan_it.hasNext()) + { + QString file = scan_it.next(); + + if (!checkValidPath(file)) + { + continue; + } + + addHashTask(file); + } + + if (start_task) + { + runHashTask(); + } +} + +/// @brief add a hashing task for the file located at path, add the path to the pending set if the hashing task is already running +/// @param path the path to the local file being hashed +void BlockedModsDialog::addHashTask(QString path) +{ + qDebug() << "[Blocked Mods Dialog] adding a Hash task for" << path << "to the pending set."; + m_pendingHashPaths.insert(path); +} + +/// @brief add a hashing task for the file located at path and connect it to check that hash against +/// our blocked mods list +/// @param path the path to the local file being hashed +void BlockedModsDialog::buildHashTask(QString path) +{ + auto hash_task = Hashing::createHasher(path, m_hashType); + + qDebug() << "[Blocked Mods Dialog] Creating Hash task for path: " << path; + + connect(hash_task.get(), + &Task::succeeded, + this, + [this, hash_task, path] { checkMatchHash(hash_task->getResult(), path); }); + connect(hash_task.get(), &Task::failed, this, [path] { qDebug() << "Failed to hash path: " << path; }); + + m_hashingTask->addTask(hash_task); +} + +/// @brief check if the computed hash for the provided path matches a blocked +/// mod we are looking for +/// @param hash the computed hash for the provided path +/// @param path the path to the local file being compared +void BlockedModsDialog::checkMatchHash(QString hash, QString path) +{ + bool match = false; + + qDebug() << "[Blocked Mods Dialog] Checking for match on hash: " << hash << "| From path:" << path; + + auto downloadDir = QFileInfo(APPLICATION->settings()->get("DownloadsDir").toString()).absoluteFilePath(); + auto moveFiles = APPLICATION->settings()->get("MoveModsFromDownloadsDir").toBool(); + for (auto& mod : m_mods) + { + if (mod.matched) + { + continue; + } + if (mod.hash.compare(hash, Qt::CaseInsensitive) == 0) + { + mod.matched = true; + mod.localPath = path; + if (moveFiles) + { + mod.move = QFileInfo(path).absoluteFilePath().startsWith(downloadDir); + } + match = true; + + qDebug() << "[Blocked Mods Dialog] Hash match found:" << mod.name << hash << "| From path:" << path; + + break; + } + } + + if (match) + { + update(); + } +} + +/// @brief Check if the name of the file at path matches the name of a blocked mod we are searching for +/// @param path the path to check +/// @return boolean: did the path match the name of a blocked mod? +bool BlockedModsDialog::checkValidPath(QString path) +{ + const QFileInfo file = QFileInfo(path); + const QString filename = file.fileName(); + + auto compare = [](QString fsFilename, QString metadataFilename) + { return metadataFilename.compare(fsFilename, Qt::CaseInsensitive) == 0; }; + + // super lax compare (but not fuzzy) + // convert to lowercase + // convert all speratores to whitespace + // simplify sequence of internal whitespace to a single space + // efectivly compare two strings ignoring all separators and case + auto laxCompare = [](QString fsfilename, QString metadataFilename) + { + // allowed character seperators + QList<QChar> allowedSeperators = { '-', '+', '.', '_' }; + + // copy in lowercase + auto fsName = fsfilename.toLower(); + auto metaName = metadataFilename.toLower(); + + // replace all potential allowed seperatores with whitespace + for (auto sep : allowedSeperators) + { + fsName = fsName.replace(sep, ' '); + metaName = metaName.replace(sep, ' '); + } + + // remove extraneous whitespace + fsName = fsName.simplified(); + metaName = metaName.simplified(); + + return fsName.compare(metaName) == 0; + }; + + auto downloadDir = QFileInfo(APPLICATION->settings()->get("DownloadsDir").toString()).absoluteFilePath(); + auto moveFiles = APPLICATION->settings()->get("MoveModsFromDownloadsDir").toBool(); + for (auto& mod : m_mods) + { + if (compare(filename, mod.name)) + { + // if the mod is not yet matched and doesn't have a hash then + // just match it with the file that has the exact same name + if (!mod.matched && mod.hash.isEmpty()) + { + mod.matched = true; + mod.localPath = path; + if (moveFiles) + { + mod.move = QFileInfo(path).absoluteFilePath().startsWith(downloadDir); + } + return false; + } + qDebug() << "[Blocked Mods Dialog] Name match found:" << mod.name << "| From path:" << path; + return true; + } + if (laxCompare(filename, mod.name)) + { + qDebug() << "[Blocked Mods Dialog] Lax name match found:" << mod.name << "| From path:" << path; + return true; + } + } + + return false; +} + +bool BlockedModsDialog::allModsMatched() +{ + return std::all_of(m_mods.begin(), m_mods.end(), [](auto const& mod) { return mod.matched; }); +} + +/// @brief ensure matched file paths still exist +void BlockedModsDialog::validateMatchedMods() +{ + bool changed = false; + for (auto& mod : m_mods) + { + if (mod.matched) + { + QFileInfo file = QFileInfo(mod.localPath); + if (!file.exists() || !file.isFile()) + { + qDebug() << "[Blocked Mods Dialog] File" << mod.localPath << "for mod" << mod.name + << "has vanshed! marking as not matched."; + mod.localPath = ""; + mod.matched = false; + changed = true; + } + } + } + if (changed) + { + update(); + } +} + +/// @brief run hash task or mark a pending run if it is already running +void BlockedModsDialog::runHashTask() +{ + if (!m_hashingTask->isRunning()) + { + m_rehashPending = false; + + if (!m_pendingHashPaths.isEmpty()) + { + qDebug() << "[Blocked Mods Dialog] there are pending hash tasks, building and running tasks"; + + auto path = m_pendingHashPaths.begin(); + while (path != m_pendingHashPaths.end()) + { + buildHashTask(*path); + path = m_pendingHashPaths.erase(path); + } + + m_hashingTask->start(); + } + } + else + { + qDebug() << "[Blocked Mods Dialog] queueing another run of the hashing task"; + qDebug() << "[Blocked Mods Dialog] pending hash tasks:" << m_pendingHashPaths; + m_rehashPending = true; + } +} + +void BlockedModsDialog::hashTaskFinished() +{ + qDebug() << "[Blocked Mods Dialog] All hash tasks finished"; + if (m_rehashPending) + { + qDebug() << "[Blocked Mods Dialog] task finished with a rehash pending, rerunning"; + runHashTask(); + } +} + +/// qDebug print support for the BlockedMod struct +QDebug operator<<(QDebug debug, const BlockedMod& m) +{ + QDebugStateSaver saver(debug); + + debug.nospace() << "{ name: " << m.name << ", websiteUrl: " << m.websiteUrl << ", hash: " << m.hash + << ", matched: " << m.matched << ", localPath: " << m.localPath << "}"; + + return debug; +} diff --git a/archived/projt-launcher/launcher/ui/dialogs/BlockedModsDialog.h b/archived/projt-launcher/launcher/ui/dialogs/BlockedModsDialog.h new file mode 100644 index 0000000000..6be4eccaea --- /dev/null +++ b/archived/projt-launcher/launcher/ui/dialogs/BlockedModsDialog.h @@ -0,0 +1,129 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * 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, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * + * + * + * + * + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> + * Copyright (C) 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> + * Copyright (C) 2022 kumquat-ir <66188216+kumquat-ir@users.noreply.github.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * + * + * ======================================================================== */ + +#pragma once + +#include <QDialog> +#include <QList> +#include <QString> + +#include <QFileSystemWatcher> + +#include "tasks/ConcurrentTask.h" + +class QPushButton; + +struct BlockedMod +{ + QString name; + QString websiteUrl; + QString hash; + bool matched; + QString localPath; + QString targetFolder; + bool disabled = false; + bool move = false; +}; + +QT_BEGIN_NAMESPACE +namespace Ui +{ + class BlockedModsDialog; +} +QT_END_NAMESPACE + +class BlockedModsDialog : public QDialog +{ + Q_OBJECT + + public: + BlockedModsDialog(QWidget* parent, + const QString& title, + const QString& text, + QList<BlockedMod>& mods, + QString hash_type = "sha1"); + + ~BlockedModsDialog() override; + + protected: + void dragEnterEvent(QDragEnterEvent* event) override; + void dropEvent(QDropEvent* event) override; + + protected slots: + void done(int r) override; + + private: + Ui::BlockedModsDialog* ui; + QList<BlockedMod>& m_mods; + QFileSystemWatcher m_watcher; + shared_qobject_ptr<ConcurrentTask> m_hashingTask; + QSet<QString> m_pendingHashPaths; + bool m_rehashPending; + QString m_hashType; + + void openAll(bool missingOnly); + void addDownloadFolder(); + void update(); + void directoryChanged(QString path); + void setupWatch(); + void watchPath(QString path, bool watch_recursive = false); + void scanPaths(); + void scanPath(QString path, bool start_task); + void addHashTask(QString path); + void buildHashTask(QString path); + void checkMatchHash(QString hash, QString path); + void validateMatchedMods(); + void runHashTask(); + void hashTaskFinished(); + + bool checkValidPath(QString path); + bool allModsMatched(); +}; + +QDebug operator<<(QDebug debug, const BlockedMod& m); diff --git a/archived/projt-launcher/launcher/ui/dialogs/BlockedModsDialog.ui b/archived/projt-launcher/launcher/ui/dialogs/BlockedModsDialog.ui new file mode 100644 index 0000000000..850ad713e6 --- /dev/null +++ b/archived/projt-launcher/launcher/ui/dialogs/BlockedModsDialog.ui @@ -0,0 +1,205 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>BlockedModsDialog</class> + <widget class="QDialog" name="BlockedModsDialog"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>800</width> + <height>500</height> + </rect> + </property> + <property name="sizePolicy"> + <sizepolicy hsizetype="Maximum" vsizetype="Preferred"> + <horstretch>2</horstretch> + <verstretch>1</verstretch> + </sizepolicy> + </property> + <property name="minimumSize"> + <size> + <width>700</width> + <height>350</height> + </size> + </property> + <property name="windowTitle"> + <string notr="true">BlockedModsDialog</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout" stretch="0,0,0,0"> + <item> + <widget class="QLabel" name="labelDescription"> + <property name="text"> + <string notr="true">Placeholder description</string> + </property> + <property name="textFormat"> + <enum>Qt::RichText</enum> + </property> + <property name="wordWrap"> + <bool>true</bool> + </property> + </widget> + </item> + <item> + <widget class="QLabel" name="labelExplain"> + <property name="text"> + <string><html><head/><body><p>Your configured global mods folder and default downloads folder are automatically checked for the downloaded mods and they will be copied to the instance if found.</p><p>Optionally, you may drag and drop the downloaded mods onto this dialog or add a folder to watch if you did not download the mods to a default location.</p><p><span style=" font-weight:600;">Click 'Open Missing' to open all the download links in the browser. </span></p></body></html></string> + </property> + <property name="wordWrap"> + <bool>true</bool> + </property> + </widget> + </item> + <item> + <widget class="QTabWidget" name="tabWidget"> + <property name="currentIndex"> + <number>0</number> + </property> + <widget class="QWidget" name="tab"> + <attribute name="title"> + <string>Blocked Mods</string> + </attribute> + <layout class="QVBoxLayout" name="verticalLayout_3"> + <item> + <widget class="QTextBrowser" name="textBrowserModsListing"> + <property name="acceptRichText"> + <bool>true</bool> + </property> + <property name="openExternalLinks"> + <bool>true</bool> + </property> + </widget> + </item> + <item> + <layout class="QHBoxLayout" name="openMissingLayout"> + <item> + <widget class="QPushButton" name="openMissingButton"> + <property name="text"> + <string>Open Missing</string> + </property> + </widget> + </item> + <item> + <spacer name="openMissingSpacer"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + </layout> + </item> + </layout> + </widget> + <widget class="QWidget" name="tab_2"> + <attribute name="title"> + <string>Watched Folders</string> + </attribute> + <layout class="QVBoxLayout" name="verticalLayout_2"> + <item> + <widget class="QTextBrowser" name="textBrowserWatched"> + <property name="baseSize"> + <size> + <width>0</width> + <height>12</height> + </size> + </property> + <property name="openExternalLinks"> + <bool>true</bool> + </property> + <property name="openLinks"> + <bool>false</bool> + </property> + </widget> + </item> + <item> + <layout class="QHBoxLayout" name="downloadFolderLayout"> + <item> + <widget class="QPushButton" name="downloadFolderButton"> + <property name="text"> + <string>Add Download Folder</string> + </property> + </widget> + </item> + <item> + <spacer name="downloadFolderSpacer"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + </layout> + </item> + </layout> + </widget> + </widget> + </item> + <item> + <layout class="QHBoxLayout" name="bottomBoxH"> + <item> + <widget class="QLabel" name="labelModsFound"> + <property name="text"> + <string/> + </property> + </widget> + </item> + <item> + <widget class="QDialogButtonBox" name="buttonBox"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="standardButtons"> + <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set> + </property> + </widget> + </item> + </layout> + </item> + </layout> + </widget> + <resources/> + <connections> + <connection> + <sender>buttonBox</sender> + <signal>accepted()</signal> + <receiver>BlockedModsDialog</receiver> + <slot>accept()</slot> + <hints> + <hint type="sourcelabel"> + <x>199</x> + <y>425</y> + </hint> + <hint type="destinationlabel"> + <x>199</x> + <y>227</y> + </hint> + </hints> + </connection> + <connection> + <sender>buttonBox</sender> + <signal>rejected()</signal> + <receiver>BlockedModsDialog</receiver> + <slot>reject()</slot> + <hints> + <hint type="sourcelabel"> + <x>199</x> + <y>425</y> + </hint> + <hint type="destinationlabel"> + <x>199</x> + <y>227</y> + </hint> + </hints> + </connection> + </connections> +</ui> diff --git a/archived/projt-launcher/launcher/ui/dialogs/ChooseProviderDialog.cpp b/archived/projt-launcher/launcher/ui/dialogs/ChooseProviderDialog.cpp new file mode 100644 index 0000000000..a0e1fed11f --- /dev/null +++ b/archived/projt-launcher/launcher/ui/dialogs/ChooseProviderDialog.cpp @@ -0,0 +1,118 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * 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, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +#include "ChooseProviderDialog.h" +#include "ui_ChooseProviderDialog.h" + +#include <QPushButton> +#include <QRadioButton> + +#include "modplatform/ModIndex.h" + +ChooseProviderDialog::ChooseProviderDialog(QWidget* parent, bool single_choice, bool allow_skipping) + : QDialog(parent), + ui(new Ui::ChooseProviderDialog) +{ + ui->setupUi(this); + + addProviders(); + m_providers.button(0)->click(); + + connect(ui->skipOneButton, &QPushButton::clicked, this, &ChooseProviderDialog::skipOne); + connect(ui->skipAllButton, &QPushButton::clicked, this, &ChooseProviderDialog::skipAll); + + connect(ui->confirmOneButton, &QPushButton::clicked, this, &ChooseProviderDialog::confirmOne); + connect(ui->confirmAllButton, &QPushButton::clicked, this, &ChooseProviderDialog::confirmAll); + + if (single_choice) + { + ui->providersLayout->removeWidget(ui->skipAllButton); + ui->providersLayout->removeWidget(ui->confirmAllButton); + } + + if (!allow_skipping) + { + ui->providersLayout->removeWidget(ui->skipOneButton); + ui->providersLayout->removeWidget(ui->skipAllButton); + } +} + +ChooseProviderDialog::~ChooseProviderDialog() +{ + delete ui; +} + +void ChooseProviderDialog::setDescription(QString desc) +{ + ui->explanationLabel->setText(desc); +} + +void ChooseProviderDialog::skipOne() +{ + reject(); +} +void ChooseProviderDialog::skipAll() +{ + m_response.skip_all = true; + reject(); +} + +void ChooseProviderDialog::confirmOne() +{ + m_response.chosen = getSelectedProvider(); + m_response.try_others = ui->tryOthersCheckbox->isChecked(); + accept(); +} +void ChooseProviderDialog::confirmAll() +{ + m_response.chosen = getSelectedProvider(); + m_response.confirm_all = true; + m_response.try_others = ui->tryOthersCheckbox->isChecked(); + accept(); +} + +auto ChooseProviderDialog::getSelectedProvider() const -> ModPlatform::ResourceProvider +{ + return ModPlatform::ResourceProvider(m_providers.checkedId()); +} + +void ChooseProviderDialog::addProviders() +{ + int btn_index = 0; + QRadioButton* btn; + + for (auto& provider : { ModPlatform::ResourceProvider::MODRINTH, ModPlatform::ResourceProvider::FLAME }) + { + btn = new QRadioButton(ModPlatform::ProviderCapabilities::readableName(provider), this); + m_providers.addButton(btn, btn_index++); + ui->providersLayout->addWidget(btn); + } +} + +void ChooseProviderDialog::disableInput() +{ + for (auto& btn : m_providers.buttons()) + btn->setEnabled(false); + + ui->skipOneButton->setEnabled(false); + ui->skipAllButton->setEnabled(false); + ui->confirmOneButton->setEnabled(false); + ui->confirmAllButton->setEnabled(false); +} diff --git a/archived/projt-launcher/launcher/ui/dialogs/ChooseProviderDialog.h b/archived/projt-launcher/launcher/ui/dialogs/ChooseProviderDialog.h new file mode 100644 index 0000000000..6a8974fd75 --- /dev/null +++ b/archived/projt-launcher/launcher/ui/dialogs/ChooseProviderDialog.h @@ -0,0 +1,82 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * 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, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +#pragma once + +#include <QButtonGroup> +#include <QDialog> + +namespace Ui +{ + class ChooseProviderDialog; +} + +namespace ModPlatform +{ + enum class ResourceProvider; +} + +class Mod; +class NetJob; + +class ChooseProviderDialog : public QDialog +{ + Q_OBJECT + + struct Response + { + bool skip_all = false; + bool confirm_all = false; + + bool try_others = false; + + ModPlatform::ResourceProvider chosen; + }; + + public: + explicit ChooseProviderDialog(QWidget* parent, bool single_choice = false, bool allow_skipping = true); + ~ChooseProviderDialog(); + + auto getResponse() const -> Response + { + return m_response; + } + + void setDescription(QString desc); + + private slots: + void skipOne(); + void skipAll(); + void confirmOne(); + void confirmAll(); + + private: + void addProviders(); + void disableInput(); + + auto getSelectedProvider() const -> ModPlatform::ResourceProvider; + + private: + Ui::ChooseProviderDialog* ui; + + QButtonGroup m_providers; + + Response m_response; +}; diff --git a/archived/projt-launcher/launcher/ui/dialogs/ChooseProviderDialog.ui b/archived/projt-launcher/launcher/ui/dialogs/ChooseProviderDialog.ui new file mode 100644 index 0000000000..78cd9613bb --- /dev/null +++ b/archived/projt-launcher/launcher/ui/dialogs/ChooseProviderDialog.ui @@ -0,0 +1,89 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>ChooseProviderDialog</class> + <widget class="QDialog" name="ChooseProviderDialog"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>453</width> + <height>197</height> + </rect> + </property> + <property name="windowTitle"> + <string>Choose a mod provider</string> + </property> + <layout class="QGridLayout" name="gridLayout"> + <item row="0" column="0" colspan="2"> + <widget class="QLabel" name="explanationLabel"> + <property name="alignment"> + <set>Qt::AlignJustify|Qt::AlignTop</set> + </property> + <property name="wordWrap"> + <bool>true</bool> + </property> + <property name="indent"> + <number>-1</number> + </property> + </widget> + </item> + <item row="1" column="0" colspan="2"> + <layout class="QFormLayout" name="providersLayout"> + <property name="labelAlignment"> + <set>Qt::AlignHCenter|Qt::AlignTop</set> + </property> + <property name="formAlignment"> + <set>Qt::AlignHCenter|Qt::AlignTop</set> + </property> + </layout> + </item> + <item row="4" column="0" colspan="2"> + <layout class="QHBoxLayout" name="buttonsLayout"> + <item> + <widget class="QPushButton" name="skipOneButton"> + <property name="text"> + <string>Skip this mod</string> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="skipAllButton"> + <property name="text"> + <string>Skip all</string> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="confirmAllButton"> + <property name="text"> + <string>Confirm for all</string> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="confirmOneButton"> + <property name="text"> + <string>Confirm</string> + </property> + <property name="default"> + <bool>true</bool> + </property> + </widget> + </item> + </layout> + </item> + <item row="3" column="0"> + <widget class="QCheckBox" name="tryOthersCheckbox"> + <property name="text"> + <string>Try to automatically use other providers if the chosen one fails</string> + </property> + <property name="checked"> + <bool>true</bool> + </property> + </widget> + </item> + </layout> + </widget> + <resources/> + <connections/> +</ui> diff --git a/archived/projt-launcher/launcher/ui/dialogs/CopyInstanceDialog.cpp b/archived/projt-launcher/launcher/ui/dialogs/CopyInstanceDialog.cpp new file mode 100644 index 0000000000..941b867e92 --- /dev/null +++ b/archived/projt-launcher/launcher/ui/dialogs/CopyInstanceDialog.cpp @@ -0,0 +1,353 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * 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, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * + * + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> + * Copyright (C) 2023 TheKodeToad <TheKodeToad@proton.me> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * 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, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * 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 <QLayout> +#include <QPushButton> + +#include "Application.h" +#include "BuildConfig.h" +#include "CopyInstanceDialog.h" +#include "ui_CopyInstanceDialog.h" + +#include "ui/dialogs/IconPickerDialog.h" + +#include "BaseInstance.h" +#include "BaseVersion.h" +#include "DesktopServices.h" +#include "FileSystem.h" +#include "InstanceList.h" +#include "icons/IconList.hpp" + +CopyInstanceDialog::CopyInstanceDialog(InstancePtr original, QWidget* parent) + : QDialog(parent), + ui(new Ui::CopyInstanceDialog), + m_original(original) +{ + ui->setupUi(this); + resize(minimumSizeHint()); + layout()->setSizeConstraint(QLayout::SetFixedSize); + + InstIconKey = original->iconKey(); + ui->iconButton->setIcon(APPLICATION->icons()->getIcon(InstIconKey)); + ui->instNameTextBox->setText(original->name()); + ui->instNameTextBox->setFocus(); + + QStringList groups = APPLICATION->instances()->getGroups(); + groups.prepend(""); + ui->groupBox->addItems(groups); + int index = groups.indexOf(APPLICATION->instances()->getInstanceGroup(m_original->id())); + if (index == -1) + index = 0; + + ui->groupBox->setCurrentIndex(index); + ui->groupBox->lineEdit()->setPlaceholderText(tr("No group")); + ui->copySavesCheckbox->setChecked(m_selectedOptions.isCopySavesEnabled()); + ui->keepPlaytimeCheckbox->setChecked(m_selectedOptions.isKeepPlaytimeEnabled()); + ui->copyGameOptionsCheckbox->setChecked(m_selectedOptions.isCopyGameOptionsEnabled()); + ui->copyResPacksCheckbox->setChecked(m_selectedOptions.isCopyResourcePacksEnabled()); + ui->copyShaderPacksCheckbox->setChecked(m_selectedOptions.isCopyShaderPacksEnabled()); + ui->copyServersCheckbox->setChecked(m_selectedOptions.isCopyServersEnabled()); + ui->copyModsCheckbox->setChecked(m_selectedOptions.isCopyModsEnabled()); + ui->copyScreenshotsCheckbox->setChecked(m_selectedOptions.isCopyScreenshotsEnabled()); + + ui->symbolicLinksCheckbox->setChecked(m_selectedOptions.isUseSymLinksEnabled()); + ui->hardLinksCheckbox->setChecked(m_selectedOptions.isUseHardLinksEnabled()); + + ui->recursiveLinkCheckbox->setChecked(m_selectedOptions.isLinkRecursivelyEnabled()); + ui->dontLinkSavesCheckbox->setChecked(m_selectedOptions.isDontLinkSavesEnabled()); + + auto detectedFS = FS::statFS(m_original->instanceRoot()).fsType; + + m_cloneSupported = FS::canCloneOnFS(detectedFS); + m_linkSupported = FS::canLinkOnFS(detectedFS); + + if (m_cloneSupported) + { + ui->cloneSupportedLabel->setText(tr("Reflinks are supported on %1").arg(FS::getFilesystemTypeName(detectedFS))); + } + else + { + ui->cloneSupportedLabel->setText( + tr("Reflinks aren't supported on %1").arg(FS::getFilesystemTypeName(detectedFS))); + } + +#if defined(Q_OS_WIN) + ui->symbolicLinksCheckbox->setIcon(style()->standardIcon(QStyle::SP_VistaShield)); + ui->symbolicLinksCheckbox->setToolTip(tr("Use symbolic links instead of copying files.") + "\n" + + tr("On Windows, symbolic links may require admin permission to create.")); +#endif + + updateLinkOptions(); + updateUseCloneCheckbox(); + + auto HelpButton = ui->buttonBox->button(QDialogButtonBox::Help); + connect(HelpButton, &QPushButton::clicked, this, &CopyInstanceDialog::help); + HelpButton->setText(tr("Help")); + ui->buttonBox->button(QDialogButtonBox::Cancel)->setText(tr("Cancel")); + ui->buttonBox->button(QDialogButtonBox::Ok)->setText(tr("OK")); +} + +CopyInstanceDialog::~CopyInstanceDialog() +{ + delete ui; +} + +void CopyInstanceDialog::updateDialogState() +{ + auto allowOK = !instName().isEmpty(); + auto OkButton = ui->buttonBox->button(QDialogButtonBox::Ok); + if (OkButton->isEnabled() != allowOK) + { + OkButton->setEnabled(allowOK); + } +} + +QString CopyInstanceDialog::instName() const +{ + auto result = ui->instNameTextBox->text().trimmed(); + if (result.size()) + { + return result; + } + return QString(); +} + +QString CopyInstanceDialog::iconKey() const +{ + return InstIconKey; +} + +QString CopyInstanceDialog::instGroup() const +{ + return ui->groupBox->currentText(); +} + +const InstanceCopyPrefs& CopyInstanceDialog::getChosenOptions() const +{ + return m_selectedOptions; +} + +void CopyInstanceDialog::help() +{ + DesktopServices::openUrl(QUrl(BuildConfig.HELP_URL.arg("instance-copy"))); +} + +void CopyInstanceDialog::checkAllCheckboxes(const bool& b) +{ + ui->keepPlaytimeCheckbox->setChecked(b); + ui->copySavesCheckbox->setChecked(b); + ui->copyGameOptionsCheckbox->setChecked(b); + ui->copyResPacksCheckbox->setChecked(b); + ui->copyShaderPacksCheckbox->setChecked(b); + ui->copyServersCheckbox->setChecked(b); + ui->copyModsCheckbox->setChecked(b); + ui->copyScreenshotsCheckbox->setChecked(b); +} + +// Check the "Select all" checkbox if all options are already selected: +void CopyInstanceDialog::updateSelectAllCheckbox() +{ + ui->selectAllCheckbox->blockSignals(true); + ui->selectAllCheckbox->setChecked(m_selectedOptions.allTrue()); + ui->selectAllCheckbox->blockSignals(false); +} + +void CopyInstanceDialog::updateUseCloneCheckbox() +{ + ui->useCloneCheckbox->setEnabled(m_cloneSupported && !ui->symbolicLinksCheckbox->isChecked() + && !ui->hardLinksCheckbox->isChecked()); + ui->useCloneCheckbox->setChecked(m_cloneSupported && m_selectedOptions.isUseCloneEnabled() + && !ui->symbolicLinksCheckbox->isChecked() && !ui->hardLinksCheckbox->isChecked()); +} + +void CopyInstanceDialog::updateLinkOptions() +{ + ui->symbolicLinksCheckbox->setEnabled(m_linkSupported && !ui->hardLinksCheckbox->isChecked() + && !ui->useCloneCheckbox->isChecked()); + ui->hardLinksCheckbox->setEnabled(m_linkSupported && !ui->symbolicLinksCheckbox->isChecked() + && !ui->useCloneCheckbox->isChecked()); + + ui->symbolicLinksCheckbox->setChecked(m_linkSupported && m_selectedOptions.isUseSymLinksEnabled() + && !ui->useCloneCheckbox->isChecked()); + ui->hardLinksCheckbox->setChecked(m_linkSupported && m_selectedOptions.isUseHardLinksEnabled() + && !ui->useCloneCheckbox->isChecked()); + + bool linksInUse = (ui->symbolicLinksCheckbox->isChecked() || ui->hardLinksCheckbox->isChecked()); + ui->recursiveLinkCheckbox->setEnabled(m_linkSupported && linksInUse && !ui->hardLinksCheckbox->isChecked()); + ui->dontLinkSavesCheckbox->setEnabled(m_linkSupported && linksInUse); + ui->recursiveLinkCheckbox->setChecked(m_linkSupported && linksInUse + && m_selectedOptions.isLinkRecursivelyEnabled()); + ui->dontLinkSavesCheckbox->setChecked(m_linkSupported && linksInUse && m_selectedOptions.isDontLinkSavesEnabled()); + +#if defined(Q_OS_WIN) + auto OkButton = ui->buttonBox->button(QDialogButtonBox::Ok); + OkButton->setIcon(m_selectedOptions.isUseSymLinksEnabled() ? style()->standardIcon(QStyle::SP_VistaShield) + : QIcon()); +#endif +} + +void CopyInstanceDialog::on_iconButton_clicked() +{ + IconPickerDialog dlg(this); + dlg.execWithSelection(InstIconKey); + + if (dlg.result() == QDialog::Accepted) + { + InstIconKey = dlg.selectedIconKey; + ui->iconButton->setIcon(APPLICATION->icons()->getIcon(InstIconKey)); + } +} + +void CopyInstanceDialog::on_instNameTextBox_textChanged([[maybe_unused]] const QString& arg1) +{ + updateDialogState(); +} + +void CopyInstanceDialog::on_selectAllCheckbox_stateChanged(int state) +{ + bool checked; + checked = (state == Qt::Checked); + checkAllCheckboxes(checked); +} + +void CopyInstanceDialog::on_copySavesCheckbox_stateChanged(int state) +{ + m_selectedOptions.enableCopySaves(state == Qt::Checked); + ui->dontLinkSavesCheckbox->setChecked((state == Qt::Checked) && ui->dontLinkSavesCheckbox->isChecked()); + updateSelectAllCheckbox(); +} + +void CopyInstanceDialog::on_keepPlaytimeCheckbox_stateChanged(int state) +{ + m_selectedOptions.enableKeepPlaytime(state == Qt::Checked); + updateSelectAllCheckbox(); +} + +void CopyInstanceDialog::on_copyGameOptionsCheckbox_stateChanged(int state) +{ + m_selectedOptions.enableCopyGameOptions(state == Qt::Checked); + updateSelectAllCheckbox(); +} + +void CopyInstanceDialog::on_copyResPacksCheckbox_stateChanged(int state) +{ + m_selectedOptions.enableCopyResourcePacks(state == Qt::Checked); + updateSelectAllCheckbox(); +} + +void CopyInstanceDialog::on_copyShaderPacksCheckbox_stateChanged(int state) +{ + m_selectedOptions.enableCopyShaderPacks(state == Qt::Checked); + updateSelectAllCheckbox(); +} + +void CopyInstanceDialog::on_copyServersCheckbox_stateChanged(int state) +{ + m_selectedOptions.enableCopyServers(state == Qt::Checked); + updateSelectAllCheckbox(); +} + +void CopyInstanceDialog::on_copyModsCheckbox_stateChanged(int state) +{ + m_selectedOptions.enableCopyMods(state == Qt::Checked); + updateSelectAllCheckbox(); +} + +void CopyInstanceDialog::on_copyScreenshotsCheckbox_stateChanged(int state) +{ + m_selectedOptions.enableCopyScreenshots(state == Qt::Checked); + updateSelectAllCheckbox(); +} + +void CopyInstanceDialog::on_symbolicLinksCheckbox_stateChanged(int state) +{ + m_selectedOptions.enableUseSymLinks(state == Qt::Checked); + updateUseCloneCheckbox(); + updateLinkOptions(); +} + +void CopyInstanceDialog::on_hardLinksCheckbox_stateChanged(int state) +{ + m_selectedOptions.enableUseHardLinks(state == Qt::Checked); + if (state == Qt::Checked && !ui->recursiveLinkCheckbox->isChecked()) + { + ui->recursiveLinkCheckbox->setChecked(true); + } + updateUseCloneCheckbox(); + updateLinkOptions(); +} + +void CopyInstanceDialog::on_recursiveLinkCheckbox_stateChanged(int state) +{ + m_selectedOptions.enableLinkRecursively(state == Qt::Checked); + updateLinkOptions(); +} + +void CopyInstanceDialog::on_dontLinkSavesCheckbox_stateChanged(int state) +{ + m_selectedOptions.enableDontLinkSaves(state == Qt::Checked); +} + +void CopyInstanceDialog::on_useCloneCheckbox_stateChanged(int state) +{ + m_selectedOptions.enableUseClone(m_cloneSupported && (state == Qt::Checked)); + updateUseCloneCheckbox(); + updateLinkOptions(); +} diff --git a/archived/projt-launcher/launcher/ui/dialogs/CopyInstanceDialog.h b/archived/projt-launcher/launcher/ui/dialogs/CopyInstanceDialog.h new file mode 100644 index 0000000000..448616995a --- /dev/null +++ b/archived/projt-launcher/launcher/ui/dialogs/CopyInstanceDialog.h @@ -0,0 +1,102 @@ +// SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0 +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * 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, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * 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 "BaseInstance.h" +#include "BaseVersion.h" +#include "InstanceCopyPrefs.h" + +class BaseInstance; + +namespace Ui +{ + class CopyInstanceDialog; +} + +class CopyInstanceDialog : public QDialog +{ + Q_OBJECT + + public: + explicit CopyInstanceDialog(InstancePtr original, QWidget* parent = 0); + ~CopyInstanceDialog(); + + void updateDialogState(); + + QString instName() const; + QString instGroup() const; + QString iconKey() const; + const InstanceCopyPrefs& getChosenOptions() const; + + public slots: + void help(); + + private slots: + void on_iconButton_clicked(); + void on_instNameTextBox_textChanged(const QString& arg1); + // Checkboxes + void on_selectAllCheckbox_stateChanged(int state); + void on_copySavesCheckbox_stateChanged(int state); + void on_keepPlaytimeCheckbox_stateChanged(int state); + void on_copyGameOptionsCheckbox_stateChanged(int state); + void on_copyResPacksCheckbox_stateChanged(int state); + void on_copyShaderPacksCheckbox_stateChanged(int state); + void on_copyServersCheckbox_stateChanged(int state); + void on_copyModsCheckbox_stateChanged(int state); + void on_copyScreenshotsCheckbox_stateChanged(int state); + void on_symbolicLinksCheckbox_stateChanged(int state); + void on_hardLinksCheckbox_stateChanged(int state); + void on_recursiveLinkCheckbox_stateChanged(int state); + void on_dontLinkSavesCheckbox_stateChanged(int state); + void on_useCloneCheckbox_stateChanged(int state); + + private: + void checkAllCheckboxes(const bool& b); + void updateSelectAllCheckbox(); + void updateUseCloneCheckbox(); + void updateLinkOptions(); + + /* data */ + Ui::CopyInstanceDialog* ui; + QString InstIconKey; + InstancePtr m_original; + InstanceCopyPrefs m_selectedOptions; + bool m_cloneSupported = false; + bool m_linkSupported = false; +}; diff --git a/archived/projt-launcher/launcher/ui/dialogs/CopyInstanceDialog.ui b/archived/projt-launcher/launcher/ui/dialogs/CopyInstanceDialog.ui new file mode 100644 index 0000000000..5060debcf2 --- /dev/null +++ b/archived/projt-launcher/launcher/ui/dialogs/CopyInstanceDialog.ui @@ -0,0 +1,447 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>CopyInstanceDialog</class> + <widget class="QDialog" name="CopyInstanceDialog"> + <property name="windowModality"> + <enum>Qt::ApplicationModal</enum> + </property> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>575</width> + <height>695</height> + </rect> + </property> + <property name="windowTitle"> + <string>Copy Instance</string> + </property> + <property name="windowIcon"> + <iconset> + <normaloff>:/icons/toolbar/copy</normaloff>:/icons/toolbar/copy</iconset> + </property> + <property name="modal"> + <bool>true</bool> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <layout class="QHBoxLayout" name="iconBtnLayout"> + <item> + <spacer name="iconBtnLeftSpacer"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>60</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item> + <widget class="QToolButton" name="iconButton"> + <property name="icon"> + <iconset> + <normaloff>:/icons/instances/grass</normaloff>:/icons/instances/grass</iconset> + </property> + <property name="iconSize"> + <size> + <width>80</width> + <height>80</height> + </size> + </property> + </widget> + </item> + <item> + <spacer name="iconBtnRightSpacer"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>60</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + </layout> + </item> + <item> + <widget class="QLineEdit" name="instNameTextBox"> + <property name="placeholderText"> + <string>Name</string> + </property> + </widget> + </item> + <item> + <widget class="Line" name="line"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + </widget> + </item> + <item> + <layout class="QGridLayout" name="groupDropdownLayout"> + <property name="verticalSpacing"> + <number>6</number> + </property> + <item row="0" column="0"> + <widget class="QLabel" name="labelVersion_3"> + <property name="text"> + <string>&Group</string> + </property> + <property name="buddy"> + <cstring>groupBox</cstring> + </property> + </widget> + </item> + <item row="0" column="1" colspan="2"> + <widget class="QComboBox" name="groupBox"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="editable"> + <bool>true</bool> + </property> + </widget> + </item> + </layout> + </item> + <item> + <widget class="QGroupBox" name="copyOptionsGroup"> + <property name="title"> + <string>Instance Copy Options</string> + </property> + <layout class="QGridLayout" name="copyOptionsLayout"> + <item row="1" column="0"> + <widget class="QCheckBox" name="keepPlaytimeCheckbox"> + <property name="text"> + <string>Keep play time</string> + </property> + </widget> + </item> + <item row="6" column="1"> + <widget class="QCheckBox" name="copyModsCheckbox"> + <property name="toolTip"> + <string>Disabling this will still keep the mod loader (ex: Fabric, Quilt, etc.) but erase the mods folder and their configs.</string> + </property> + <property name="text"> + <string>Copy mods</string> + </property> + </widget> + </item> + <item row="6" column="0"> + <widget class="QCheckBox" name="copyResPacksCheckbox"> + <property name="enabled"> + <bool>true</bool> + </property> + <property name="text"> + <string>Copy resource packs</string> + </property> + </widget> + </item> + <item row="5" column="0"> + <widget class="QCheckBox" name="copyGameOptionsCheckbox"> + <property name="toolTip"> + <string>Copy the in-game options like FOV, max framerate, etc.</string> + </property> + <property name="text"> + <string>Copy game options</string> + </property> + </widget> + </item> + <item row="3" column="1"> + <widget class="QCheckBox" name="copyShaderPacksCheckbox"> + <property name="text"> + <string>Copy shader packs</string> + </property> + </widget> + </item> + <item row="5" column="1"> + <widget class="QCheckBox" name="copyServersCheckbox"> + <property name="text"> + <string>Copy servers</string> + </property> + </widget> + </item> + <item row="3" column="0"> + <widget class="QCheckBox" name="copySavesCheckbox"> + <property name="text"> + <string>Copy saves</string> + </property> + </widget> + </item> + <item row="1" column="1"> + <widget class="QCheckBox" name="copyScreenshotsCheckbox"> + <property name="text"> + <string>Copy screenshots</string> + </property> + </widget> + </item> + <item row="7" column="1"> + <widget class="QCheckBox" name="selectAllCheckbox"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Minimum" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="layoutDirection"> + <enum>Qt::LeftToRight</enum> + </property> + <property name="text"> + <string>Select all</string> + </property> + <property name="checked"> + <bool>false</bool> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item> + <widget class="Line" name="line_2"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + </widget> + </item> + <item> + <widget class="QLabel" name="advancedOptionsLabel"> + <property name="text"> + <string>Advanced Copy Options</string> + </property> + <property name="alignment"> + <set>Qt::AlignCenter</set> + </property> + </widget> + </item> + <item> + <layout class="QVBoxLayout" name="copyModeLayout"> + <item> + <widget class="QGroupBox" name="linkFilesGroup"> + <property name="toolTip"> + <string>Use symbolic or hard links instead of copying files.</string> + </property> + <property name="title"> + <string>Symbolic and Hard Link Options</string> + </property> + <property name="flat"> + <bool>false</bool> + </property> + <property name="checkable"> + <bool>false</bool> + </property> + <property name="checked"> + <bool>false</bool> + </property> + <layout class="QVBoxLayout" name="linkOptionsLayout"> + <item> + <widget class="QLabel" name="linkOptionsLabel"> + <property name="text"> + <string>Links are supported on most filesystems except FAT</string> + </property> + <property name="alignment"> + <set>Qt::AlignCenter</set> + </property> + </widget> + </item> + <item> + <layout class="QGridLayout" name="linkOptionsGridLayout" rowstretch="0,0,0,0" columnstretch="0,0" rowminimumheight="0,0,0,0" columnminimumwidth="0,0"> + <property name="leftMargin"> + <number>6</number> + </property> + <property name="topMargin"> + <number>6</number> + </property> + <property name="rightMargin"> + <number>6</number> + </property> + <property name="bottomMargin"> + <number>6</number> + </property> + <item row="2" column="1"> + <widget class="QCheckBox" name="recursiveLinkCheckbox"> + <property name="enabled"> + <bool>false</bool> + </property> + <property name="toolTip"> + <string>Link each resource individually instead of linking whole folders at once</string> + </property> + <property name="text"> + <string>Link files recursively</string> + </property> + </widget> + </item> + <item row="3" column="1"> + <widget class="QCheckBox" name="dontLinkSavesCheckbox"> + <property name="enabled"> + <bool>false</bool> + </property> + <property name="toolTip"> + <string>If "copy saves" is selected world save data will be copied instead of linked and thus not shared between instances.</string> + </property> + <property name="text"> + <string>Don't link saves</string> + </property> + <property name="checked"> + <bool>false</bool> + </property> + </widget> + </item> + <item row="3" column="0"> + <widget class="QCheckBox" name="hardLinksCheckbox"> + <property name="enabled"> + <bool>true</bool> + </property> + <property name="toolTip"> + <string>Use hard links instead of copying files.</string> + </property> + <property name="text"> + <string>Use hard links</string> + </property> + </widget> + </item> + <item row="2" column="0"> + <widget class="QCheckBox" name="symbolicLinksCheckbox"> + <property name="toolTip"> + <string>Use symbolic links instead of copying files.</string> + </property> + <property name="text"> + <string>Use symbolic links</string> + </property> + </widget> + </item> + </layout> + </item> + </layout> + </widget> + </item> + <item> + <widget class="QGroupBox" name="horizontalGroupBox"> + <property name="title"> + <string>CoW (Copy-on-Write) Options</string> + </property> + <layout class="QHBoxLayout" name="useCloneLayout"> + <item> + <widget class="QCheckBox" name="useCloneCheckbox"> + <property name="enabled"> + <bool>false</bool> + </property> + <property name="toolTip"> + <string>Files cloned with reflinks take up no extra space until they are modified.</string> + </property> + <property name="text"> + <string>Clone instead of copying</string> + </property> + </widget> + </item> + <item> + <spacer name="CoWSpacer"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item> + <widget class="QLabel" name="cloneSupportedLabel"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Preferred" vsizetype="Preferred"> + <horstretch>1</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string>Your filesystem and/or OS doesn't support reflinks</string> + </property> + <property name="alignment"> + <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set> + </property> + <property name="margin"> + <number>4</number> + </property> + </widget> + </item> + </layout> + </widget> + </item> + </layout> + </item> + <item> + <widget class="QDialogButtonBox" name="buttonBox"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="standardButtons"> + <set>QDialogButtonBox::Cancel|QDialogButtonBox::Help|QDialogButtonBox::Ok</set> + </property> + </widget> + </item> + </layout> + </widget> + <tabstops> + <tabstop>iconButton</tabstop> + <tabstop>instNameTextBox</tabstop> + <tabstop>groupBox</tabstop> + <tabstop>keepPlaytimeCheckbox</tabstop> + <tabstop>copyScreenshotsCheckbox</tabstop> + <tabstop>copySavesCheckbox</tabstop> + <tabstop>copyShaderPacksCheckbox</tabstop> + <tabstop>copyGameOptionsCheckbox</tabstop> + <tabstop>copyServersCheckbox</tabstop> + <tabstop>copyResPacksCheckbox</tabstop> + <tabstop>copyModsCheckbox</tabstop> + <tabstop>symbolicLinksCheckbox</tabstop> + <tabstop>recursiveLinkCheckbox</tabstop> + <tabstop>hardLinksCheckbox</tabstop> + <tabstop>dontLinkSavesCheckbox</tabstop> + <tabstop>useCloneCheckbox</tabstop> + </tabstops> + <resources/> + <connections> + <connection> + <sender>buttonBox</sender> + <signal>accepted()</signal> + <receiver>CopyInstanceDialog</receiver> + <slot>accept()</slot> + <hints> + <hint type="sourcelabel"> + <x>269</x> + <y>692</y> + </hint> + <hint type="destinationlabel"> + <x>157</x> + <y>274</y> + </hint> + </hints> + </connection> + <connection> + <sender>buttonBox</sender> + <signal>rejected()</signal> + <receiver>CopyInstanceDialog</receiver> + <slot>reject()</slot> + <hints> + <hint type="sourcelabel"> + <x>337</x> + <y>692</y> + </hint> + <hint type="destinationlabel"> + <x>286</x> + <y>274</y> + </hint> + </hints> + </connection> + </connections> +</ui> diff --git a/archived/projt-launcher/launcher/ui/dialogs/CreateShortcutDialog.cpp b/archived/projt-launcher/launcher/ui/dialogs/CreateShortcutDialog.cpp new file mode 100644 index 0000000000..61e991ec9d --- /dev/null +++ b/archived/projt-launcher/launcher/ui/dialogs/CreateShortcutDialog.cpp @@ -0,0 +1,264 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * 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, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * + * + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> + * Copyright (C) 2023 TheKodeToad <TheKodeToad@proton.me> + * Copyright (C) 2025 Yihe Li <winmikedows@hotmail.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * 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 <QLayout> +#include <QPushButton> + +#include "BuildConfig.h" +#include "CreateShortcutDialog.h" +#include "ui_CreateShortcutDialog.h" + +#include "ui/dialogs/IconPickerDialog.h" + +#include "BaseInstance.h" +#include "DesktopServices.h" +#include "FileSystem.h" +#include "InstanceList.h" +#include "icons/IconList.hpp" + +#include "minecraft/MinecraftInstance.h" +#include "minecraft/ShortcutUtils.h" +#include "minecraft/WorldList.h" +#include "minecraft/auth/AccountList.hpp" + +CreateShortcutDialog::CreateShortcutDialog(InstancePtr instance, QWidget* parent) + : QDialog(parent), + ui(new Ui::CreateShortcutDialog), + m_instance(instance) +{ + ui->setupUi(this); + + InstIconKey = instance->iconKey(); + ui->iconButton->setIcon(APPLICATION->icons()->getIcon(InstIconKey)); + ui->instNameTextBox->setPlaceholderText(instance->name()); + + auto mInst = std::dynamic_pointer_cast<MinecraftInstance>(instance); + m_QuickJoinSupported = mInst && mInst->traits().contains("feature:is_quick_play_singleplayer"); + auto worldList = mInst->worldList(); + worldList->update(); + if (!m_QuickJoinSupported || worldList->empty()) + { + ui->worldTarget->hide(); + ui->worldSelectionBox->hide(); + ui->serverTarget->setChecked(true); + ui->serverTarget->hide(); + ui->serverLabel->show(); + } + + // Populate save targets + if (!DesktopServices::isFlatpak()) + { + QString desktopDir = FS::getDesktopDir(); + QString applicationDir = FS::getApplicationsDir(); + + if (!desktopDir.isEmpty()) + ui->saveTargetSelectionBox->addItem(tr("Desktop"), QVariant::fromValue(ShortcutTarget::Desktop)); + + if (!applicationDir.isEmpty()) + ui->saveTargetSelectionBox->addItem(tr("Applications"), QVariant::fromValue(ShortcutTarget::Applications)); + } + ui->saveTargetSelectionBox->addItem(tr("Other..."), QVariant::fromValue(ShortcutTarget::Other)); + + // Populate worlds + if (m_QuickJoinSupported) + { + for (const auto& world : worldList->allWorlds()) + { + // Entry name: World Name [Game Mode] - Last Played: DateTime + QString entry_name = + tr("%1 [%2] - Last Played: %3") + .arg(world.name(), world.gameType().toTranslatedString(), world.lastPlayed().toString(Qt::ISODate)); + ui->worldSelectionBox->addItem(entry_name, world.name()); + } + } + + // Populate accounts + auto accounts = APPLICATION->accounts(); + MinecraftAccountPtr defaultAccount = accounts->defaultAccount(); + if (accounts->count() <= 0) + { + ui->overrideAccountCheckbox->setEnabled(false); + } + else + { + for (int i = 0; i < accounts->count(); i++) + { + MinecraftAccountPtr account = accounts->at(i); + auto profileLabel = account->profileName(); + if (account->isInUse()) + profileLabel = tr("%1 (in use)").arg(profileLabel); + auto face = account->getFace(); + QIcon icon = face.isNull() ? QIcon::fromTheme("noaccount") : face; + ui->accountSelectionBox->addItem(profileLabel, account->profileName()); + ui->accountSelectionBox->setItemIcon(i, icon); + if (defaultAccount == account) + ui->accountSelectionBox->setCurrentIndex(i); + } + } +} + +CreateShortcutDialog::~CreateShortcutDialog() +{ + delete ui; +} + +void CreateShortcutDialog::on_iconButton_clicked() +{ + IconPickerDialog dlg(this); + dlg.execWithSelection(InstIconKey); + + if (dlg.result() == QDialog::Accepted) + { + InstIconKey = dlg.selectedIconKey; + ui->iconButton->setIcon(APPLICATION->icons()->getIcon(InstIconKey)); + } +} + +void CreateShortcutDialog::on_overrideAccountCheckbox_stateChanged(int state) +{ + ui->accountOptionsGroup->setEnabled(state == Qt::Checked); +} + +void CreateShortcutDialog::on_targetCheckbox_stateChanged(int state) +{ + ui->targetOptionsGroup->setEnabled(state == Qt::Checked); + ui->worldSelectionBox->setEnabled(ui->worldTarget->isChecked()); + ui->serverAddressBox->setEnabled(ui->serverTarget->isChecked()); + stateChanged(); +} + +void CreateShortcutDialog::on_worldTarget_toggled(bool checked) +{ + ui->worldSelectionBox->setEnabled(checked); + stateChanged(); +} + +void CreateShortcutDialog::on_serverTarget_toggled(bool checked) +{ + ui->serverAddressBox->setEnabled(checked); + stateChanged(); +} + +void CreateShortcutDialog::on_worldSelectionBox_currentIndexChanged(int index) +{ + stateChanged(); +} + +void CreateShortcutDialog::on_serverAddressBox_textChanged(const QString& text) +{ + stateChanged(); +} + +void CreateShortcutDialog::stateChanged() +{ + QString result = m_instance->name(); + if (ui->targetCheckbox->isChecked()) + { + if (ui->worldTarget->isChecked()) + result = tr("%1 - %2").arg(result, ui->worldSelectionBox->currentData().toString()); + else if (ui->serverTarget->isChecked()) + result = tr("%1 - Server %2").arg(result, ui->serverAddressBox->text()); + } + ui->instNameTextBox->setPlaceholderText(result); + if (!ui->targetCheckbox->isChecked()) + ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(true); + else + { + ui->buttonBox->button(QDialogButtonBox::Ok) + ->setEnabled((ui->worldTarget->isChecked() && ui->worldSelectionBox->currentIndex() != -1) + || (ui->serverTarget->isChecked() && !ui->serverAddressBox->text().isEmpty())); + } +} + +// Real work +void CreateShortcutDialog::createShortcut() +{ + QString targetString = tr("instance"); + QStringList extraArgs; + if (ui->targetCheckbox->isChecked()) + { + if (ui->worldTarget->isChecked()) + { + targetString = tr("world"); + extraArgs = { "--world", ui->worldSelectionBox->currentData().toString() }; + } + else if (ui->serverTarget->isChecked()) + { + targetString = tr("server"); + extraArgs = { "--server", ui->serverAddressBox->text() }; + } + } + + auto target = ui->saveTargetSelectionBox->currentData().value<ShortcutTarget>(); + auto name = ui->instNameTextBox->text(); + if (name.isEmpty()) + name = ui->instNameTextBox->placeholderText(); + if (ui->overrideAccountCheckbox->isChecked()) + extraArgs.append({ "--profile", ui->accountSelectionBox->currentData().toString() }); + + ShortcutUtils::Shortcut args{ m_instance.get(), name, targetString, this, extraArgs, InstIconKey, target }; + if (target == ShortcutTarget::Desktop) + ShortcutUtils::createInstanceShortcutOnDesktop(args); + else if (target == ShortcutTarget::Applications) + ShortcutUtils::createInstanceShortcutInApplications(args); + else + ShortcutUtils::createInstanceShortcutInOther(args); +} diff --git a/archived/projt-launcher/launcher/ui/dialogs/CreateShortcutDialog.h b/archived/projt-launcher/launcher/ui/dialogs/CreateShortcutDialog.h new file mode 100644 index 0000000000..9e2381170b --- /dev/null +++ b/archived/projt-launcher/launcher/ui/dialogs/CreateShortcutDialog.h @@ -0,0 +1,83 @@ +// SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0 +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * 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, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * 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 "BaseInstance.h" + +class BaseInstance; + +namespace Ui +{ + class CreateShortcutDialog; +} + +class CreateShortcutDialog : public QDialog +{ + Q_OBJECT + + public: + explicit CreateShortcutDialog(InstancePtr instance, QWidget* parent = nullptr); + ~CreateShortcutDialog(); + + void createShortcut(); + + private slots: + // Icon, target and name + void on_iconButton_clicked(); + + // Override account + void on_overrideAccountCheckbox_stateChanged(int state); + + // Override target (world, server) + void on_targetCheckbox_stateChanged(int state); + void on_worldTarget_toggled(bool checked); + void on_serverTarget_toggled(bool checked); + void on_worldSelectionBox_currentIndexChanged(int index); + void on_serverAddressBox_textChanged(const QString& text); + + private: + // Data + Ui::CreateShortcutDialog* ui; + QString InstIconKey; + InstancePtr m_instance; + bool m_QuickJoinSupported = false; + + // Functions + void stateChanged(); +}; diff --git a/archived/projt-launcher/launcher/ui/dialogs/CreateShortcutDialog.ui b/archived/projt-launcher/launcher/ui/dialogs/CreateShortcutDialog.ui new file mode 100644 index 0000000000..24d4dc2dcd --- /dev/null +++ b/archived/projt-launcher/launcher/ui/dialogs/CreateShortcutDialog.ui @@ -0,0 +1,264 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>CreateShortcutDialog</class> + <widget class="QDialog" name="CreateShortcutDialog"> + <property name="windowModality"> + <enum>Qt::WindowModality::ApplicationModal</enum> + </property> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>450</width> + <height>370</height> + </rect> + </property> + <property name="windowTitle"> + <string>Create Instance Shortcut</string> + </property> + <property name="modal"> + <bool>true</bool> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <layout class="QHBoxLayout" name="iconBtnLayout"> + <item> + <widget class="QToolButton" name="iconButton"> + <property name="icon"> + <iconset> + <normaloff>:/icons/instances/grass</normaloff>:/icons/instances/grass</iconset> + </property> + <property name="iconSize"> + <size> + <width>80</width> + <height>80</height> + </size> + </property> + </widget> + </item> + <item> + <layout class="QGridLayout" name="iconBtnGridLayout"> + <item row="0" column="0"> + <widget class="QLabel" name="saveToLabel"> + <property name="text"> + <string>Save To:</string> + </property> + </widget> + </item> + <item row="0" column="1"> + <widget class="QComboBox" name="saveTargetSelectionBox"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + </widget> + </item> + <item row="1" column="0"> + <widget class="QLabel" name="nameLabel"> + <property name="text"> + <string>Name:</string> + </property> + </widget> + </item> + <item row="1" column="1"> + <widget class="QLineEdit" name="instNameTextBox"> + <property name="placeholderText"> + <string>Name</string> + </property> + </widget> + </item> + </layout> + </item> + </layout> + </item> + <item> + <widget class="QCheckBox" name="overrideAccountCheckbox"> + <property name="toolTip"> + <string>Use a different account than the default specified.</string> + </property> + <property name="text"> + <string>Override the default account</string> + </property> + </widget> + </item> + <item> + <widget class="QGroupBox" name="accountOptionsGroup"> + <property name="enabled"> + <bool>false</bool> + </property> + <property name="sizePolicy"> + <sizepolicy hsizetype="Preferred" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <layout class="QVBoxLayout" name="accountOptionsLayout"> + <item> + <widget class="QComboBox" name="accountSelectionBox"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item> + <widget class="QCheckBox" name="targetCheckbox"> + <property name="toolTip"> + <string>Specify a world or server to automatically join on launch.</string> + </property> + <property name="text"> + <string>Select a target to join on launch</string> + </property> + </widget> + </item> + <item> + <widget class="QGroupBox" name="targetOptionsGroup"> + <property name="enabled"> + <bool>false</bool> + </property> + <property name="sizePolicy"> + <sizepolicy hsizetype="Preferred" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <layout class="QGridLayout" name="targetOptionsGridLayout"> + <item row="0" column="0"> + <layout class="QVBoxLayout" name="worldOverlap"> + <property name="spacing"> + <number>0</number> + </property> + <item> + <widget class="QRadioButton" name="worldTarget"> + <property name="text"> + <string>World:</string> + </property> + <attribute name="buttonGroup"> + <string notr="true">targetBtnGroup</string> + </attribute> + </widget> + </item> + </layout> + </item> + <item row="0" column="1"> + <widget class="QComboBox" name="worldSelectionBox"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + </widget> + </item> + <item row="1" column="0"> + <layout class="QVBoxLayout" name="serverOverlap"> + <property name="spacing"> + <number>0</number> + </property> + <item> + <widget class="QRadioButton" name="serverTarget"> + <property name="text"> + <string>Server Address:</string> + </property> + <attribute name="buttonGroup"> + <string notr="true">targetBtnGroup</string> + </attribute> + </widget> + </item> + <item> + <widget class="QLabel" name="serverLabel"> + <property name="visible"> + <bool>false</bool> + </property> + <property name="text"> + <string>Server Address:</string> + </property> + </widget> + </item> + </layout> + </item> + <item row="1" column="1"> + <widget class="QLineEdit" name="serverAddressBox"> + <property name="placeholderText"> + <string>Server Address</string> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item> + <widget class="QLabel" name="noteLabel"> + <property name="text"> + <string>Note: If a shortcut is moved after creation, it won't be deleted when deleting the instance.</string> + </property> + </widget> + </item> + <item> + <widget class="QLabel" name="noteLabel2"> + <property name="text"> + <string>You'll need to delete them manually if that is the case.</string> + </property> + </widget> + </item> + <item> + <widget class="QDialogButtonBox" name="buttonBox"> + <property name="orientation"> + <enum>Qt::Orientation::Horizontal</enum> + </property> + <property name="standardButtons"> + <set>QDialogButtonBox::StandardButton::Cancel|QDialogButtonBox::StandardButton::Ok</set> + </property> + </widget> + </item> + </layout> + </widget> + <tabstops> + <tabstop>iconButton</tabstop> + </tabstops> + <resources/> + <connections> + <connection> + <sender>buttonBox</sender> + <signal>accepted()</signal> + <receiver>CreateShortcutDialog</receiver> + <slot>accept()</slot> + <hints> + <hint type="sourcelabel"> + <x>20</x> + <y>20</y> + </hint> + <hint type="destinationlabel"> + <x>20</x> + <y>20</y> + </hint> + </hints> + </connection> + <connection> + <sender>buttonBox</sender> + <signal>rejected()</signal> + <receiver>CreateShortcutDialog</receiver> + <slot>reject()</slot> + <hints> + <hint type="sourcelabel"> + <x>20</x> + <y>20</y> + </hint> + <hint type="destinationlabel"> + <x>20</x> + <y>20</y> + </hint> + </hints> + </connection> + </connections> + <buttongroups> + <buttongroup name="targetBtnGroup"/> + </buttongroups> +</ui> diff --git a/archived/projt-launcher/launcher/ui/dialogs/CustomMessageBox.cpp b/archived/projt-launcher/launcher/ui/dialogs/CustomMessageBox.cpp new file mode 100644 index 0000000000..783b08dcc0 --- /dev/null +++ b/archived/projt-launcher/launcher/ui/dialogs/CustomMessageBox.cpp @@ -0,0 +1,63 @@ +// SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0 +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * 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, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * 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 "CustomMessageBox.h" + +namespace CustomMessageBox +{ + QMessageBox* selectable(QWidget* parent, + const QString& title, + const QString& text, + QMessageBox::Icon icon, + QMessageBox::StandardButtons buttons, + QMessageBox::StandardButton defaultButton, + QCheckBox* checkBox) + { + QMessageBox* messageBox = new QMessageBox(parent); + messageBox->setWindowTitle(title); + messageBox->setText(text); + messageBox->setStandardButtons(buttons); + messageBox->setDefaultButton(defaultButton); + messageBox->setTextInteractionFlags(Qt::TextSelectableByMouse); + messageBox->setIcon(icon); + messageBox->setTextInteractionFlags(Qt::TextBrowserInteraction); + if (checkBox) + messageBox->setCheckBox(checkBox); + + return messageBox; + } +} // namespace CustomMessageBox diff --git a/archived/projt-launcher/launcher/ui/dialogs/CustomMessageBox.h b/archived/projt-launcher/launcher/ui/dialogs/CustomMessageBox.h new file mode 100644 index 0000000000..2682021cbe --- /dev/null +++ b/archived/projt-launcher/launcher/ui/dialogs/CustomMessageBox.h @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0 +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * 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, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * 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 <QMessageBox> + +namespace CustomMessageBox +{ + QMessageBox* selectable(QWidget* parent, + const QString& title, + const QString& text, + QMessageBox::Icon icon = QMessageBox::NoIcon, + QMessageBox::StandardButtons buttons = QMessageBox::Ok, + QMessageBox::StandardButton defaultButton = QMessageBox::NoButton, + QCheckBox* checkBox = nullptr); +} diff --git a/archived/projt-launcher/launcher/ui/dialogs/ExportInstanceDialog.cpp b/archived/projt-launcher/launcher/ui/dialogs/ExportInstanceDialog.cpp new file mode 100644 index 0000000000..143908ee72 --- /dev/null +++ b/archived/projt-launcher/launcher/ui/dialogs/ExportInstanceDialog.cpp @@ -0,0 +1,240 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * 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, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * + * + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> + * Copyright (C) 2023 TheKodeToad <TheKodeToad@proton.me> + * Copyright (c) 2023 Trial97 <alexandru.tripon97@gmail.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * 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 "ExportInstanceDialog.h" +#include <BaseInstance.h> +#include <MMCZip.h> +#include <QFileDialog> +#include <QFileSystemModel> +#include <QMessageBox> +#include "QObjectPtr.h" +#include "ui/dialogs/CustomMessageBox.h" +#include "ui/dialogs/ProgressDialog.h" +#include "ui/widgets/FileIgnoreProxy.h" +#include "ui_ExportInstanceDialog.h" + +#include <FileSystem.h> +#include <icons/IconList.hpp> +#include <QDebug> +#include <QFileInfo> +#include <QPushButton> +#include <QSaveFile> +#include <QSortFilterProxyModel> +#include <QStack> +#include <functional> +#include "Application.h" +#include "SeparatorPrefixTree.h" + +ExportInstanceDialog::ExportInstanceDialog(InstancePtr instance, QWidget* parent) + : QDialog(parent), + m_ui(new Ui::ExportInstanceDialog), + m_instance(instance) +{ + m_ui->setupUi(this); + auto model = new QFileSystemModel(this); + model->setIconProvider(&m_icons); + auto root = instance->instanceRoot(); + m_proxyModel = new FileIgnoreProxy(root, this); + m_proxyModel->setSourceModel(model); + auto prefix = QDir(instance->instanceRoot()).relativeFilePath(instance->gameRoot()); + for (auto path : { "logs", "crash-reports", ".cache", ".fabric", ".quilt" }) + { + m_proxyModel->ignoreFilesWithPath().insert(FS::PathCombine(prefix, path)); + } + m_proxyModel->ignoreFilesWithName().append({ ".DS_Store", "thumbs.db", "Thumbs.db" }); + m_proxyModel->loadBlockedPathsFromFile(ignoreFileName()); + + m_ui->treeView->setModel(m_proxyModel); + m_ui->treeView->setRootIndex(m_proxyModel->mapFromSource(model->index(root))); + m_ui->treeView->sortByColumn(0, Qt::AscendingOrder); + + connect(m_proxyModel, &QAbstractItemModel::rowsInserted, this, &ExportInstanceDialog::rowsInserted); + + model->setFilter(QDir::AllEntries | QDir::NoDotAndDotDot | QDir::AllDirs | QDir::Hidden); + model->setRootPath(root); + auto headerView = m_ui->treeView->header(); + headerView->setSectionResizeMode(QHeaderView::ResizeToContents); + headerView->setSectionResizeMode(0, QHeaderView::Stretch); + + m_ui->buttonBox->button(QDialogButtonBox::Cancel)->setText(tr("Cancel")); + m_ui->buttonBox->button(QDialogButtonBox::Ok)->setText(tr("OK")); +} + +ExportInstanceDialog::~ExportInstanceDialog() +{ + delete m_ui; +} + +/// Save icon to instance's folder is needed +void SaveIcon(InstancePtr m_instance) +{ + auto iconKey = m_instance->iconKey(); + auto iconList = APPLICATION->icons(); + auto mmcIcon = iconList->icon(iconKey); + if (!mmcIcon || mmcIcon->isBuiltIn()) + { + return; + } + auto path = mmcIcon->getFilePath(); + if (!path.isNull()) + { + QFileInfo inInfo(path); + FS::copy(path, FS::PathCombine(m_instance->instanceRoot(), inInfo.fileName()))(); + return; + } + auto& image = mmcIcon->m_images[mmcIcon->type()]; + auto& icon = image.icon; + auto sizes = icon.availableSizes(); + if (sizes.size() == 0) + { + return; + } + auto areaOf = [](QSize size) { return size.width() * size.height(); }; + QSize largest = sizes[0]; + // find variant with largest area + for (auto size : sizes) + { + if (areaOf(largest) < areaOf(size)) + { + largest = size; + } + } + auto pixmap = icon.pixmap(largest); + pixmap.save(FS::PathCombine(m_instance->instanceRoot(), iconKey + ".png")); +} + +void ExportInstanceDialog::doExport() +{ + auto name = FS::RemoveInvalidFilenameChars(m_instance->name()); + + const QString output = QFileDialog::getSaveFileName(this, + tr("Export %1").arg(m_instance->name()), + FS::PathCombine(QDir::homePath(), name + ".zip"), + "Zip (*.zip)", + nullptr); + if (output.isEmpty()) + { + QDialog::done(QDialog::Rejected); + return; + } + + SaveIcon(m_instance); + + auto files = QFileInfoList(); + if (!MMCZip::collectFileListRecursively( + m_instance->instanceRoot(), + nullptr, + &files, + std::bind(&FileIgnoreProxy::filterFile, m_proxyModel, std::placeholders::_1))) + { + QMessageBox::warning(this, tr("Error"), tr("Unable to export instance")); + QDialog::done(QDialog::Rejected); + return; + } + + auto task = makeShared<MMCZip::ExportToZipTask>(output, m_instance->instanceRoot(), files, "", true, true); + + connect(task.get(), + &Task::failed, + this, + [this, output](QString reason) + { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->show(); }); + connect(task.get(), &Task::finished, this, [task] { task->deleteLater(); }); + + ProgressDialog progress(this); + progress.setSkipButton(true, tr("Abort")); + auto result = progress.execWithTask(*task); + QDialog::done(result); +} + +void ExportInstanceDialog::done(int result) +{ + m_proxyModel->saveBlockedPathsToFile(ignoreFileName()); + if (result == QDialog::Accepted) + { + doExport(); + return; + } + QDialog::done(result); +} + +void ExportInstanceDialog::rowsInserted(QModelIndex parent, int top, int bottom) +{ + // WARNING: possible off-by-one? + for (int i = top; i < bottom; i++) + { + auto node = m_proxyModel->index(i, 0, parent); + if (m_proxyModel->shouldExpand(node)) + { + auto expNode = node.parent(); + if (!expNode.isValid()) + { + continue; + } + m_ui->treeView->expand(node); + } + } +} + +QString ExportInstanceDialog::ignoreFileName() +{ + return FS::PathCombine(m_instance->instanceRoot(), ".packignore"); +} diff --git a/archived/projt-launcher/launcher/ui/dialogs/ExportInstanceDialog.h b/archived/projt-launcher/launcher/ui/dialogs/ExportInstanceDialog.h new file mode 100644 index 0000000000..a3348b9fd3 --- /dev/null +++ b/archived/projt-launcher/launcher/ui/dialogs/ExportInstanceDialog.h @@ -0,0 +1,99 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * 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, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * + * + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2023 TheKodeToad <TheKodeToad@proton.me> + * Copyright (c) 2023 Trial97 <alexandru.tripon97@gmail.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * ======================================================================== */ + +#pragma once + +#include <QDialog> +#include <QModelIndex> +#include <memory> +#include "ui/widgets/FastFileIconProvider.h" +#include "ui/widgets/FileIgnoreProxy.h" + +class BaseInstance; +using InstancePtr = std::shared_ptr<BaseInstance>; + +namespace Ui +{ + class ExportInstanceDialog; +} + +class ExportInstanceDialog : public QDialog +{ + Q_OBJECT + + public: + explicit ExportInstanceDialog(InstancePtr instance, QWidget* parent = 0); + ~ExportInstanceDialog(); + + virtual void done(int result); + + private: + void doExport(); + QString ignoreFileName(); + + private: + Ui::ExportInstanceDialog* m_ui; + InstancePtr m_instance; + FileIgnoreProxy* m_proxyModel; + FastFileIconProvider m_icons; + + private slots: + void rowsInserted(QModelIndex parent, int top, int bottom); +}; diff --git a/archived/projt-launcher/launcher/ui/dialogs/ExportInstanceDialog.ui b/archived/projt-launcher/launcher/ui/dialogs/ExportInstanceDialog.ui new file mode 100644 index 0000000000..bcd4e84a4d --- /dev/null +++ b/archived/projt-launcher/launcher/ui/dialogs/ExportInstanceDialog.ui @@ -0,0 +1,83 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>ExportInstanceDialog</class> + <widget class="QDialog" name="ExportInstanceDialog"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>720</width> + <height>625</height> + </rect> + </property> + <property name="windowTitle"> + <string>Export Instance</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <widget class="QTreeView" name="treeView"> + <property name="alternatingRowColors"> + <bool>true</bool> + </property> + <property name="selectionMode"> + <enum>QAbstractItemView::ExtendedSelection</enum> + </property> + <property name="sortingEnabled"> + <bool>true</bool> + </property> + <attribute name="headerStretchLastSection"> + <bool>false</bool> + </attribute> + </widget> + </item> + <item> + <widget class="QDialogButtonBox" name="buttonBox"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="standardButtons"> + <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set> + </property> + </widget> + </item> + </layout> + </widget> + <tabstops> + <tabstop>treeView</tabstop> + </tabstops> + <resources/> + <connections> + <connection> + <sender>buttonBox</sender> + <signal>accepted()</signal> + <receiver>ExportInstanceDialog</receiver> + <slot>accept()</slot> + <hints> + <hint type="sourcelabel"> + <x>248</x> + <y>254</y> + </hint> + <hint type="destinationlabel"> + <x>157</x> + <y>274</y> + </hint> + </hints> + </connection> + <connection> + <sender>buttonBox</sender> + <signal>rejected()</signal> + <receiver>ExportInstanceDialog</receiver> + <slot>reject()</slot> + <hints> + <hint type="sourcelabel"> + <x>316</x> + <y>260</y> + </hint> + <hint type="destinationlabel"> + <x>286</x> + <y>274</y> + </hint> + </hints> + </connection> + </connections> +</ui> diff --git a/archived/projt-launcher/launcher/ui/dialogs/ExportPackDialog.cpp b/archived/projt-launcher/launcher/ui/dialogs/ExportPackDialog.cpp new file mode 100644 index 0000000000..052804fe37 --- /dev/null +++ b/archived/projt-launcher/launcher/ui/dialogs/ExportPackDialog.cpp @@ -0,0 +1,284 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * 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, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * + * + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2023 TheKodeToad <TheKodeToad@proton.me> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * 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, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * ======================================================================== */ + +#include "ExportPackDialog.h" +#include "minecraft/mod/ResourceFolderModel.hpp" +#include "modplatform/ModIndex.h" +#include "modplatform/flame/FlamePackExportTask.h" +#include "ui/dialogs/CustomMessageBox.h" +#include "ui/dialogs/ProgressDialog.h" +#include "ui_ExportPackDialog.h" + +#include <QFileDialog> +#include <QFileSystemModel> +#include <QJsonDocument> +#include <QMessageBox> +#include <QPushButton> +#include "FileSystem.h" +#include "MMCZip.h" +#include "modplatform/modrinth/ModrinthPackExportTask.h" + +ExportPackDialog::ExportPackDialog(MinecraftInstancePtr instance, + QWidget* parent, + ModPlatform::ResourceProvider provider) + : QDialog(parent), + m_instance(instance), + m_ui(new Ui::ExportPackDialog), + m_provider(provider) +{ + Q_ASSERT(m_provider == ModPlatform::ResourceProvider::MODRINTH + || m_provider == ModPlatform::ResourceProvider::FLAME); + + m_ui->setupUi(this); + m_ui->name->setPlaceholderText(instance->name()); + m_ui->name->setText(instance->settings()->get("ExportName").toString()); + m_ui->version->setText(instance->settings()->get("ExportVersion").toString()); + m_ui->optionalFiles->setChecked(instance->settings()->get("ExportOptionalFiles").toBool()); + + connect(m_ui->recommendedMemoryCheckBox, &QCheckBox::toggled, m_ui->recommendedMemory, &QWidget::setEnabled); + + if (m_provider == ModPlatform::ResourceProvider::MODRINTH) + { + setWindowTitle(tr("Export Modrinth Pack")); + + m_ui->authorLabel->hide(); + m_ui->author->hide(); + + m_ui->recommendedMemoryWidget->hide(); + + m_ui->summary->setPlainText(instance->settings()->get("ExportSummary").toString()); + } + else + { + setWindowTitle(tr("Export CurseForge Pack")); + + m_ui->summaryLabel->hide(); + m_ui->summary->hide(); + + const int recommendedRAM = instance->settings()->get("ExportRecommendedRAM").toInt(); + + if (recommendedRAM > 0) + { + m_ui->recommendedMemoryCheckBox->setChecked(true); + m_ui->recommendedMemory->setValue(recommendedRAM); + } + else + { + m_ui->recommendedMemoryCheckBox->setChecked(false); + + // recommend based on setting - limited to 12 GiB (CurseForge warns above this amount) + const int defaultRecommendation = qMin(m_instance->settings()->get("MaxMemAlloc").toInt(), 1024 * 12); + m_ui->recommendedMemory->setValue(defaultRecommendation); + } + + m_ui->author->setText(instance->settings()->get("ExportAuthor").toString()); + } + + // ensure a valid pack is generated + // the name and version fields mustn't be empty + connect(m_ui->name, &QLineEdit::textEdited, this, &ExportPackDialog::validate); + connect(m_ui->version, &QLineEdit::textEdited, this, &ExportPackDialog::validate); + // the instance name can technically be empty + validate(); + + QFileSystemModel* model = new QFileSystemModel(this); + model->setIconProvider(&m_icons); + + // use the game root - everything outside cannot be exported + const QDir instanceRoot(instance->instanceRoot()); + m_proxy = new FileIgnoreProxy(instance->instanceRoot(), this); + auto prefix = QDir(instance->instanceRoot()).relativeFilePath(instance->gameRoot()); + for (auto path : { "logs", "crash-reports", ".cache", ".fabric", ".quilt" }) + { + m_proxy->ignoreFilesWithPath().insert(FS::PathCombine(prefix, path)); + } + m_proxy->ignoreFilesWithName().append({ ".DS_Store", "thumbs.db", "Thumbs.db" }); + m_proxy->setSourceModel(model); + m_proxy->loadBlockedPathsFromFile(ignoreFileName()); + + const QDir::Filters filter(QDir::AllEntries | QDir::NoDotAndDotDot | QDir::AllDirs | QDir::Hidden); + + MinecraftInstance* mcInstance = dynamic_cast<MinecraftInstance*>(instance.get()); + if (mcInstance) + { + for (auto resourceModel : mcInstance->resourceLists()) + { + if (resourceModel && resourceModel->indexDir().exists()) + m_proxy->ignoreFilesWithPath().insert( + instanceRoot.relativeFilePath(resourceModel->indexDir().absolutePath())); + } + } + + m_ui->files->setModel(m_proxy); + m_ui->files->setRootIndex(m_proxy->mapFromSource(model->index(instance->gameRoot()))); + m_ui->files->sortByColumn(0, Qt::AscendingOrder); + + model->setFilter(filter); + model->setRootPath(instance->gameRoot()); + + QHeaderView* headerView = m_ui->files->header(); + headerView->setSectionResizeMode(QHeaderView::ResizeToContents); + headerView->setSectionResizeMode(0, QHeaderView::Stretch); + + m_ui->buttonBox->button(QDialogButtonBox::Cancel)->setText(tr("Cancel")); + m_ui->buttonBox->button(QDialogButtonBox::Ok)->setText(tr("OK")); +} + +ExportPackDialog::~ExportPackDialog() +{ + delete m_ui; +} + +void ExportPackDialog::done(int result) +{ + m_proxy->saveBlockedPathsToFile(ignoreFileName()); + auto settings = m_instance->settings(); + settings->set("ExportName", m_ui->name->text()); + settings->set("ExportVersion", m_ui->version->text()); + settings->set("ExportOptionalFiles", m_ui->optionalFiles->isChecked()); + + if (m_provider == ModPlatform::ResourceProvider::MODRINTH) + settings->set("ExportSummary", m_ui->summary->toPlainText()); + else + { + settings->set("ExportAuthor", m_ui->author->text()); + + if (m_ui->recommendedMemoryCheckBox->isChecked()) + settings->set("ExportRecommendedRAM", m_ui->recommendedMemory->value()); + else + settings->reset("ExportRecommendedRAM"); + } + + if (result == Accepted) + { + const QString name = m_ui->name->text().isEmpty() ? m_instance->name() : m_ui->name->text(); + const QString filename = FS::RemoveInvalidFilenameChars(name); + + QString output; + if (m_provider == ModPlatform::ResourceProvider::MODRINTH) + { + output = QFileDialog::getSaveFileName(this, + tr("Export %1").arg(name), + FS::PathCombine(QDir::homePath(), filename + ".mrpack"), + tr("Modrinth pack") + " (*.mrpack *.zip)", + nullptr); + if (output.isEmpty()) + return; + if (!(output.endsWith(".zip") || output.endsWith(".mrpack"))) + output.append(".mrpack"); + } + else + { + output = QFileDialog::getSaveFileName(this, + tr("Export %1").arg(name), + FS::PathCombine(QDir::homePath(), filename + ".zip"), + tr("CurseForge pack") + " (*.zip)", + nullptr); + if (output.isEmpty()) + return; + if (!output.endsWith(".zip")) + output.append(".zip"); + } + + Task* task; + if (m_provider == ModPlatform::ResourceProvider::MODRINTH) + { + task = new ModrinthPackExportTask(name, + m_ui->version->text(), + m_ui->summary->toPlainText(), + m_ui->optionalFiles->isChecked(), + m_instance, + output, + std::bind(&FileIgnoreProxy::filterFile, m_proxy, std::placeholders::_1)); + } + else + { + FlamePackExportOptions options{}; + + options.name = name; + options.version = m_ui->version->text(); + options.author = m_ui->author->text(); + options.optionalFiles = m_ui->optionalFiles->isChecked(); + options.instance = m_instance; + options.output = output; + options.filter = std::bind(&FileIgnoreProxy::filterFile, m_proxy, std::placeholders::_1); + options.recommendedRAM = + m_ui->recommendedMemoryCheckBox->isChecked() ? m_ui->recommendedMemory->value() : 0; + + task = new FlamePackExportTask(std::move(options)); + } + + connect(task, + &Task::failed, + [this](const QString reason) + { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->show(); }); + connect(task, + &Task::aborted, + [this] + { + CustomMessageBox::selectable(this, + tr("Task aborted"), + tr("The task has been aborted by the user."), + QMessageBox::Information) + ->show(); + }); + connect(task, &Task::finished, [task] { task->deleteLater(); }); + + ProgressDialog progress(this); + progress.setSkipButton(true, tr("Abort")); + if (progress.execWithTask(*task) != QDialog::Accepted) + return; + } + + QDialog::done(result); +} + +void ExportPackDialog::validate() +{ + m_ui->buttonBox->button(QDialogButtonBox::Ok) + ->setDisabled(m_provider == ModPlatform::ResourceProvider::MODRINTH && m_ui->version->text().isEmpty()); +} + +QString ExportPackDialog::ignoreFileName() +{ + return FS::PathCombine(m_instance->instanceRoot(), ".packignore"); +} diff --git a/archived/projt-launcher/launcher/ui/dialogs/ExportPackDialog.h b/archived/projt-launcher/launcher/ui/dialogs/ExportPackDialog.h new file mode 100644 index 0000000000..31d74c2828 --- /dev/null +++ b/archived/projt-launcher/launcher/ui/dialogs/ExportPackDialog.h @@ -0,0 +1,79 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * 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, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * + * + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2023 TheKodeToad <TheKodeToad@proton.me> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * 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, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * ======================================================================== */ + +#pragma once + +#include <QDialog> +#include "BaseInstance.h" +#include "minecraft/MinecraftInstance.h" +#include "modplatform/ModIndex.h" +#include "ui/widgets/FastFileIconProvider.h" +#include "ui/widgets/FileIgnoreProxy.h" + +namespace Ui +{ + class ExportPackDialog; +} + +class ExportPackDialog : public QDialog +{ + Q_OBJECT + + public: + explicit ExportPackDialog(MinecraftInstancePtr instance, + QWidget* parent = nullptr, + ModPlatform::ResourceProvider provider = ModPlatform::ResourceProvider::MODRINTH); + ~ExportPackDialog(); + + void done(int result) override; + void validate(); + + private: + QString ignoreFileName(); + + private: + const MinecraftInstancePtr m_instance; + Ui::ExportPackDialog* m_ui; + FileIgnoreProxy* m_proxy; + FastFileIconProvider m_icons; + const ModPlatform::ResourceProvider m_provider; +}; diff --git a/archived/projt-launcher/launcher/ui/dialogs/ExportPackDialog.ui b/archived/projt-launcher/launcher/ui/dialogs/ExportPackDialog.ui new file mode 100644 index 0000000000..bda8b8dd0a --- /dev/null +++ b/archived/projt-launcher/launcher/ui/dialogs/ExportPackDialog.ui @@ -0,0 +1,267 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>ExportPackDialog</class> + <widget class="QDialog" name="ExportPackDialog"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>650</width> + <height>532</height> + </rect> + </property> + <property name="sizeGripEnabled"> + <bool>true</bool> + </property> + <layout class="QVBoxLayout" name="verticalLayout_2"> + <item> + <widget class="QGroupBox" name="information"> + <property name="title"> + <string>&Description</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <layout class="QFormLayout" name="formLayout"> + <property name="labelAlignment"> + <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter</set> + </property> + <item row="0" column="0"> + <widget class="QLabel" name="nameLabel"> + <property name="text"> + <string>&Name:</string> + </property> + <property name="buddy"> + <cstring>name</cstring> + </property> + </widget> + </item> + <item row="0" column="1"> + <widget class="QLineEdit" name="name"/> + </item> + <item row="1" column="0"> + <widget class="QLabel" name="versionLabel"> + <property name="text"> + <string>&Version:</string> + </property> + <property name="buddy"> + <cstring>version</cstring> + </property> + </widget> + </item> + <item row="1" column="1"> + <widget class="QLineEdit" name="version"> + <property name="text"> + <string>1.0.0</string> + </property> + </widget> + </item> + <item row="2" column="0"> + <widget class="QLabel" name="authorLabel"> + <property name="text"> + <string>&Author:</string> + </property> + <property name="buddy"> + <cstring>author</cstring> + </property> + </widget> + </item> + <item row="2" column="1"> + <widget class="QLineEdit" name="author"/> + </item> + </layout> + </item> + <item> + <widget class="QLabel" name="summaryLabel"> + <property name="text"> + <string>&Summary</string> + </property> + <property name="buddy"> + <cstring>summary</cstring> + </property> + </widget> + </item> + <item> + <widget class="QPlainTextEdit" name="summary"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="minimumSize"> + <size> + <width>0</width> + <height>100</height> + </size> + </property> + <property name="maximumSize"> + <size> + <width>16777215</width> + <height>100</height> + </size> + </property> + <property name="tabChangesFocus"> + <bool>true</bool> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item> + <widget class="QGroupBox" name="options"> + <property name="title"> + <string>&Options</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout_3"> + <item> + <widget class="QWidget" name="recommendedMemoryWidget" native="true"> + <layout class="QHBoxLayout" name="horizontalLayout"> + <property name="leftMargin"> + <number>0</number> + </property> + <property name="topMargin"> + <number>0</number> + </property> + <property name="rightMargin"> + <number>0</number> + </property> + <property name="bottomMargin"> + <number>0</number> + </property> + <item> + <widget class="QCheckBox" name="recommendedMemoryCheckBox"> + <property name="text"> + <string>&Recommended Memory:</string> + </property> + </widget> + </item> + <item> + <widget class="QSpinBox" name="recommendedMemory"> + <property name="enabled"> + <bool>false</bool> + </property> + <property name="sizePolicy"> + <sizepolicy hsizetype="Maximum" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="suffix"> + <string> MiB</string> + </property> + <property name="minimum"> + <number>8</number> + </property> + <property name="maximum"> + <number>32768</number> + </property> + <property name="singleStep"> + <number>128</number> + </property> + </widget> + </item> + <item> + <spacer name="horizontalSpacer"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + </layout> + </widget> + </item> + <item> + <widget class="QLabel" name="filesLabel"> + <property name="text"> + <string>&Files</string> + </property> + <property name="buddy"> + <cstring>files</cstring> + </property> + </widget> + </item> + <item> + <widget class="QTreeView" name="files"> + <property name="alternatingRowColors"> + <bool>true</bool> + </property> + <property name="selectionMode"> + <enum>QAbstractItemView::ExtendedSelection</enum> + </property> + <property name="sortingEnabled"> + <bool>true</bool> + </property> + <attribute name="headerStretchLastSection"> + <bool>false</bool> + </attribute> + </widget> + </item> + <item> + <widget class="QCheckBox" name="optionalFiles"> + <property name="text"> + <string>&Mark disabled files as optional</string> + </property> + <property name="checked"> + <bool>true</bool> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item> + <widget class="QDialogButtonBox" name="buttonBox"> + <property name="standardButtons"> + <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set> + </property> + </widget> + </item> + </layout> + </widget> + <tabstops> + <tabstop>files</tabstop> + <tabstop>optionalFiles</tabstop> + </tabstops> + <resources/> + <connections> + <connection> + <sender>buttonBox</sender> + <signal>accepted()</signal> + <receiver>ExportPackDialog</receiver> + <slot>accept()</slot> + <hints> + <hint type="sourcelabel"> + <x>324</x> + <y>390</y> + </hint> + <hint type="destinationlabel"> + <x>324</x> + <y>206</y> + </hint> + </hints> + </connection> + <connection> + <sender>buttonBox</sender> + <signal>rejected()</signal> + <receiver>ExportPackDialog</receiver> + <slot>reject()</slot> + <hints> + <hint type="sourcelabel"> + <x>324</x> + <y>390</y> + </hint> + <hint type="destinationlabel"> + <x>324</x> + <y>206</y> + </hint> + </hints> + </connection> + </connections> +</ui> diff --git a/archived/projt-launcher/launcher/ui/dialogs/ExportToModListDialog.cpp b/archived/projt-launcher/launcher/ui/dialogs/ExportToModListDialog.cpp new file mode 100644 index 0000000000..19c3a84ad6 --- /dev/null +++ b/archived/projt-launcher/launcher/ui/dialogs/ExportToModListDialog.cpp @@ -0,0 +1,264 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * 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, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * + * + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2023 Trial97 <alexandru.tripon97@gmail.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * ======================================================================== */ + +#include "ExportToModListDialog.h" +#include <QCheckBox> +#include <QComboBox> +#include <QTextEdit> +#include "FileSystem.h" +#include "Markdown.h" +#include "StringUtils.h" +#include "modplatform/helpers/ExportToModList.h" +#include "ui_ExportToModListDialog.h" + +#include <QFileDialog> +#include <QFileSystemModel> +#include <QJsonDocument> +#include <QMessageBox> +#include <QPushButton> + +const QHash<ExportToModList::Formats, QString> ExportToModListDialog::exampleLines = { + { ExportToModList::HTML, "<li><a href=\"{url}\">{name}</a> [{version}] by {authors}</li>" }, + { ExportToModList::MARKDOWN, "[{name}]({url}) [{version}] by {authors}" }, + { ExportToModList::PLAINTXT, "{name} ({url}) [{version}] by {authors}" }, + { ExportToModList::JSON, + "{\"name\":\"{name}\",\"url\":\"{url}\",\"version\":\"{version}\",\"authors\":\"{authors}\"}," }, + { ExportToModList::CSV, "{name},{url},{version},\"{authors}\"" }, +}; + +ExportToModListDialog::ExportToModListDialog(QString name, QList<Mod*> mods, QWidget* parent) + : QDialog(parent), + m_mods(mods), + m_template_changed(false), + m_name(name), + ui(new Ui::ExportToModListDialog) +{ + ui->setupUi(this); + enableCustom(false); + + connect(ui->formatComboBox, &QComboBox::currentIndexChanged, this, &ExportToModListDialog::formatChanged); + connect(ui->authorsCheckBox, &QCheckBox::stateChanged, this, &ExportToModListDialog::trigger); + connect(ui->versionCheckBox, &QCheckBox::stateChanged, this, &ExportToModListDialog::trigger); + connect(ui->urlCheckBox, &QCheckBox::stateChanged, this, &ExportToModListDialog::trigger); + connect(ui->filenameCheckBox, &QCheckBox::stateChanged, this, &ExportToModListDialog::trigger); + connect(ui->authorsButton, &QPushButton::clicked, this, [this](bool) { addExtra(ExportToModList::Authors); }); + connect(ui->versionButton, &QPushButton::clicked, this, [this](bool) { addExtra(ExportToModList::Version); }); + connect(ui->urlButton, &QPushButton::clicked, this, [this](bool) { addExtra(ExportToModList::Url); }); + connect(ui->filenameButton, &QPushButton::clicked, this, [this](bool) { addExtra(ExportToModList::FileName); }); + connect(ui->templateText, + &QTextEdit::textChanged, + this, + [this] + { + if (ui->templateText->toPlainText() != exampleLines[m_format]) + ui->formatComboBox->setCurrentIndex(5); + triggerImp(); + }); + connect(ui->copyButton, + &QPushButton::clicked, + this, + [this](bool) + { + this->ui->finalText->selectAll(); + this->ui->finalText->copy(); + }); + + ui->buttonBox->button(QDialogButtonBox::Cancel)->setText(tr("Cancel")); + ui->buttonBox->button(QDialogButtonBox::Save)->setText(tr("Save")); + triggerImp(); +} + +ExportToModListDialog::~ExportToModListDialog() +{ + delete ui; +} + +void ExportToModListDialog::formatChanged(int index) +{ + switch (index) + { + case 0: + { + enableCustom(false); + ui->resultText->show(); + m_format = ExportToModList::HTML; + break; + } + case 1: + { + enableCustom(false); + ui->resultText->show(); + m_format = ExportToModList::MARKDOWN; + break; + } + case 2: + { + enableCustom(false); + ui->resultText->hide(); + m_format = ExportToModList::PLAINTXT; + break; + } + case 3: + { + enableCustom(false); + ui->resultText->hide(); + m_format = ExportToModList::JSON; + break; + } + case 4: + { + enableCustom(false); + ui->resultText->hide(); + m_format = ExportToModList::CSV; + break; + } + case 5: + { + m_template_changed = true; + enableCustom(true); + ui->resultText->hide(); + m_format = ExportToModList::CUSTOM; + break; + } + } + triggerImp(); +} + +void ExportToModListDialog::triggerImp() +{ + if (m_format == ExportToModList::CUSTOM) + { + ui->finalText->setPlainText(ExportToModList::exportToModList(m_mods, ui->templateText->toPlainText())); + return; + } + auto opt = 0; + if (ui->authorsCheckBox->isChecked()) + opt |= ExportToModList::Authors; + if (ui->versionCheckBox->isChecked()) + opt |= ExportToModList::Version; + if (ui->urlCheckBox->isChecked()) + opt |= ExportToModList::Url; + if (ui->filenameCheckBox->isChecked()) + opt |= ExportToModList::FileName; + auto txt = ExportToModList::exportToModList(m_mods, m_format, static_cast<ExportToModList::OptionalData>(opt)); + ui->finalText->setPlainText(txt); + switch (m_format) + { + case ExportToModList::CUSTOM: return; + case ExportToModList::HTML: ui->resultText->setHtml(StringUtils::htmlListPatch(txt)); break; + case ExportToModList::MARKDOWN: ui->resultText->setHtml(StringUtils::htmlListPatch(markdownToHTML(txt))); break; + case ExportToModList::PLAINTXT: break; + case ExportToModList::JSON: break; + case ExportToModList::CSV: break; + } + auto exampleLine = exampleLines[m_format]; + if (!m_template_changed && ui->templateText->toPlainText() != exampleLine) + ui->templateText->setPlainText(exampleLine); +} + +void ExportToModListDialog::done(int result) +{ + if (result == Accepted) + { + const QString filename = FS::RemoveInvalidFilenameChars(m_name); + const QString output = QFileDialog::getSaveFileName(this, + tr("Export %1").arg(m_name), + FS::PathCombine(QDir::homePath(), filename + extension()), + tr("File") + " (*.txt *.html *.md *.json *.csv)", + nullptr); + + if (output.isEmpty()) + return; + + try + { + FS::write(output, ui->finalText->toPlainText().toUtf8()); + } + catch (const FS::FileSystemException& e) + { + qCritical() << "Failed to save mod list file :" << e.cause(); + } + } + + QDialog::done(result); +} + +QString ExportToModListDialog::extension() +{ + switch (m_format) + { + case ExportToModList::HTML: return ".html"; + case ExportToModList::MARKDOWN: return ".md"; + case ExportToModList::PLAINTXT: return ".txt"; + case ExportToModList::CUSTOM: return ".txt"; + case ExportToModList::JSON: return ".json"; + case ExportToModList::CSV: return ".csv"; + } + return ".txt"; +} + +void ExportToModListDialog::addExtra(ExportToModList::OptionalData option) +{ + if (m_format != ExportToModList::CUSTOM) + return; + switch (option) + { + case ExportToModList::Authors: ui->templateText->insertPlainText("{authors}"); break; + case ExportToModList::Url: ui->templateText->insertPlainText("{url}"); break; + case ExportToModList::Version: ui->templateText->insertPlainText("{version}"); break; + case ExportToModList::FileName: ui->templateText->insertPlainText("{filename}"); break; + } +} +void ExportToModListDialog::enableCustom(bool enabled) +{ + ui->authorsCheckBox->setHidden(enabled); + ui->authorsButton->setHidden(!enabled); + + ui->versionCheckBox->setHidden(enabled); + ui->versionButton->setHidden(!enabled); + + ui->urlCheckBox->setHidden(enabled); + ui->urlButton->setHidden(!enabled); + + ui->filenameCheckBox->setHidden(enabled); + ui->filenameButton->setHidden(!enabled); +} diff --git a/archived/projt-launcher/launcher/ui/dialogs/ExportToModListDialog.h b/archived/projt-launcher/launcher/ui/dialogs/ExportToModListDialog.h new file mode 100644 index 0000000000..05e7c7b2d8 --- /dev/null +++ b/archived/projt-launcher/launcher/ui/dialogs/ExportToModListDialog.h @@ -0,0 +1,84 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * 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, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * + * + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2023 Trial97 <alexandru.tripon97@gmail.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * ======================================================================== */ + +#pragma once + +#include <QDialog> +#include <QList> +#include "minecraft/mod/Mod.hpp" +#include "modplatform/helpers/ExportToModList.h" + +namespace Ui +{ + class ExportToModListDialog; +} + +class ExportToModListDialog : public QDialog +{ + Q_OBJECT + + public: + explicit ExportToModListDialog(QString name, QList<Mod*> mods, QWidget* parent = nullptr); + ~ExportToModListDialog(); + + void done(int result) override; + + protected slots: + void formatChanged(int index); + void triggerImp(); + void trigger(int) + { + triggerImp(); + }; + void addExtra(ExportToModList::OptionalData option); + + private: + QString extension(); + void enableCustom(bool enabled); + + QList<Mod*> m_mods; + bool m_template_changed; + QString m_name; + ExportToModList::Formats m_format = ExportToModList::Formats::HTML; + Ui::ExportToModListDialog* ui; + static const QHash<ExportToModList::Formats, QString> exampleLines; +}; diff --git a/archived/projt-launcher/launcher/ui/dialogs/ExportToModListDialog.ui b/archived/projt-launcher/launcher/ui/dialogs/ExportToModListDialog.ui new file mode 100644 index 0000000000..ec049d7e76 --- /dev/null +++ b/archived/projt-launcher/launcher/ui/dialogs/ExportToModListDialog.ui @@ -0,0 +1,276 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>ExportToModListDialog</class> + <widget class="QDialog" name="ExportToModListDialog"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>650</width> + <height>522</height> + </rect> + </property> + <property name="windowTitle"> + <string>Export Pack to ModList</string> + </property> + <property name="sizeGripEnabled"> + <bool>true</bool> + </property> + <layout class="QVBoxLayout" name="verticalLayout_2"> + <item> + <layout class="QVBoxLayout" name="verticalLayout" stretch="0,0,0"> + <item> + <widget class="QGroupBox" name="groupBox_3"> + <property name="title"> + <string>Settings</string> + </property> + <layout class="QGridLayout" name="gridLayout"> + <item row="0" column="1"> + <widget class="QComboBox" name="formatComboBox"> + <item> + <property name="text"> + <string>HTML</string> + </property> + </item> + <item> + <property name="text"> + <string>Markdown</string> + </property> + </item> + <item> + <property name="text"> + <string>Plaintext</string> + </property> + </item> + <item> + <property name="text"> + <string>JSON</string> + </property> + </item> + <item> + <property name="text"> + <string>CSV</string> + </property> + </item> + <item> + <property name="text"> + <string>Custom</string> + </property> + </item> + </widget> + </item> + <item row="1" column="0"> + <widget class="QGroupBox" name="templateGroup"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Preferred" vsizetype="Maximum"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="title"> + <string>Template</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout_4"> + <item> + <widget class="QTextEdit" name="templateText"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Maximum"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="toolTip"> + <string>This text supports the following placeholders: {name} - Mod name {mod_id} - Mod ID {url} - Mod URL {version} - Mod version {authors} - Mod authors</string> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item row="1" column="1"> + <widget class="QGroupBox" name="optionsGroup"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Preferred" vsizetype="MinimumExpanding"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="title"> + <string>Optional Info</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout_5"> + <item> + <widget class="QCheckBox" name="versionCheckBox"> + <property name="text"> + <string>Version</string> + </property> + </widget> + </item> + <item> + <widget class="QCheckBox" name="authorsCheckBox"> + <property name="text"> + <string>Authors</string> + </property> + </widget> + </item> + <item> + <widget class="QCheckBox" name="urlCheckBox"> + <property name="text"> + <string>URL</string> + </property> + </widget> + </item> + <item> + <widget class="QCheckBox" name="filenameCheckBox"> + <property name="text"> + <string>Filename</string> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="versionButton"> + <property name="text"> + <string>Version</string> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="authorsButton"> + <property name="text"> + <string>Authors</string> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="urlButton"> + <property name="text"> + <string>URL</string> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="filenameButton"> + <property name="text"> + <string>Filename</string> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item row="0" column="0"> + <widget class="QLabel" name="label"> + <property name="frameShape"> + <enum>QFrame::NoFrame</enum> + </property> + <property name="frameShadow"> + <enum>QFrame::Plain</enum> + </property> + <property name="lineWidth"> + <number>1</number> + </property> + <property name="text"> + <string>Format</string> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item> + <widget class="QGroupBox" name="groupBox_4"> + <property name="title"> + <string>Result</string> + </property> + <layout class="QHBoxLayout" name="horizontalLayout"> + <item> + <widget class="QPlainTextEdit" name="finalText"> + <property name="minimumSize"> + <size> + <width>0</width> + <height>143</height> + </size> + </property> + <property name="readOnly"> + <bool>true</bool> + </property> + </widget> + </item> + <item> + <widget class="QTextBrowser" name="resultText"> + <property name="openExternalLinks"> + <bool>true</bool> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item> + <widget class="QLabel" name="warningLabel"> + <property name="text"> + <string>This depends on the mods' metadata. To ensure it is available, run an update on the instance. Installing the updates isn't necessary.</string> + </property> + <property name="wordWrap"> + <bool>true</bool> + </property> + </widget> + </item> + </layout> + </item> + <item> + <layout class="QHBoxLayout" name="horizontalLayout_2"> + <item> + <widget class="QPushButton" name="copyButton"> + <property name="text"> + <string>Copy</string> + </property> + </widget> + </item> + <item> + <widget class="QDialogButtonBox" name="buttonBox"> + <property name="standardButtons"> + <set>QDialogButtonBox::Cancel|QDialogButtonBox::Save</set> + </property> + </widget> + </item> + </layout> + </item> + </layout> + </widget> + <resources/> + <connections> + <connection> + <sender>buttonBox</sender> + <signal>accepted()</signal> + <receiver>ExportToModListDialog</receiver> + <slot>accept()</slot> + <hints> + <hint type="sourcelabel"> + <x>334</x> + <y>435</y> + </hint> + <hint type="destinationlabel"> + <x>324</x> + <y>206</y> + </hint> + </hints> + </connection> + <connection> + <sender>buttonBox</sender> + <signal>rejected()</signal> + <receiver>ExportToModListDialog</receiver> + <slot>reject()</slot> + <hints> + <hint type="sourcelabel"> + <x>324</x> + <y>390</y> + </hint> + <hint type="destinationlabel"> + <x>324</x> + <y>206</y> + </hint> + </hints> + </connection> + </connections> +</ui> diff --git a/archived/projt-launcher/launcher/ui/dialogs/IconPickerDialog.cpp b/archived/projt-launcher/launcher/ui/dialogs/IconPickerDialog.cpp new file mode 100644 index 0000000000..450d1235ea --- /dev/null +++ b/archived/projt-launcher/launcher/ui/dialogs/IconPickerDialog.cpp @@ -0,0 +1,217 @@ +// SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0 +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * 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, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * 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 <QFileDialog> +#include <QKeyEvent> +#include <QLineEdit> +#include <QPushButton> +#include <QSortFilterProxyModel> + +#include "Application.h" + +#include "IconPickerDialog.h" +#include "ui_IconPickerDialog.h" + +#include "ui/instanceview/InstanceDelegate.h" + +#include <DesktopServices.h> +#include "icons/IconList.hpp" +#include "icons/IconUtils.hpp" + +IconPickerDialog::IconPickerDialog(QWidget* parent) : QDialog(parent), ui(new Ui::IconPickerDialog) +{ + ui->setupUi(this); + setWindowModality(Qt::WindowModal); + + searchBar = new QLineEdit(this); + searchBar->setPlaceholderText(tr("Search...")); + ui->verticalLayout->insertWidget(0, searchBar); + + proxyModel = new QSortFilterProxyModel(this); + proxyModel->setSourceModel(APPLICATION->icons().get()); + proxyModel->setFilterCaseSensitivity(Qt::CaseInsensitive); + ui->iconView->setModel(proxyModel); + + auto contentsWidget = ui->iconView; + contentsWidget->setViewMode(QListView::IconMode); + contentsWidget->setFlow(QListView::LeftToRight); + contentsWidget->setIconSize(QSize(48, 48)); + contentsWidget->setMovement(QListView::Static); + contentsWidget->setResizeMode(QListView::Adjust); + contentsWidget->setSelectionMode(QAbstractItemView::SingleSelection); + contentsWidget->setSpacing(5); + contentsWidget->setWordWrap(false); + contentsWidget->setWrapping(true); + contentsWidget->setUniformItemSizes(true); + contentsWidget->setTextElideMode(Qt::ElideRight); + contentsWidget->setVerticalScrollMode(QAbstractItemView::ScrollPerPixel); + contentsWidget->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + contentsWidget->setItemDelegate(new ListViewDelegate(contentsWidget)); + + // contentsWidget->setAcceptDrops(true); + contentsWidget->setDropIndicatorShown(true); + contentsWidget->viewport()->setAcceptDrops(true); + contentsWidget->setDragDropMode(QAbstractItemView::DropOnly); + contentsWidget->setDefaultDropAction(Qt::CopyAction); + + contentsWidget->installEventFilter(this); + + contentsWidget->setModel(proxyModel); + + // NOTE: ResetRole forces the button to be on the left, while the OK/Cancel ones are on the right. We win. + auto buttonAdd = ui->buttonBox->addButton(tr("Add Icon"), QDialogButtonBox::ResetRole); + buttonRemove = ui->buttonBox->addButton(tr("Remove Icon"), QDialogButtonBox::ResetRole); + + ui->buttonBox->button(QDialogButtonBox::Cancel)->setText(tr("Cancel")); + ui->buttonBox->button(QDialogButtonBox::Ok)->setText(tr("OK")); + + connect(buttonAdd, &QPushButton::clicked, this, &IconPickerDialog::addNewIcon); + connect(buttonRemove, &QPushButton::clicked, this, &IconPickerDialog::removeSelectedIcon); + + connect(contentsWidget, &QAbstractItemView::doubleClicked, this, &IconPickerDialog::activated); + + connect(contentsWidget->selectionModel(), + &QItemSelectionModel::selectionChanged, + this, + &IconPickerDialog::selectionChanged); + + auto buttonFolder = ui->buttonBox->addButton(tr("Open Folder"), QDialogButtonBox::ResetRole); + connect(buttonFolder, &QPushButton::clicked, this, &IconPickerDialog::openFolder); + connect(searchBar, &QLineEdit::textChanged, this, &IconPickerDialog::filterIcons); + // Prevent incorrect indices from e.g. filesystem changes + connect(APPLICATION->icons().get(), + &projt::icons::IconList::iconUpdated, + this, + [this]() { proxyModel->invalidate(); }); +} + +bool IconPickerDialog::eventFilter(QObject* obj, QEvent* evt) +{ + if (obj != ui->iconView) + return QDialog::eventFilter(obj, evt); + if (evt->type() != QEvent::KeyPress) + { + return QDialog::eventFilter(obj, evt); + } + QKeyEvent* keyEvent = static_cast<QKeyEvent*>(evt); + switch (keyEvent->key()) + { + case Qt::Key_Delete: removeSelectedIcon(); return true; + case Qt::Key_Plus: addNewIcon(); return true; + default: break; + } + return QDialog::eventFilter(obj, evt); +} + +void IconPickerDialog::addNewIcon() +{ + //: The title of the select icons open file dialog + QString selectIcons = tr("Select Icons"); + //: The type of icon files + auto filter = projt::icons::getIconFilter(); + QStringList fileNames = QFileDialog::getOpenFileNames(this, selectIcons, QString(), tr("Icons %1").arg(filter)); + APPLICATION->icons()->installIcons(fileNames); +} + +void IconPickerDialog::removeSelectedIcon() +{ + if (APPLICATION->icons()->trashIcon(selectedIconKey)) + return; + + APPLICATION->icons()->deleteIcon(selectedIconKey); +} + +void IconPickerDialog::activated(QModelIndex index) +{ + selectedIconKey = index.data(Qt::UserRole).toString(); + accept(); +} + +void IconPickerDialog::selectionChanged(QItemSelection selected, QItemSelection deselected) +{ + if (selected.empty()) + return; + + QString key = selected.first().indexes().first().data(Qt::UserRole).toString(); + if (!key.isEmpty()) + { + selectedIconKey = key; + } + buttonRemove->setEnabled(APPLICATION->icons()->iconFileExists(selectedIconKey)); +} + +int IconPickerDialog::execWithSelection(QString selection) +{ + auto list = APPLICATION->icons(); + auto contentsWidget = ui->iconView; + selectedIconKey = selection; + + int index_nr = list->getIconIndex(selection); + auto model_index = list->index(index_nr); + auto proxyIndex = proxyModel->mapFromSource(model_index); + + if (proxyIndex.isValid()) + { + contentsWidget->selectionModel()->select(proxyIndex, + QItemSelectionModel::Current | QItemSelectionModel::Select); + QMetaObject::invokeMethod(this, "delayed_scroll", Qt::QueuedConnection, Q_ARG(QModelIndex, proxyIndex)); + } + return QDialog::exec(); +} + +void IconPickerDialog::delayed_scroll(QModelIndex proxy_index) +{ + if (proxy_index.isValid()) + { + ui->iconView->scrollTo(proxy_index); + } +} + +IconPickerDialog::~IconPickerDialog() +{ + delete ui; +} + +void IconPickerDialog::openFolder() +{ + DesktopServices::openPath(APPLICATION->icons()->iconDirectory(selectedIconKey), true); +} + +void IconPickerDialog::filterIcons(const QString& query) +{ + proxyModel->setFilterFixedString(query); +} diff --git a/archived/projt-launcher/launcher/ui/dialogs/IconPickerDialog.h b/archived/projt-launcher/launcher/ui/dialogs/IconPickerDialog.h new file mode 100644 index 0000000000..f4aae5cb13 --- /dev/null +++ b/archived/projt-launcher/launcher/ui/dialogs/IconPickerDialog.h @@ -0,0 +1,76 @@ +// SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0 +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * 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, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * 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 <QItemSelection> +#include <QLineEdit> +#include <QSortFilterProxyModel> + +namespace Ui +{ + class IconPickerDialog; +} + +class IconPickerDialog : public QDialog +{ + Q_OBJECT + + public: + explicit IconPickerDialog(QWidget* parent = 0); + ~IconPickerDialog(); + int execWithSelection(QString selection); + QString selectedIconKey; + + protected: + virtual bool eventFilter(QObject*, QEvent*); + + private: + Ui::IconPickerDialog* ui; + QPushButton* buttonRemove; + QLineEdit* searchBar; + QSortFilterProxyModel* proxyModel; + + private slots: + void selectionChanged(QItemSelection, QItemSelection); + void activated(QModelIndex); + void delayed_scroll(QModelIndex); + void addNewIcon(); + void removeSelectedIcon(); + void openFolder(); + void filterIcons(const QString& text); +}; diff --git a/archived/projt-launcher/launcher/ui/dialogs/IconPickerDialog.ui b/archived/projt-launcher/launcher/ui/dialogs/IconPickerDialog.ui new file mode 100644 index 0000000000..c548edfb7a --- /dev/null +++ b/archived/projt-launcher/launcher/ui/dialogs/IconPickerDialog.ui @@ -0,0 +1,67 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>IconPickerDialog</class> + <widget class="QDialog" name="IconPickerDialog"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>676</width> + <height>555</height> + </rect> + </property> + <property name="windowTitle"> + <string>Pick icon</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <widget class="QListView" name="iconView"/> + </item> + <item> + <widget class="QDialogButtonBox" name="buttonBox"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="standardButtons"> + <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set> + </property> + </widget> + </item> + </layout> + </widget> + <resources/> + <connections> + <connection> + <sender>buttonBox</sender> + <signal>accepted()</signal> + <receiver>IconPickerDialog</receiver> + <slot>accept()</slot> + <hints> + <hint type="sourcelabel"> + <x>248</x> + <y>254</y> + </hint> + <hint type="destinationlabel"> + <x>157</x> + <y>274</y> + </hint> + </hints> + </connection> + <connection> + <sender>buttonBox</sender> + <signal>rejected()</signal> + <receiver>IconPickerDialog</receiver> + <slot>reject()</slot> + <hints> + <hint type="sourcelabel"> + <x>316</x> + <y>260</y> + </hint> + <hint type="destinationlabel"> + <x>286</x> + <y>274</y> + </hint> + </hints> + </connection> + </connections> +</ui> diff --git a/archived/projt-launcher/launcher/ui/dialogs/ImportResourceDialog.cpp b/archived/projt-launcher/launcher/ui/dialogs/ImportResourceDialog.cpp new file mode 100644 index 0000000000..303fa9caf3 --- /dev/null +++ b/archived/projt-launcher/launcher/ui/dialogs/ImportResourceDialog.cpp @@ -0,0 +1,101 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * 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, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +#include "ImportResourceDialog.h" +#include "ui_ImportResourceDialog.h" + +#include <QFileDialog> +#include <QPushButton> + +#include "Application.h" +#include "InstanceList.h" + +#include <InstanceList.h> +#include "modplatform/ResourceType.h" +#include "ui/instanceview/InstanceDelegate.h" +#include "ui/instanceview/InstanceProxyModel.h" + +ImportResourceDialog::ImportResourceDialog(QString file_path, ModPlatform::ResourceType type, QWidget* parent) + : QDialog(parent), + ui(new Ui::ImportResourceDialog), + m_resource_type(type), + m_file_path(file_path) +{ + ui->setupUi(this); + setWindowModality(Qt::WindowModal); + + auto contentsWidget = ui->instanceView; + contentsWidget->setViewMode(QListView::ListMode); + contentsWidget->setFlow(QListView::LeftToRight); + contentsWidget->setIconSize(QSize(48, 48)); + contentsWidget->setMovement(QListView::Static); + contentsWidget->setResizeMode(QListView::Adjust); + contentsWidget->setSelectionMode(QAbstractItemView::SingleSelection); + contentsWidget->setSpacing(5); + contentsWidget->setWordWrap(true); + contentsWidget->setWrapping(true); + // NOTE: We can't have uniform sizes because the text may wrap if it's too long. If we set this, it will cut off the + // wrapped text. + contentsWidget->setUniformItemSizes(false); + contentsWidget->setVerticalScrollMode(QAbstractItemView::ScrollPerPixel); + contentsWidget->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + contentsWidget->setItemDelegate(new ListViewDelegate()); + + proxyModel = new InstanceProxyModel(this); + proxyModel->setSourceModel(APPLICATION->instances().get()); + proxyModel->sort(0); + contentsWidget->setModel(proxyModel); + + connect(contentsWidget, &QAbstractItemView::doubleClicked, this, &ImportResourceDialog::activated); + connect(contentsWidget->selectionModel(), + &QItemSelectionModel::selectionChanged, + this, + &ImportResourceDialog::selectionChanged); + + ui->label->setText(tr("Choose the instance you would like to import this %1 to.") + .arg(ModPlatform::ResourceTypeUtils::getName(m_resource_type))); + ui->label_file_path->setText(tr("File: %1").arg(m_file_path)); + + ui->buttonBox->button(QDialogButtonBox::Cancel)->setText(tr("Cancel")); + ui->buttonBox->button(QDialogButtonBox::Ok)->setText(tr("OK")); +} + +void ImportResourceDialog::activated(QModelIndex index) +{ + selectedInstanceKey = index.data(InstanceList::InstanceIDRole).toString(); + accept(); +} + +void ImportResourceDialog::selectionChanged(QItemSelection selected, QItemSelection deselected) +{ + if (selected.empty()) + return; + + QString key = selected.first().indexes().first().data(InstanceList::InstanceIDRole).toString(); + if (!key.isEmpty()) + { + selectedInstanceKey = key; + } +} + +ImportResourceDialog::~ImportResourceDialog() +{ + delete ui; +} diff --git a/archived/projt-launcher/launcher/ui/dialogs/ImportResourceDialog.h b/archived/projt-launcher/launcher/ui/dialogs/ImportResourceDialog.h new file mode 100644 index 0000000000..4cfd1cb8b4 --- /dev/null +++ b/archived/projt-launcher/launcher/ui/dialogs/ImportResourceDialog.h @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * 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, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +#pragma once + +#include <QDialog> +#include <QItemSelection> + +#include "modplatform/ResourceType.h" +#include "ui/instanceview/InstanceProxyModel.h" + +namespace Ui +{ + class ImportResourceDialog; +} + +class ImportResourceDialog : public QDialog +{ + Q_OBJECT + + public: + explicit ImportResourceDialog(QString file_path, ModPlatform::ResourceType type, QWidget* parent = nullptr); + ~ImportResourceDialog() override; + QString selectedInstanceKey; + + private: + Ui::ImportResourceDialog* ui; + ModPlatform::ResourceType m_resource_type; + QString m_file_path; + InstanceProxyModel* proxyModel; + + private slots: + void selectionChanged(QItemSelection, QItemSelection); + void activated(QModelIndex); +}; diff --git a/archived/projt-launcher/launcher/ui/dialogs/ImportResourceDialog.ui b/archived/projt-launcher/launcher/ui/dialogs/ImportResourceDialog.ui new file mode 100644 index 0000000000..cc3f4ec113 --- /dev/null +++ b/archived/projt-launcher/launcher/ui/dialogs/ImportResourceDialog.ui @@ -0,0 +1,81 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>ImportResourceDialog</class> + <widget class="QDialog" name="ImportResourceDialog"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>676</width> + <height>555</height> + </rect> + </property> + <property name="windowTitle"> + <string>Choose instance to import to</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <widget class="QLabel" name="label"> + <property name="text"> + <string>Choose the instance you would like to import this resource pack to.</string> + </property> + </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> + <widget class="QDialogButtonBox" name="buttonBox"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="standardButtons"> + <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set> + </property> + </widget> + </item> + </layout> + </widget> + <resources/> + <connections> + <connection> + <sender>buttonBox</sender> + <signal>accepted()</signal> + <receiver>ImportResourceDialog</receiver> + <slot>accept()</slot> + <hints> + <hint type="sourcelabel"> + <x>248</x> + <y>254</y> + </hint> + <hint type="destinationlabel"> + <x>157</x> + <y>274</y> + </hint> + </hints> + </connection> + <connection> + <sender>buttonBox</sender> + <signal>rejected()</signal> + <receiver>ImportResourceDialog</receiver> + <slot>reject()</slot> + <hints> + <hint type="sourcelabel"> + <x>316</x> + <y>260</y> + </hint> + <hint type="destinationlabel"> + <x>286</x> + <y>274</y> + </hint> + </hints> + </connection> + </connections> +</ui> diff --git a/archived/projt-launcher/launcher/ui/dialogs/InstallLoaderDialog.cpp b/archived/projt-launcher/launcher/ui/dialogs/InstallLoaderDialog.cpp new file mode 100644 index 0000000000..803f27f074 --- /dev/null +++ b/archived/projt-launcher/launcher/ui/dialogs/InstallLoaderDialog.cpp @@ -0,0 +1,233 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * 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, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * + * + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2023 TheKodeToad <TheKodeToad@proton.me> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * 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, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * ======================================================================== */ + +#include "InstallLoaderDialog.h" + +#include <QDialogButtonBox> +#include <QPushButton> +#include <QVBoxLayout> +#include "Application.h" +#include "BuildConfig.h" +#include "DesktopServices.h" +#include "meta/Index.hpp" +#include "minecraft/MinecraftInstance.h" +#include "minecraft/PackProfile.h" +#include "ui/widgets/PageContainer.h" +#include "ui/widgets/VersionSelectWidget.h" + +class InstallLoaderPage : public VersionSelectWidget, public BasePage +{ + Q_OBJECT + public: + InstallLoaderPage(const QString& id, + const QString& iconName, + const QString& name, + const Version& oldestVersion, + const std::shared_ptr<PackProfile> profile) + : VersionSelectWidget(nullptr), + uid(id), + iconName(iconName), + name(name) + { + const QString minecraftVersion = profile->getComponentVersion("net.minecraft"); + setEmptyString(tr("No versions are currently available for Minecraft %1").arg(minecraftVersion)); + setExactIfPresentFilter(BaseVersionList::ParentVersionRole, minecraftVersion); + + if (oldestVersion != Version() && Version(minecraftVersion) < oldestVersion) + setExactFilter(BaseVersionList::ParentVersionRole, "AAA"); + + if (const QString currentVersion = profile->getComponentVersion(id); !currentVersion.isNull()) + setCurrentVersion(currentVersion); + } + + QString id() const override + { + return uid; + } + QString displayName() const override + { + return name; + } + QIcon icon() const override + { + return QIcon::fromTheme(iconName); + } + + void openedImpl() override + { + if (loaded) + return; + + const auto versions = APPLICATION->metadataIndex()->component(uid); + if (!versions) + return; + + initialize(versions.get()); + loaded = true; + } + + void setParentContainer(BasePageContainer* container) override + { + auto* pageContainer = dynamic_cast<PageContainer*>(container); + auto* dialog = pageContainer ? qobject_cast<QDialog*>(pageContainer->parent()) : nullptr; + if (!dialog || !view()) + return; + connect(view(), &QAbstractItemView::doubleClicked, dialog, &QDialog::accept); + } + + private: + const QString uid; + const QString iconName; + const QString name; + bool loaded = false; +}; + +static InstallLoaderPage* pageCast(BasePage* page) +{ + auto result = dynamic_cast<InstallLoaderPage*>(page); + Q_ASSERT(result != nullptr); + return result; +} + +InstallLoaderDialog::InstallLoaderDialog(std::shared_ptr<PackProfile> profile, const QString& uid, QWidget* parent) + : QDialog(parent), + profile(std::move(profile)), + container(new PageContainer(this, QString(), this)), + buttons(new QDialogButtonBox(this)) +{ + auto layout = new QVBoxLayout(this); + + container->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Expanding); + layout->addWidget(container); + + auto buttonLayout = new QHBoxLayout(this); + + auto refreshButton = new QPushButton(tr("&Refresh"), this); + connect(refreshButton, + &QPushButton::clicked, + this, + [this] + { + if (auto* selectedPage = pageCast(container->selectedPage())) + selectedPage->loadList(); + }); + buttonLayout->addWidget(refreshButton); + + buttons->setOrientation(Qt::Horizontal); + buttons->setStandardButtons(QDialogButtonBox::Cancel | QDialogButtonBox::Ok); + buttons->button(QDialogButtonBox::Ok)->setText(tr("Ok")); + buttons->button(QDialogButtonBox::Cancel)->setText(tr("Cancel")); + connect(buttons, &QDialogButtonBox::accepted, this, &QDialog::accept); + connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject); + buttonLayout->addWidget(buttons); + + layout->addLayout(buttonLayout); + + setWindowTitle(dialogTitle()); + setWindowModality(Qt::WindowModal); + resize(520, 347); + + for (BasePage* page : container->getPages()) + { + if (page->id() == uid) + container->selectPage(page->id()); + + connect(pageCast(page), + &VersionSelectWidget::selectedVersionChanged, + this, + [this, page] + { + auto* selectedPage = container->selectedPage(); + if (selectedPage && page->id() == selectedPage->id()) + validate(selectedPage); + }); + } + connect(container, + &PageContainer::selectedPageChanged, + this, + [this](BasePage* previous, BasePage* current) { validate(current); }); + if (auto* selectedPage = pageCast(container->selectedPage())) + selectedPage->selectSearch(); + validate(container->selectedPage()); +} + +QList<BasePage*> InstallLoaderDialog::getPages() +{ + return { // NeoForge + new InstallLoaderPage("net.neoforged", "neoforged", tr("NeoForge"), {}, profile), + // Forge + new InstallLoaderPage("net.minecraftforge", "forge", tr("Forge"), {}, profile), + // Fabric + new InstallLoaderPage("net.fabricmc.fabric-loader", "fabricmc", tr("Fabric"), Version("1.14"), profile), + // Quilt + new InstallLoaderPage("org.quiltmc.quilt-loader", "quiltmc", tr("Quilt"), Version("1.14"), profile), + // LiteLoader + new InstallLoaderPage("com.mumfrey.liteloader", "liteloader", tr("LiteLoader"), {}, profile) + }; +} + +QString InstallLoaderDialog::dialogTitle() +{ + return tr("Install Loader"); +} + +void InstallLoaderDialog::validate(BasePage* page) +{ + auto* loaderPage = pageCast(page); + buttons->button(QDialogButtonBox::Ok)->setEnabled(loaderPage && loaderPage->selectedVersion() != nullptr); +} + +void InstallLoaderDialog::done(int result) +{ + if (result == Accepted) + { + auto* page = pageCast(container->selectedPage()); + if (page && page->selectedVersion()) + { + profile->setComponentVersion(page->id(), page->selectedVersion()->descriptor()); + profile->resolve(Net::Mode::Online); + } + } + + QDialog::done(result); +} +#include "InstallLoaderDialog.moc" diff --git a/archived/projt-launcher/launcher/ui/dialogs/InstallLoaderDialog.h b/archived/projt-launcher/launcher/ui/dialogs/InstallLoaderDialog.h new file mode 100644 index 0000000000..309a3e2481 --- /dev/null +++ b/archived/projt-launcher/launcher/ui/dialogs/InstallLoaderDialog.h @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * 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, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * + * + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2023 TheKodeToad <TheKodeToad@proton.me> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * 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, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * ======================================================================== */ + +#pragma once + +#include <QDialog> +#include "ui/pages/BasePageProvider.h" + +class MinecraftInstance; +class PageContainer; +class PackProfile; +class QDialogButtonBox; + +class InstallLoaderDialog final : public QDialog, protected BasePageProvider +{ + Q_OBJECT + + public: + explicit InstallLoaderDialog(std::shared_ptr<PackProfile> instance, + const QString& uid = QString(), + QWidget* parent = nullptr); + + QList<BasePage*> getPages() override; + QString dialogTitle() override; + + void validate(BasePage* page); + void done(int result) override; + + private: + std::shared_ptr<PackProfile> profile; + PageContainer* container; + QDialogButtonBox* buttons; +}; diff --git a/archived/projt-launcher/launcher/ui/dialogs/LauncherHubDialog.cpp b/archived/projt-launcher/launcher/ui/dialogs/LauncherHubDialog.cpp new file mode 100644 index 0000000000..0ce7c500cf --- /dev/null +++ b/archived/projt-launcher/launcher/ui/dialogs/LauncherHubDialog.cpp @@ -0,0 +1,79 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * 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, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include "LauncherHubDialog.h" + +#include <QVBoxLayout> + +#include "ui/widgets/LauncherHubWidget.h" + +LauncherHubDialog::LauncherHubDialog(QWidget* parent) : QDialog(parent) +{ + setWindowTitle(tr("Launcher Hub")); + setAttribute(Qt::WA_DeleteOnClose); + resize(1100, 720); + + auto* layout = new QVBoxLayout(this); + layout->setContentsMargins(0, 0, 0, 0); + + m_widget = new LauncherHubWidget(this); + layout->addWidget(m_widget); + m_widget->ensureLoaded(); + + connect(m_widget, &LauncherHubWidget::selectInstanceRequested, this, &LauncherHubDialog::selectInstanceRequested); + connect(m_widget, &LauncherHubWidget::launchInstanceRequested, this, &LauncherHubDialog::launchInstanceRequested); + connect(m_widget, &LauncherHubWidget::editInstanceRequested, this, &LauncherHubDialog::editInstanceRequested); + connect(m_widget, &LauncherHubWidget::backupsRequested, this, &LauncherHubDialog::backupsRequested); + connect(m_widget, + &LauncherHubWidget::openInstanceFolderRequested, + this, + &LauncherHubDialog::openInstanceFolderRequested); +} + +LauncherHubDialog::~LauncherHubDialog() = default; + +void LauncherHubDialog::openUrl(const QUrl& url) +{ + if (!m_widget) + { + return; + } + m_widget->ensureLoaded(); + m_widget->openUrl(url); +} + +void LauncherHubDialog::setSelectedInstanceId(const QString& id) +{ + if (!m_widget) + { + return; + } + m_widget->setSelectedInstanceId(id); +} + +void LauncherHubDialog::refreshCockpit() +{ + if (!m_widget) + { + return; + } + m_widget->refreshCockpit(); +} diff --git a/archived/projt-launcher/launcher/ui/dialogs/LauncherHubDialog.h b/archived/projt-launcher/launcher/ui/dialogs/LauncherHubDialog.h new file mode 100644 index 0000000000..6d70abeb4c --- /dev/null +++ b/archived/projt-launcher/launcher/ui/dialogs/LauncherHubDialog.h @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * 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, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +#pragma once + +#include <QDialog> +#include <QString> +#include <QUrl> + +class LauncherHubWidget; + +class LauncherHubDialog : public QDialog +{ + Q_OBJECT + + public: + explicit LauncherHubDialog(QWidget* parent = nullptr); + ~LauncherHubDialog() override; + + void openUrl(const QUrl& url); + void setSelectedInstanceId(const QString& id); + void refreshCockpit(); + + signals: + void selectInstanceRequested(const QString& instanceId); + void launchInstanceRequested(const QString& instanceId); + void editInstanceRequested(const QString& instanceId); + void backupsRequested(const QString& instanceId); + void openInstanceFolderRequested(const QString& instanceId); + + private: + LauncherHubWidget* m_widget = nullptr; +}; diff --git a/archived/projt-launcher/launcher/ui/dialogs/MSALoginDialog.cpp b/archived/projt-launcher/launcher/ui/dialogs/MSALoginDialog.cpp new file mode 100644 index 0000000000..6a9cb13d75 --- /dev/null +++ b/archived/projt-launcher/launcher/ui/dialogs/MSALoginDialog.cpp @@ -0,0 +1,294 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * 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, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * + * + * Prism Launcher - 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, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * 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 "MSALoginDialog.h" +#include "Application.h" + +#include "ui_MSALoginDialog.h" + +#include "DesktopServices.h" +#include "minecraft/auth/AuthFlow.hpp" + +#include <QApplication> +#include <QClipboard> +#include <QColor> +#include <QPainter> +#include <QPixmap> +#include <QSize> +#include <QUrl> +#include <QtWidgets/QPushButton> + +#include <qrencode.h> + +MSALoginDialog::MSALoginDialog(QWidget* parent) : QDialog(parent), ui(new Ui::MSALoginDialog) +{ + ui->setupUi(this); + + // make font monospace + QFont font; + font.setPixelSize(ui->code->fontInfo().pixelSize()); + font.setFamily(APPLICATION->settings()->get("ConsoleFont").toString()); + font.setStyleHint(QFont::Monospace); + font.setFixedPitch(true); + ui->code->setFont(font); + + connect(ui->copyCode, + &QPushButton::clicked, + this, + [this] { QApplication::clipboard()->setText(ui->code->text()); }); + connect(ui->loginButton, + &QPushButton::clicked, + this, + [this] + { + if (m_url.isValid()) + { + if (!DesktopServices::openUrl(m_url)) + { + QApplication::clipboard()->setText(m_url.toString()); + } + } + }); + + ui->buttonBox->button(QDialogButtonBox::Cancel)->setText(tr("Cancel")); +} + +int MSALoginDialog::exec() +{ + // Setup the login task and start it + m_account = MinecraftAccount::createBlankMSA(); + m_authflow_task = m_account->login(false); + connect(m_authflow_task.get(), &Task::failed, this, &MSALoginDialog::onTaskFailed); + connect(m_authflow_task.get(), &Task::succeeded, this, &QDialog::accept); + connect(m_authflow_task.get(), &Task::aborted, this, &MSALoginDialog::reject); + connect(m_authflow_task.get(), &Task::status, this, &MSALoginDialog::onAuthFlowStatus); + connect(m_authflow_task.get(), &AuthFlow::authorizeWithBrowser, this, &MSALoginDialog::authorizeWithBrowser); + connect(m_authflow_task.get(), + &AuthFlow::authorizeWithBrowserWithExtra, + this, + &MSALoginDialog::authorizeWithBrowserWithExtra); + connect(ui->buttonBox->button(QDialogButtonBox::Cancel), + &QPushButton::clicked, + m_authflow_task.get(), + &Task::abort); + + m_devicecode_task.reset(new AuthFlow(m_account->accountData(), AuthFlow::Action::DeviceCode)); + connect(m_devicecode_task.get(), &Task::failed, this, &MSALoginDialog::onTaskFailed); + connect(m_devicecode_task.get(), &Task::succeeded, this, &QDialog::accept); + connect(m_devicecode_task.get(), &Task::aborted, this, &MSALoginDialog::reject); + connect(m_devicecode_task.get(), &Task::status, this, &MSALoginDialog::onDeviceFlowStatus); + connect(m_devicecode_task.get(), &AuthFlow::authorizeWithBrowser, this, &MSALoginDialog::authorizeWithBrowser); + connect(m_devicecode_task.get(), + &AuthFlow::authorizeWithBrowserWithExtra, + this, + &MSALoginDialog::authorizeWithBrowserWithExtra); + connect(ui->buttonBox->button(QDialogButtonBox::Cancel), + &QPushButton::clicked, + m_devicecode_task.get(), + &Task::abort); + QMetaObject::invokeMethod(m_authflow_task.get(), &Task::start, Qt::QueuedConnection); + QMetaObject::invokeMethod(m_devicecode_task.get(), &Task::start, Qt::QueuedConnection); + + return QDialog::exec(); +} + +MSALoginDialog::~MSALoginDialog() +{ + delete ui; +} + +void MSALoginDialog::onTaskFailed(QString reason) +{ + // Set message + m_authflow_task->disconnect(); + m_devicecode_task->disconnect(); + ui->stackedWidget->setCurrentIndex(0); + auto lines = reason.split('\n'); + QString processed; + for (auto line : lines) + { + if (line.size()) + { + processed += "<font color='red'>" + line + "</font><br />"; + } + else + { + processed += "<br />"; + } + } + ui->status->setText(processed); + auto task = m_authflow_task; + if (task->failReason().isEmpty()) + { + task = m_devicecode_task; + } + if (task) + { + ui->loadingLabel->setText(task->getStatus()); + } + disconnect(ui->buttonBox->button(QDialogButtonBox::Cancel), + &QPushButton::clicked, + m_authflow_task.get(), + &Task::abort); + disconnect(ui->buttonBox->button(QDialogButtonBox::Cancel), + &QPushButton::clicked, + m_devicecode_task.get(), + &Task::abort); + connect(ui->buttonBox->button(QDialogButtonBox::Cancel), &QPushButton::clicked, this, &MSALoginDialog::reject); +} + +void MSALoginDialog::authorizeWithBrowser(const QUrl& url) +{ + ui->stackedWidget2->setCurrentIndex(1); + ui->stackedWidget2->adjustSize(); + ui->stackedWidget2->updateGeometry(); + this->adjustSize(); + ui->loginButton->setToolTip(QString("<div style='width: 200px;'>%1</div>").arg(url.toString())); + m_url = url; +} + +// https://stackoverflow.com/questions/21400254/how-to-draw-a-qr-code-with-qt-in-native-c-c +void paintQR(QPainter& painter, const QSize sz, const QString& data, QColor fg) +{ + QRcode* qr = QRcode_encodeString(data.toUtf8().constData(), 0, QR_ECLEVEL_L, QR_MODE_8, 1); + if (!qr) + { + return; + } + + const int s = qr->width > 0 ? qr->width : 1; + const double w = sz.width(); + const double h = sz.height(); + const double aspect = w / h; + const double size = ((aspect > 1.0) ? h : w); + const double scale = size / (s + 2); + + painter.setPen(Qt::NoPen); + painter.setBrush(fg); + for (int y = 0; y < s; y++) + { + for (int x = 0; x < s; x++) + { + const int color = qr->data[y * s + x] & 1; // LSB is 1 for black, 0 for white + if (0 != color) + { + const double rx1 = (x + 1) * scale, ry1 = (y + 1) * scale; + QRectF r(rx1, ry1, scale, scale); + painter.drawRect(r); + } + } + } + QRcode_free(qr); +} + +void MSALoginDialog::authorizeWithBrowserWithExtra(QString url, QString code, [[maybe_unused]] int expiresIn) +{ + ui->stackedWidget->setCurrentIndex(1); + ui->stackedWidget->adjustSize(); + ui->stackedWidget->updateGeometry(); + this->adjustSize(); + + const auto linkString = QString("<a href=\"%1\">%2</a>").arg(url, url); + if (url == "https://www.microsoft.com/link" && !code.isEmpty()) + { + url += QString("?otc=%1").arg(code); + } + ui->code->setText(code); + + auto size = QSize(150, 150); + QPixmap pixmap(size); + pixmap.fill(Qt::white); + + QPainter painter(&pixmap); + paintQR(painter, size, url, Qt::black); + + // Set the generated pixmap to the label + ui->qr->setPixmap(pixmap); + + ui->qrMessage->setText(tr("Open %1 or scan the QR and enter the above code if needed.").arg(linkString)); +} + +void MSALoginDialog::onDeviceFlowStatus(QString status) +{ + ui->stackedWidget->setCurrentIndex(0); + ui->stackedWidget->adjustSize(); + ui->stackedWidget->updateGeometry(); + this->adjustSize(); + ui->status->setText(status); +} + +void MSALoginDialog::onAuthFlowStatus(QString status) +{ + ui->stackedWidget2->setCurrentIndex(0); + ui->stackedWidget2->adjustSize(); + ui->stackedWidget2->updateGeometry(); + this->adjustSize(); + ui->status2->setText(status); +} + +// Public interface +MinecraftAccountPtr MSALoginDialog::newAccount(QWidget* parent) +{ + MSALoginDialog dlg(parent); + if (dlg.exec() == QDialog::Accepted) + { + return dlg.m_account; + } + return nullptr; +}
\ No newline at end of file diff --git a/archived/projt-launcher/launcher/ui/dialogs/MSALoginDialog.h b/archived/projt-launcher/launcher/ui/dialogs/MSALoginDialog.h new file mode 100644 index 0000000000..5a93f544f5 --- /dev/null +++ b/archived/projt-launcher/launcher/ui/dialogs/MSALoginDialog.h @@ -0,0 +1,78 @@ +// SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0 +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * 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, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * 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 <QTimer> +#include <QtWidgets/QDialog> + +#include "minecraft/auth/AuthFlow.hpp" +#include "minecraft/auth/MinecraftAccount.hpp" + +namespace Ui +{ + class MSALoginDialog; +} + +class MSALoginDialog : public QDialog +{ + Q_OBJECT + + public: + ~MSALoginDialog(); + + static MinecraftAccountPtr newAccount(QWidget* parent); + int exec() override; + + private: + explicit MSALoginDialog(QWidget* parent = 0); + + protected slots: + void onTaskFailed(QString reason); + void onDeviceFlowStatus(QString status); + void onAuthFlowStatus(QString status); + void authorizeWithBrowser(const QUrl& url); + void authorizeWithBrowserWithExtra(QString url, QString code, int expiresIn); + + private: + Ui::MSALoginDialog* ui; + MinecraftAccountPtr m_account; + shared_qobject_ptr<AuthFlow> m_devicecode_task; + shared_qobject_ptr<AuthFlow> m_authflow_task; + + QUrl m_url; +}; diff --git a/archived/projt-launcher/launcher/ui/dialogs/MSALoginDialog.ui b/archived/projt-launcher/launcher/ui/dialogs/MSALoginDialog.ui new file mode 100644 index 0000000000..69cd2e1ab9 --- /dev/null +++ b/archived/projt-launcher/launcher/ui/dialogs/MSALoginDialog.ui @@ -0,0 +1,429 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>MSALoginDialog</class> + <widget class="QDialog" name="MSALoginDialog"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>440</width> + <height>447</height> + </rect> + </property> + <property name="minimumSize"> + <size> + <width>0</width> + <height>430</height> + </size> + </property> + <property name="windowTitle"> + <string>Add Microsoft Account</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout_6"> + <item> + <widget class="QStackedWidget" name="stackedWidget2"> + <property name="currentIndex"> + <number>1</number> + </property> + <widget class="QWidget" name="loadingPage2"> + <layout class="QVBoxLayout" name="verticalLayout_31"> + <item> + <spacer name="verticalSpacer_4"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>40</height> + </size> + </property> + </spacer> + </item> + <item> + <widget class="QLabel" name="loadingLabel2"> + <property name="font"> + <font> + <pointsize>16</pointsize> + <weight>75</weight> + <bold>true</bold> + </font> + </property> + <property name="text"> + <string>Please wait...</string> + </property> + <property name="alignment"> + <set>Qt::AlignCenter</set> + </property> + <property name="wordWrap"> + <bool>true</bool> + </property> + </widget> + </item> + <item> + <widget class="QLabel" name="status2"> + <property name="text"> + <string>Status</string> + </property> + <property name="alignment"> + <set>Qt::AlignCenter</set> + </property> + <property name="wordWrap"> + <bool>true</bool> + </property> + </widget> + </item> + <item> + <spacer name="verticalSpacer_31"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>40</height> + </size> + </property> + </spacer> + </item> + </layout> + </widget> + <widget class="QWidget" name="mpPage1"> + <layout class="QVBoxLayout" name="verticalLayout_21" stretch="0"> + <item> + <layout class="QHBoxLayout" name="horizontalLayout_7"> + <item> + <spacer name="horizontalSpacer_5"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item> + <widget class="QPushButton" name="loginButton"> + <property name="minimumSize"> + <size> + <width>250</width> + <height>40</height> + </size> + </property> + <property name="text"> + <string>Sign in with Microsoft</string> + </property> + <property name="default"> + <bool>true</bool> + </property> + </widget> + </item> + <item> + <spacer name="horizontalSpacer_6"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + </layout> + </item> + </layout> + </widget> + </widget> + </item> + <item> + <layout class="QHBoxLayout" name="horizontalLayout_5"> + <item> + <widget class="Line" name="line_3"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + </widget> + </item> + <item> + <widget class="QLabel" name="orLabel"> + <property name="font"> + <font> + <pointsize>16</pointsize> + </font> + </property> + <property name="text"> + <string>Or</string> + </property> + <property name="alignment"> + <set>Qt::AlignCenter</set> + </property> + </widget> + </item> + <item> + <widget class="Line" name="line_4"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + </widget> + </item> + </layout> + </item> + <item> + <widget class="QStackedWidget" name="stackedWidget"> + <property name="currentIndex"> + <number>1</number> + </property> + <widget class="QWidget" name="loadingPage"> + <layout class="QVBoxLayout" name="verticalLayout_3"> + <item> + <spacer name="verticalSpacer_41"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>40</height> + </size> + </property> + </spacer> + </item> + <item> + <widget class="QLabel" name="loadingLabel"> + <property name="font"> + <font> + <pointsize>16</pointsize> + <weight>75</weight> + <bold>true</bold> + </font> + </property> + <property name="text"> + <string>Please wait...</string> + </property> + <property name="alignment"> + <set>Qt::AlignCenter</set> + </property> + <property name="wordWrap"> + <bool>true</bool> + </property> + </widget> + </item> + <item> + <widget class="QLabel" name="status"> + <property name="text"> + <string>Status</string> + </property> + <property name="alignment"> + <set>Qt::AlignCenter</set> + </property> + <property name="wordWrap"> + <bool>true</bool> + </property> + </widget> + </item> + <item> + <spacer name="verticalSpacer_3"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>40</height> + </size> + </property> + </spacer> + </item> + </layout> + </widget> + <widget class="QWidget" name="mpPage"> + <layout class="QVBoxLayout" name="verticalLayout_2" stretch="0,0,0"> + <item> + <layout class="QHBoxLayout" name="horizontalLayout"> + <item> + <spacer name="horizontalSpacer"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item> + <widget class="QLabel" name="qr"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Fixed" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="minimumSize"> + <size> + <width>150</width> + <height>150</height> + </size> + </property> + <property name="maximumSize"> + <size> + <width>150</width> + <height>150</height> + </size> + </property> + <property name="text"> + <string/> + </property> + <property name="scaledContents"> + <bool>true</bool> + </property> + <property name="alignment"> + <set>Qt::AlignCenter</set> + </property> + </widget> + </item> + <item> + <spacer name="horizontalSpacer_2"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + </layout> + </item> + <item> + <layout class="QHBoxLayout" name="horizontalLayout_2"> + <item> + <spacer name="horizontalSpacer_3"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item> + <widget class="QLabel" name="code"> + <property name="font"> + <font> + <pointsize>30</pointsize> + <weight>75</weight> + <bold>true</bold> + </font> + </property> + <property name="cursor"> + <cursorShape>IBeamCursor</cursorShape> + </property> + <property name="text"> + <string>CODE</string> + </property> + <property name="alignment"> + <set>Qt::AlignCenter</set> + </property> + <property name="textInteractionFlags"> + <set>Qt::TextBrowserInteraction</set> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="copyCode"> + <property name="toolTip"> + <string>Copy code to clipboard</string> + </property> + <property name="text"> + <string/> + </property> + <property name="icon"> + <iconset theme="copy"> + <normaloff>.</normaloff>.</iconset> + </property> + <property name="iconSize"> + <size> + <width>22</width> + <height>22</height> + </size> + </property> + <property name="flat"> + <bool>true</bool> + </property> + </widget> + </item> + <item> + <spacer name="horizontalSpacer_4"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + </layout> + </item> + <item> + <widget class="QLabel" name="qrMessage"> + <property name="text"> + <string>Info</string> + </property> + <property name="alignment"> + <set>Qt::AlignCenter</set> + </property> + <property name="wordWrap"> + <bool>true</bool> + </property> + <property name="openExternalLinks"> + <bool>true</bool> + </property> + <property name="textInteractionFlags"> + <set>Qt::TextBrowserInteraction</set> + </property> + </widget> + </item> + </layout> + </widget> + </widget> + </item> + <item> + <widget class="QDialogButtonBox" name="buttonBox"> + <property name="standardButtons"> + <set>QDialogButtonBox::Cancel</set> + </property> + </widget> + </item> + </layout> + </widget> + <resources/> + <connections/> +</ui> diff --git a/archived/projt-launcher/launcher/ui/dialogs/NewComponentDialog.cpp b/archived/projt-launcher/launcher/ui/dialogs/NewComponentDialog.cpp new file mode 100644 index 0000000000..26f21baa68 --- /dev/null +++ b/archived/projt-launcher/launcher/ui/dialogs/NewComponentDialog.cpp @@ -0,0 +1,154 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * 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, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * + * + * Prism Launcher - 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, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * 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 "NewComponentDialog.h" +#include "Application.h" +#include "ui_NewComponentDialog.h" + +#include <BaseVersion.h> +#include <InstanceList.h> +#include <icons/IconList.hpp> +#include <tasks/Task.h> + +#include "IconPickerDialog.h" +#include "ProgressDialog.h" +#include "VersionSelectDialog.h" + +#include <QFileDialog> +#include <QLayout> +#include <QPushButton> +#include <QValidator> + +#include <meta/Index.hpp> +#include <meta/VersionList.hpp> + +NewComponentDialog::NewComponentDialog(const QString& initialName, const QString& initialUid, QWidget* parent) + : QDialog(parent), + ui(new Ui::NewComponentDialog) +{ + ui->setupUi(this); + resize(minimumSizeHint()); + + ui->nameTextBox->setText(initialName); + ui->uidTextBox->setText(initialUid); + + connect(ui->nameTextBox, &QLineEdit::textChanged, this, &NewComponentDialog::updateDialogState); + connect(ui->uidTextBox, &QLineEdit::textChanged, this, &NewComponentDialog::updateDialogState); + + ui->nameTextBox->setFocus(); + + ui->buttonBox->button(QDialogButtonBox::Cancel)->setText(tr("Cancel")); + ui->buttonBox->button(QDialogButtonBox::Ok)->setText(tr("OK")); + + originalPlaceholderText = ui->uidTextBox->placeholderText(); + updateDialogState(); +} + +NewComponentDialog::~NewComponentDialog() +{ + delete ui; +} + +void NewComponentDialog::updateDialogState() +{ + auto protoUid = ui->nameTextBox->text().toLower(); + static const QRegularExpression s_removeChars("[^a-z]"); + protoUid.remove(s_removeChars); + if (protoUid.isEmpty()) + { + ui->uidTextBox->setPlaceholderText(originalPlaceholderText); + } + else + { + QString suggestedUid = "org.multimc.custom." + protoUid; + ui->uidTextBox->setPlaceholderText(suggestedUid); + } + bool allowOK = !name().isEmpty() && !uid().isEmpty() && !uidBlacklist.contains(uid()); + ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(allowOK); +} + +QString NewComponentDialog::name() const +{ + auto result = ui->nameTextBox->text(); + if (result.size()) + { + return result.trimmed(); + } + return QString(); +} + +QString NewComponentDialog::uid() const +{ + auto result = ui->uidTextBox->text(); + if (result.size()) + { + return result.trimmed(); + } + result = ui->uidTextBox->placeholderText(); + if (result.size() && result != originalPlaceholderText) + { + return result.trimmed(); + } + return QString(); +} + +void NewComponentDialog::setBlacklist(QStringList badUids) +{ + uidBlacklist = badUids; +} diff --git a/archived/projt-launcher/launcher/ui/dialogs/NewComponentDialog.h b/archived/projt-launcher/launcher/ui/dialogs/NewComponentDialog.h new file mode 100644 index 0000000000..ccac984f6b --- /dev/null +++ b/archived/projt-launcher/launcher/ui/dialogs/NewComponentDialog.h @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0 +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * 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, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * 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 <QString> +#include <QStringList> + +namespace Ui +{ + class NewComponentDialog; +} + +class NewComponentDialog : public QDialog +{ + Q_OBJECT + + public: + explicit NewComponentDialog(const QString& initialName = QString(), + const QString& initialUid = QString(), + QWidget* parent = 0); + virtual ~NewComponentDialog(); + void setBlacklist(QStringList badUids); + + QString name() const; + QString uid() const; + + private slots: + void updateDialogState(); + + private: + Ui::NewComponentDialog* ui; + + QString originalPlaceholderText; + QStringList uidBlacklist; +}; diff --git a/archived/projt-launcher/launcher/ui/dialogs/NewComponentDialog.ui b/archived/projt-launcher/launcher/ui/dialogs/NewComponentDialog.ui new file mode 100644 index 0000000000..03b0d22294 --- /dev/null +++ b/archived/projt-launcher/launcher/ui/dialogs/NewComponentDialog.ui @@ -0,0 +1,101 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>NewComponentDialog</class> + <widget class="QDialog" name="NewComponentDialog"> + <property name="windowModality"> + <enum>Qt::ApplicationModal</enum> + </property> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>345</width> + <height>146</height> + </rect> + </property> + <property name="windowTitle"> + <string>Add Empty Component</string> + </property> + <property name="windowIcon"> + <iconset> + <normaloff>:/icons/toolbar/copy</normaloff>:/icons/toolbar/copy</iconset> + </property> + <property name="modal"> + <bool>true</bool> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <widget class="QLineEdit" name="nameTextBox"> + <property name="placeholderText"> + <string>Name</string> + </property> + </widget> + </item> + <item> + <widget class="QLineEdit" name="uidTextBox"> + <property name="placeholderText"> + <string>uid</string> + </property> + </widget> + </item> + <item> + <widget class="Line" name="line"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + </widget> + </item> + <item> + <widget class="QDialogButtonBox" name="buttonBox"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="standardButtons"> + <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set> + </property> + </widget> + </item> + </layout> + </widget> + <tabstops> + <tabstop>nameTextBox</tabstop> + <tabstop>uidTextBox</tabstop> + </tabstops> + <resources> + <include location="../../graphics.qrc"/> + </resources> + <connections> + <connection> + <sender>buttonBox</sender> + <signal>accepted()</signal> + <receiver>NewComponentDialog</receiver> + <slot>accept()</slot> + <hints> + <hint type="sourcelabel"> + <x>248</x> + <y>254</y> + </hint> + <hint type="destinationlabel"> + <x>157</x> + <y>274</y> + </hint> + </hints> + </connection> + <connection> + <sender>buttonBox</sender> + <signal>rejected()</signal> + <receiver>NewComponentDialog</receiver> + <slot>reject()</slot> + <hints> + <hint type="sourcelabel"> + <x>316</x> + <y>260</y> + </hint> + <hint type="destinationlabel"> + <x>286</x> + <y>274</y> + </hint> + </hints> + </connection> + </connections> +</ui> diff --git a/archived/projt-launcher/launcher/ui/dialogs/NewInstanceDialog.cpp b/archived/projt-launcher/launcher/ui/dialogs/NewInstanceDialog.cpp new file mode 100644 index 0000000000..21af782160 --- /dev/null +++ b/archived/projt-launcher/launcher/ui/dialogs/NewInstanceDialog.cpp @@ -0,0 +1,381 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * 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, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * + * + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> + * Copyright (C) 2023 TheKodeToad <TheKodeToad@proton.me> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * 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, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * 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 "NewInstanceDialog.h" +#include "Application.h" +#include "ui/pages/modplatform/ModpackProviderBasePage.h" +#include "ui/pages/modplatform/import_ftb/ImportFTBPage.h" +#include "ui_NewInstanceDialog.h" + +#include <BaseVersion.h> +#include <InstanceList.h> +#include <icons/IconList.hpp> +#include <tasks/Task.h> + +#include "IconPickerDialog.h" +#include "ProgressDialog.h" +#include "VersionSelectDialog.h" + +#include <QDialogButtonBox> +#include <QFileDialog> +#include <QLayout> +#include <QPushButton> +#include <QScreen> +#include <QValidator> +#include <utility> + +#include "ui/pages/modplatform/CustomPage.h" +#include "ui/pages/modplatform/ImportPage.h" +#include "ui/pages/modplatform/atlauncher/AtlPage.h" +#include "ui/pages/modplatform/flame/FlamePage.h" +#include "ui/pages/modplatform/legacy_ftb/Page.h" +#include "ui/pages/modplatform/modrinth/ModrinthPage.h" +#include "ui/pages/modplatform/technic/TechnicPage.h" +#include "ui/widgets/PageContainer.h" + +NewInstanceDialog::NewInstanceDialog(const QString& initialGroup, + const QString& url, + const QMap<QString, QString>& extra_info, + QWidget* parent) + : QDialog(parent), + ui(new Ui::NewInstanceDialog) +{ + ui->setupUi(this); + + setWindowIcon(QIcon::fromTheme("new")); + + InstIconKey = "default"; + ui->iconButton->setIcon(APPLICATION->icons()->getIcon(InstIconKey)); + + QStringList groups = APPLICATION->instances()->getGroups(); + groups.prepend(""); + int index = groups.indexOf(initialGroup); + if (index == -1) + { + index = 1; + groups.insert(index, initialGroup); + } + ui->groupBox->addItems(groups); + ui->groupBox->setCurrentIndex(index); + ui->groupBox->lineEdit()->setPlaceholderText(tr("No group")); + + // 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, {}, this); + m_container->setSizePolicy(QSizePolicy::Policy::Preferred, QSizePolicy::Policy::Expanding); + m_container->layout()->setContentsMargins(0, 0, 0, 0); + ui->verticalLayout->insertWidget(2, m_container); + + // Block updates while setting up buttons to avoid multiple layout recalculations + setUpdatesEnabled(false); + + m_container->addButtons(m_buttons); + connect(m_container, + &PageContainer::selectedPageChanged, + this, + [this](BasePage* previous, BasePage* selected) + { m_buttons->button(QDialogButtonBox::Ok)->setEnabled(creationTask && !instName().isEmpty()); }); + + // 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->setDefault(true); + OkButton->setAutoDefault(true); + OkButton->setText(tr("OK")); + connect(OkButton, &QPushButton::clicked, this, &NewInstanceDialog::accept); + + auto CancelButton = m_buttons->button(QDialogButtonBox::Cancel); + CancelButton->setDefault(false); + CancelButton->setAutoDefault(false); + CancelButton->setText(tr("Cancel")); + connect(CancelButton, &QPushButton::clicked, this, &NewInstanceDialog::reject); + + auto HelpButton = m_buttons->button(QDialogButtonBox::Help); + HelpButton->setDefault(false); + HelpButton->setAutoDefault(false); + HelpButton->setText(tr("Help")); + connect(HelpButton, &QPushButton::clicked, m_container, &PageContainer::help); + + // Re-enable updates after all setup is complete + setUpdatesEnabled(true); + + if (!url.isEmpty()) + { + QUrl actualUrl(url); + m_container->selectPage("import"); + importPage->setUrl(url); + importPage->setExtraInfo(extra_info); + } + + updateDialogState(); + + if (APPLICATION->settings()->get("NewInstanceGeometry").isValid()) + { + restoreGeometry( + QByteArray::fromBase64(APPLICATION->settings()->get("NewInstanceGeometry").toString().toUtf8())); + } + else + { + auto screen = parent->screen(); + auto geometry = screen->availableSize(); + resize(width(), qMin(geometry.height() - 50, 710)); + } + + connect(m_container, &PageContainer::selectedPageChanged, this, &NewInstanceDialog::selectedPageChanged); +} + +void NewInstanceDialog::reject() +{ + APPLICATION->settings()->set("NewInstanceGeometry", QString::fromUtf8(saveGeometry().toBase64())); + + // This is just so that the pages get the close() call and can react to it, if needed. + m_container->prepareToClose(); + + QDialog::reject(); +} + +void NewInstanceDialog::accept() +{ + APPLICATION->settings()->set("NewInstanceGeometry", QString::fromUtf8(saveGeometry().toBase64())); + importIconNow(); + + // This is just so that the pages get the close() call and can react to it, if needed. + m_container->prepareToClose(); + + QDialog::accept(); +} + +QList<BasePage*> NewInstanceDialog::getPages() +{ + QList<BasePage*> pages; + + importPage = new ImportPage(this); + + pages.append(new CustomPage(this)); + pages.append(importPage); + pages.append(new AtlPage(this)); + if (APPLICATION->capabilities() & Application::SupportsFlame) + pages.append(new FlamePage(this)); + pages.append(new LegacyFTB::Page(this)); + pages.append(new FTBImportAPP::ImportFTBPage(this)); + pages.append(new ModrinthPage(this)); + pages.append(new TechnicPage(this)); + + return pages; +} + +QString NewInstanceDialog::dialogTitle() +{ + return tr("New Instance"); +} + +NewInstanceDialog::~NewInstanceDialog() +{ + delete ui; +} + +void NewInstanceDialog::setSuggestedPack(const QString& name, InstanceTask* task) +{ + creationTask.reset(task); + + ui->instNameTextBox->setPlaceholderText(name); + importVersion.clear(); + + if (!task) + { + ui->iconButton->setIcon(APPLICATION->icons()->getIcon(InstIconKey)); + importIcon = false; + } + + auto allowOK = task && !instName().isEmpty(); + m_buttons->button(QDialogButtonBox::Ok)->setEnabled(allowOK); +} + +void NewInstanceDialog::setSuggestedPack(const QString& name, QString version, InstanceTask* task) +{ + creationTask.reset(task); + + ui->instNameTextBox->setPlaceholderText(name); + importVersion = std::move(version); + + if (!task) + { + ui->iconButton->setIcon(APPLICATION->icons()->getIcon(InstIconKey)); + importIcon = false; + } + + auto allowOK = task && !instName().isEmpty(); + m_buttons->button(QDialogButtonBox::Ok)->setEnabled(allowOK); +} + +void NewInstanceDialog::setSuggestedIconFromFile(const QString& path, const QString& name) +{ + importIcon = true; + importIconPath = path; + importIconName = name; + + // Hmm, for some reason they can be to small + ui->iconButton->setIcon(QIcon(path)); +} + +void NewInstanceDialog::setSuggestedIcon(const QString& key) +{ + if (key == "default") + return; + + auto icon = APPLICATION->icons()->getIcon(key); + importIcon = false; + + ui->iconButton->setIcon(icon); +} + +InstanceTask* NewInstanceDialog::extractTask() +{ + InstanceTask* extracted = creationTask.release(); + + InstanceName inst_name(ui->instNameTextBox->placeholderText().trimmed(), importVersion); + inst_name.setName(ui->instNameTextBox->text().trimmed()); + extracted->setName(inst_name); + + extracted->setGroup(instGroup()); + extracted->setIcon(iconKey()); + return extracted; +} + +void NewInstanceDialog::updateDialogState() +{ + auto allowOK = creationTask && !instName().isEmpty(); + auto OkButton = m_buttons->button(QDialogButtonBox::Ok); + if (OkButton->isEnabled() != allowOK) + { + OkButton->setEnabled(allowOK); + } +} + +QString NewInstanceDialog::instName() const +{ + auto result = ui->instNameTextBox->text().trimmed(); + if (result.size()) + { + return result; + } + result = ui->instNameTextBox->placeholderText().trimmed(); + if (result.size()) + { + return result; + } + return QString(); +} + +QString NewInstanceDialog::instGroup() const +{ + return ui->groupBox->currentText(); +} +QString NewInstanceDialog::iconKey() const +{ + return InstIconKey; +} + +void NewInstanceDialog::on_iconButton_clicked() +{ + importIconNow(); // so the user can switch back + IconPickerDialog dlg(this); + dlg.execWithSelection(InstIconKey); + + if (dlg.result() == QDialog::Accepted) + { + InstIconKey = dlg.selectedIconKey; + ui->iconButton->setIcon(APPLICATION->icons()->getIcon(InstIconKey)); + importIcon = false; + } +} + +void NewInstanceDialog::on_instNameTextBox_textChanged([[maybe_unused]] const QString& arg1) +{ + updateDialogState(); +} + +void NewInstanceDialog::importIconNow() +{ + if (importIcon) + { + APPLICATION->icons()->installIcon(importIconPath, importIconName); + InstIconKey = importIconName.mid(0, importIconName.lastIndexOf('.')); + importIcon = false; + } + APPLICATION->settings()->set("NewInstanceGeometry", QString::fromUtf8(saveGeometry().toBase64())); +} + +void NewInstanceDialog::selectedPageChanged(BasePage* previous, BasePage* selected) +{ + auto prevPage = dynamic_cast<ModpackProviderBasePage*>(previous); + if (prevPage) + { + m_searchTerm = prevPage->getSerachTerm(); + } + + auto nextPage = dynamic_cast<ModpackProviderBasePage*>(selected); + if (nextPage) + { + nextPage->setSearchTerm(m_searchTerm); + } +} diff --git a/archived/projt-launcher/launcher/ui/dialogs/NewInstanceDialog.h b/archived/projt-launcher/launcher/ui/dialogs/NewInstanceDialog.h new file mode 100644 index 0000000000..4ca6eae8e0 --- /dev/null +++ b/archived/projt-launcher/launcher/ui/dialogs/NewInstanceDialog.h @@ -0,0 +1,131 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * 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, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * + * + * Prism Launcher - 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, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * ======================================================================== */ + +#pragma once + +#include <QDialog> + +#include "InstanceTask.h" +#include "ui/pages/BasePageProvider.h" + +namespace Ui +{ + class NewInstanceDialog; +} + +class PageContainer; +class QDialogButtonBox; +class ImportPage; +class FlamePage; + +class NewInstanceDialog : public QDialog, public BasePageProvider +{ + Q_OBJECT + + public: + explicit NewInstanceDialog(const QString& initialGroup, + const QString& url = QString(), + const QMap<QString, QString>& extra_info = {}, + QWidget* parent = 0); + ~NewInstanceDialog(); + + void updateDialogState(); + + void setSuggestedPack(const QString& name = QString(), InstanceTask* task = nullptr); + void setSuggestedPack(const QString& name, QString version, InstanceTask* task = nullptr); + void setSuggestedIconFromFile(const QString& path, const QString& name); + void setSuggestedIcon(const QString& key); + + InstanceTask* extractTask(); + + QString dialogTitle() override; + QList<BasePage*> getPages() override; + + QString instName() const; + QString instGroup() const; + QString iconKey() const; + + public slots: + void accept() override; + void reject() override; + + private slots: + void on_iconButton_clicked(); + void on_instNameTextBox_textChanged(const QString& arg1); + void selectedPageChanged(BasePage* previous, BasePage* selected); + + private: + Ui::NewInstanceDialog* ui = nullptr; + PageContainer* m_container = nullptr; + QDialogButtonBox* m_buttons = nullptr; + + QString InstIconKey; + ImportPage* importPage = nullptr; + std::unique_ptr<InstanceTask> creationTask; + + bool importIcon = false; + QString importIconPath; + QString importIconName; + + QString importVersion; + + QString m_searchTerm; + + void importIconNow(); +}; diff --git a/archived/projt-launcher/launcher/ui/dialogs/NewInstanceDialog.ui b/archived/projt-launcher/launcher/ui/dialogs/NewInstanceDialog.ui new file mode 100644 index 0000000000..8ca0b786f2 --- /dev/null +++ b/archived/projt-launcher/launcher/ui/dialogs/NewInstanceDialog.ui @@ -0,0 +1,91 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>NewInstanceDialog</class> + <widget class="QDialog" name="NewInstanceDialog"> + <property name="windowModality"> + <enum>Qt::ApplicationModal</enum> + </property> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>730</width> + <height>127</height> + </rect> + </property> + <property name="windowTitle"> + <string>New Instance</string> + </property> + <property name="windowIcon"> + <iconset> + <normaloff>:/icons/toolbar/new</normaloff>:/icons/toolbar/new</iconset> + </property> + <property name="modal"> + <bool>true</bool> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <layout class="QGridLayout" name="gridLayout_2"> + <item row="1" column="2"> + <widget class="QComboBox" name="groupBox"> + <property name="editable"> + <bool>true</bool> + </property> + </widget> + </item> + <item row="1" column="1"> + <widget class="QLabel" name="groupLabel"> + <property name="text"> + <string>&Group:</string> + </property> + <property name="buddy"> + <cstring>groupBox</cstring> + </property> + </widget> + </item> + <item row="0" column="2"> + <widget class="QLineEdit" name="instNameTextBox"> + <property name="maxLength"> + <number>128</number> + </property> + </widget> + </item> + <item row="0" column="1"> + <widget class="QLabel" name="nameLabel"> + <property name="text"> + <string>&Name:</string> + </property> + <property name="buddy"> + <cstring>instNameTextBox</cstring> + </property> + </widget> + </item> + <item row="0" column="0" rowspan="2"> + <widget class="QToolButton" name="iconButton"> + <property name="iconSize"> + <size> + <width>80</width> + <height>80</height> + </size> + </property> + </widget> + </item> + </layout> + </item> + <item> + <widget class="Line" name="line"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + </widget> + </item> + </layout> + </widget> + <tabstops> + <tabstop>iconButton</tabstop> + <tabstop>instNameTextBox</tabstop> + <tabstop>groupBox</tabstop> + </tabstops> + <resources/> + <connections/> +</ui> diff --git a/archived/projt-launcher/launcher/ui/dialogs/NewsDialog.cpp b/archived/projt-launcher/launcher/ui/dialogs/NewsDialog.cpp new file mode 100644 index 0000000000..a05f25082c --- /dev/null +++ b/archived/projt-launcher/launcher/ui/dialogs/NewsDialog.cpp @@ -0,0 +1,116 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * 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, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +#include "NewsDialog.h" + +#include "Application.h" +#include "settings/SettingsObject.h" + +#include "ui_NewsDialog.h" + +#include <QUrl> + +NewsDialog::NewsDialog(QList<NewsEntryPtr> entries, QWidget* parent) : QDialog(parent), ui(new Ui::NewsDialog()) +{ + ui->setupUi(this); + + for (auto entry : entries) + { + ui->articleListWidget->addItem(entry->title); + m_entries.insert(entry->title, entry); + } + + connect(ui->articleListWidget, &QListWidget::currentTextChanged, this, &NewsDialog::selectedArticleChanged); + connect(ui->toggleListButton, &QPushButton::clicked, this, &NewsDialog::toggleArticleList); + connect(ui->openInHubButton, + &QPushButton::clicked, + this, + [this]() + { + if (m_current_link.isEmpty()) + return; + emit openHubRequested(QUrl(m_current_link)); + accept(); + }); +#if defined(PROJT_DISABLE_LAUNCHER_HUB) + ui->openInHubButton->setEnabled(false); + ui->openInHubButton->setToolTip(tr("Launcher Hub is not available in this build.")); +#endif + + m_article_list_hidden = ui->articleListWidget->isHidden(); + + auto first_item = ui->articleListWidget->item(0); + if (!first_item) + return; + first_item->setSelected(true); + + auto article_it = m_entries.constFind(first_item->text()); + if (article_it == m_entries.cend() || !article_it.value()) + return; + auto article_entry = article_it.value(); + ui->articleTitleLabel->setText(QString("<a href='%1'>%2</a>").arg(article_entry->link, first_item->text())); + m_current_link = article_entry->link; +#if !defined(PROJT_DISABLE_LAUNCHER_HUB) + ui->openInHubButton->setEnabled(!m_current_link.isEmpty()); +#endif + + ui->currentArticleContentBrowser->setText(article_entry->content); + ui->currentArticleContentBrowser->flush(); + + connect(this, &QDialog::finished, this, [this] { + APPLICATION->settings()->set("NewsGeometry", QString::fromUtf8(saveGeometry().toBase64())); + }); + const QByteArray base64Geometry = APPLICATION->settings()->get("NewsGeometry").toString().toUtf8(); + restoreGeometry(QByteArray::fromBase64(base64Geometry)); +} + +NewsDialog::~NewsDialog() +{ + delete ui; +} + +void NewsDialog::selectedArticleChanged(const QString& new_title) +{ + auto article_it = m_entries.constFind(new_title); + if (article_it == m_entries.cend() || !article_it.value()) + return; + auto article_entry = article_it.value(); + + ui->articleTitleLabel->setText(QString("<a href='%1'>%2</a>").arg(article_entry->link, new_title)); + m_current_link = article_entry->link; +#if !defined(PROJT_DISABLE_LAUNCHER_HUB) + ui->openInHubButton->setEnabled(!m_current_link.isEmpty()); +#endif + + ui->currentArticleContentBrowser->setText(article_entry->content); + ui->currentArticleContentBrowser->flush(); +} + +void NewsDialog::toggleArticleList() +{ + m_article_list_hidden = !m_article_list_hidden; + + ui->articleListWidget->setHidden(m_article_list_hidden); + + if (m_article_list_hidden) + ui->toggleListButton->setText(tr("Show article list")); + else + ui->toggleListButton->setText(tr("Hide article list")); +} diff --git a/archived/projt-launcher/launcher/ui/dialogs/NewsDialog.h b/archived/projt-launcher/launcher/ui/dialogs/NewsDialog.h new file mode 100644 index 0000000000..2a566297b4 --- /dev/null +++ b/archived/projt-launcher/launcher/ui/dialogs/NewsDialog.h @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * 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, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +#pragma once + +#include <QDialog> +#include <QHash> +#include <QPointer> + +#include "news/NewsEntry.h" + +namespace Ui +{ + class NewsDialog; +} + +class NewsDialog : public QDialog +{ + Q_OBJECT + + public: + NewsDialog(QList<NewsEntryPtr> entries, QWidget* parent = nullptr); + ~NewsDialog(); + + signals: + void openHubRequested(const QUrl& url); + + public slots: + void toggleArticleList(); + + private slots: + void selectedArticleChanged(const QString& new_title); + + private: + Ui::NewsDialog* ui; + + QHash<QString, NewsEntryPtr> m_entries; + bool m_article_list_hidden = false; + QString m_current_link; +}; diff --git a/archived/projt-launcher/launcher/ui/dialogs/NewsDialog.ui b/archived/projt-launcher/launcher/ui/dialogs/NewsDialog.ui new file mode 100644 index 0000000000..4bf469d589 --- /dev/null +++ b/archived/projt-launcher/launcher/ui/dialogs/NewsDialog.ui @@ -0,0 +1,130 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>NewsDialog</class> + <widget class="QDialog" name="NewsDialog"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>800</width> + <height>500</height> + </rect> + </property> + <property name="windowTitle"> + <string>News</string> + </property> + <property name="sizeGripEnabled"> + <bool>true</bool> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <layout class="QHBoxLayout" name="horizontalLayout"> + <item> + <layout class="QVBoxLayout" name="leftVerticalLayout"> + <item> + <widget class="QListWidget" name="articleListWidget"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Minimum" vsizetype="Expanding"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + </widget> + </item> + </layout> + </item> + <item> + <layout class="QVBoxLayout" name="rightVerticalLayout"> + <item> + <widget class="QLabel" name="articleTitleLabel"> + <property name="text"> + <string notr="true">Placeholder</string> + </property> + <property name="alignment"> + <set>Qt::AlignCenter</set> + </property> + <property name="openExternalLinks"> + <bool>true</bool> + </property> + </widget> + </item> + <item> + <widget class="ProjectDescriptionPage" name="currentArticleContentBrowser"> + <property name="textInteractionFlags"> + <set>Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse</set> + </property> + <property name="openExternalLinks"> + <bool>true</bool> + </property> + <property name="openLinks"> + <bool>true</bool> + </property> + </widget> + </item> + </layout> + </item> + </layout> + </item> + <item> + <layout class="QGridLayout" name="gridLayout"> + <item row="0" column="2"> + <widget class="QPushButton" name="closeButton"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Fixed"> + <horstretch>10</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string>Close</string> + </property> + </widget> + </item> + <item row="0" column="1"> + <widget class="QPushButton" name="openInHubButton"> + <property name="text"> + <string>Open in Hub</string> + </property> + <property name="icon"> + <iconset theme="internet-web-browser"/> + </property> + </widget> + </item> + <item row="0" column="0"> + <widget class="QPushButton" name="toggleListButton"> + <property name="text"> + <string>Hide article list</string> + </property> + </widget> + </item> + </layout> + </item> + </layout> + </widget> + <customwidgets> + <customwidget> + <class>ProjectDescriptionPage</class> + <extends>QTextBrowser</extends> + <header>ui/widgets/ProjectDescriptionPage.h</header> + </customwidget> + </customwidgets> + <resources/> + <connections> + <connection> + <sender>closeButton</sender> + <signal>clicked()</signal> + <receiver>NewsDialog</receiver> + <slot>accept()</slot> + <hints> + <hint type="sourcelabel"> + <x>199</x> + <y>277</y> + </hint> + <hint type="destinationlabel"> + <x>199</x> + <y>149</y> + </hint> + </hints> + </connection> + </connections> +</ui> diff --git a/archived/projt-launcher/launcher/ui/dialogs/OfflineLoginDialog.cpp b/archived/projt-launcher/launcher/ui/dialogs/OfflineLoginDialog.cpp new file mode 100644 index 0000000000..d2128b36ed --- /dev/null +++ b/archived/projt-launcher/launcher/ui/dialogs/OfflineLoginDialog.cpp @@ -0,0 +1,133 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * 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, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +#include "OfflineLoginDialog.h" +#include "ui_OfflineLoginDialog.h" + +#include <QtWidgets/QPushButton> + +OfflineLoginDialog::OfflineLoginDialog(QWidget* parent) : QDialog(parent), ui(new Ui::OfflineLoginDialog) +{ + ui->setupUi(this); + ui->progressBar->setVisible(false); + ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false); + + ui->buttonBox->button(QDialogButtonBox::Cancel)->setText(tr("Cancel")); + ui->buttonBox->button(QDialogButtonBox::Ok)->setText(tr("OK")); + + connect(ui->buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept); + connect(ui->buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); +} + +OfflineLoginDialog::~OfflineLoginDialog() +{ + delete ui; +} + +// Stage 1: User interaction +void OfflineLoginDialog::accept() +{ + setUserInputsEnabled(false); + ui->progressBar->setVisible(true); + + // Setup the login task and start it + m_account = MinecraftAccount::createOffline(ui->userTextBox->text()); + m_loginTask = m_account->login(); + connect(m_loginTask.get(), &Task::failed, this, &OfflineLoginDialog::onTaskFailed); + connect(m_loginTask.get(), &Task::succeeded, this, &OfflineLoginDialog::onTaskSucceeded); + connect(m_loginTask.get(), &Task::status, this, &OfflineLoginDialog::onTaskStatus); + connect(m_loginTask.get(), &Task::progress, this, &OfflineLoginDialog::onTaskProgress); + m_loginTask->start(); +} + +void OfflineLoginDialog::setUserInputsEnabled(bool enable) +{ + ui->userTextBox->setEnabled(enable); + ui->buttonBox->setEnabled(enable); +} + +void OfflineLoginDialog::on_allowLongUsernames_stateChanged(int value) +{ + if (value == Qt::Checked) + { + ui->userTextBox->setMaxLength(INT_MAX); + } + else + { + ui->userTextBox->setMaxLength(16); + } +} + +// Enable the OK button only when the textbox contains something. +void OfflineLoginDialog::on_userTextBox_textEdited(const QString& newText) +{ + ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(!newText.isEmpty()); +} + +void OfflineLoginDialog::onTaskFailed(const QString& reason) +{ + // Set message + auto lines = reason.split('\n'); + QString processed; + for (auto line : lines) + { + if (line.size()) + { + processed += "<font color='red'>" + line + "</font><br />"; + } + else + { + processed += "<br />"; + } + } + ui->label->setText(processed); + + // Re-enable user-interaction + setUserInputsEnabled(true); + ui->progressBar->setVisible(false); +} + +void OfflineLoginDialog::onTaskSucceeded() +{ + QDialog::accept(); +} + +void OfflineLoginDialog::onTaskStatus(const QString& status) +{ + ui->label->setText(status); +} + +void OfflineLoginDialog::onTaskProgress(qint64 current, qint64 total) +{ + ui->progressBar->setMaximum(total); + ui->progressBar->setValue(current); +} + +// Public interface +MinecraftAccountPtr OfflineLoginDialog::newAccount(QWidget* parent, QString msg) +{ + OfflineLoginDialog dlg(parent); + dlg.ui->label->setText(msg); + if (dlg.exec() == QDialog::Accepted) + { + return dlg.m_account; + } + return nullptr; +} diff --git a/archived/projt-launcher/launcher/ui/dialogs/OfflineLoginDialog.h b/archived/projt-launcher/launcher/ui/dialogs/OfflineLoginDialog.h new file mode 100644 index 0000000000..7f3707b503 --- /dev/null +++ b/archived/projt-launcher/launcher/ui/dialogs/OfflineLoginDialog.h @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * 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, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +#pragma once + +#include <QtWidgets/QDialog> + +#include "minecraft/auth/MinecraftAccount.hpp" +#include "tasks/Task.h" + +namespace Ui +{ + class OfflineLoginDialog; +} + +class OfflineLoginDialog : public QDialog +{ + Q_OBJECT + + public: + ~OfflineLoginDialog(); + + static MinecraftAccountPtr newAccount(QWidget* parent, QString message); + + private: + explicit OfflineLoginDialog(QWidget* parent = 0); + + void setUserInputsEnabled(bool enable); + + protected slots: + void accept(); + + void onTaskFailed(const QString& reason); + void onTaskSucceeded(); + void onTaskStatus(const QString& status); + void onTaskProgress(qint64 current, qint64 total); + + void on_userTextBox_textEdited(const QString& newText); + void on_allowLongUsernames_stateChanged(int value); + + private: + Ui::OfflineLoginDialog* ui; + MinecraftAccountPtr m_account; + Task::Ptr m_loginTask; +}; diff --git a/archived/projt-launcher/launcher/ui/dialogs/OfflineLoginDialog.ui b/archived/projt-launcher/launcher/ui/dialogs/OfflineLoginDialog.ui new file mode 100644 index 0000000000..c2b5c427ab --- /dev/null +++ b/archived/projt-launcher/launcher/ui/dialogs/OfflineLoginDialog.ui @@ -0,0 +1,80 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>OfflineLoginDialog</class> + <widget class="QDialog" name="OfflineLoginDialog"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>400</width> + <height>150</height> + </rect> + </property> + <property name="sizePolicy"> + <sizepolicy hsizetype="Fixed" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="windowTitle"> + <string>Add Account</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <widget class="QLabel" name="label"> + <property name="text"> + <string notr="true">Message label placeholder.</string> + </property> + <property name="textFormat"> + <enum>Qt::RichText</enum> + </property> + <property name="textInteractionFlags"> + <set>Qt::LinksAccessibleByMouse</set> + </property> + </widget> + </item> + <item> + <widget class="QLineEdit" name="userTextBox"> + <property name="maxLength"> + <number>16</number> + </property> + <property name="placeholderText"> + <string>Username</string> + </property> + </widget> + </item> + <item> + <widget class="QCheckBox" name="allowLongUsernames"> + <property name="toolTip"> + <string>Usernames longer than 16 characters cannot be used for LAN games or offline-mode servers.</string> + </property> + <property name="text"> + <string>Allow long usernames</string> + </property> + </widget> + </item> + <item> + <widget class="QProgressBar" name="progressBar"> + <property name="value"> + <number>69</number> + </property> + <property name="textVisible"> + <bool>false</bool> + </property> + </widget> + </item> + <item> + <widget class="QDialogButtonBox" name="buttonBox"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="standardButtons"> + <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set> + </property> + </widget> + </item> + </layout> + </widget> + <resources/> + <connections/> +</ui> diff --git a/archived/projt-launcher/launcher/ui/dialogs/ProfileSelectDialog.cpp b/archived/projt-launcher/launcher/ui/dialogs/ProfileSelectDialog.cpp new file mode 100644 index 0000000000..87c7b73a98 --- /dev/null +++ b/archived/projt-launcher/launcher/ui/dialogs/ProfileSelectDialog.cpp @@ -0,0 +1,143 @@ +// SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0 +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * 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, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * 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 "ProfileSelectDialog.h" +#include "ui_ProfileSelectDialog.h" + +#include <QDebug> +#include <QItemSelectionModel> +#include <QPushButton> + +#include "Application.h" + +#include "ui/dialogs/ProgressDialog.h" + +ProfileSelectDialog::ProfileSelectDialog(const QString& message, int flags, QWidget* parent) + : QDialog(parent), + ui(new Ui::ProfileSelectDialog) +{ + ui->setupUi(this); + + m_accounts = APPLICATION->accounts(); + auto view = ui->listView; + // view->setModel(m_accounts.get()); + // view->hideColumn(AccountList::ActiveColumn); + view->setColumnCount(1); + view->setRootIsDecorated(false); + // Note: Manually populating QTreeWidget for custom rendering, pending refactor to QListView + Delegate. + if (QTreeWidgetItem* header = view->headerItem()) + { + header->setText(0, tr("Name")); + } + else + { + view->setHeaderLabel(tr("Name")); + } + QList<QTreeWidgetItem*> items; + for (int i = 0; i < m_accounts->count(); i++) + { + MinecraftAccountPtr account = m_accounts->at(i); + QString profileLabel; + if (account->isInUse()) + { + profileLabel = tr("%1 (in use)").arg(account->profileName()); + } + else + { + profileLabel = account->profileName(); + } + auto item = new QTreeWidgetItem(view); + item->setText(0, profileLabel); + item->setIcon(0, account->getFace()); + item->setData(0, AccountList::PointerRole, QVariant::fromValue(account)); + items.append(item); + } + view->addTopLevelItems(items); + + // Set the message label. + ui->msgLabel->setVisible(!message.isEmpty()); + ui->msgLabel->setText(message); + + // Flags... + ui->globalDefaultCheck->setVisible(flags & GlobalDefaultCheckbox); + ui->instDefaultCheck->setVisible(flags & InstanceDefaultCheckbox); + qDebug() << flags; + + // Select the first entry in the list. + ui->listView->setCurrentIndex(ui->listView->model()->index(0, 0)); + + connect(ui->listView, &QAbstractItemView::doubleClicked, this, &ProfileSelectDialog::on_buttonBox_accepted); + + ui->buttonBox->button(QDialogButtonBox::Cancel)->setText(tr("Cancel")); + ui->buttonBox->button(QDialogButtonBox::Ok)->setText(tr("OK")); +} + +ProfileSelectDialog::~ProfileSelectDialog() +{ + delete ui; +} + +MinecraftAccountPtr ProfileSelectDialog::selectedAccount() const +{ + return m_selected; +} + +bool ProfileSelectDialog::useAsGlobalDefault() const +{ + return ui->globalDefaultCheck->isChecked(); +} + +bool ProfileSelectDialog::useAsInstDefaullt() const +{ + return ui->instDefaultCheck->isChecked(); +} + +void ProfileSelectDialog::on_buttonBox_accepted() +{ + QModelIndexList selection = ui->listView->selectionModel()->selectedIndexes(); + if (selection.size() > 0) + { + QModelIndex selected = selection.first(); + m_selected = selected.data(AccountList::PointerRole).value<MinecraftAccountPtr>(); + } + close(); +} + +void ProfileSelectDialog::on_buttonBox_rejected() +{ + close(); +} diff --git a/archived/projt-launcher/launcher/ui/dialogs/ProfileSelectDialog.h b/archived/projt-launcher/launcher/ui/dialogs/ProfileSelectDialog.h new file mode 100644 index 0000000000..dfea596591 --- /dev/null +++ b/archived/projt-launcher/launcher/ui/dialogs/ProfileSelectDialog.h @@ -0,0 +1,111 @@ +// SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0 +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * 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, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ======================================================================== */ + +#pragma once + +#include <QDialog> + +#include <memory> + +#include "minecraft/auth/AccountList.hpp" + +namespace Ui +{ + class ProfileSelectDialog; +} + +class ProfileSelectDialog : public QDialog +{ + Q_OBJECT + public: + enum Flags + { + NoFlags = 0, + + /*! + * Shows a check box on the dialog that allows the user to specify that the account + * they've selected should be used as the global default for all instances. + */ + GlobalDefaultCheckbox, + + /*! + * Shows a check box on the dialog that allows the user to specify that the account + * they've selected should be used as the default for the instance they are currently launching. + * This is not currently implemented. + */ + InstanceDefaultCheckbox, + }; + + /*! + * Constructs a new account select dialog with the given parent and message. + * The message will be shown at the top of the dialog. It is an empty string by default. + */ + explicit ProfileSelectDialog(const QString& message = "", int flags = 0, QWidget* parent = 0); + ~ProfileSelectDialog(); + + /*! + * Gets a pointer to the account that the user selected. + * This is null if the user clicked cancel or hasn't clicked OK yet. + */ + MinecraftAccountPtr selectedAccount() const; + + /*! + * Returns true if the user checked the "use as global default" checkbox. + * If the checkbox wasn't shown, this function returns false. + */ + bool useAsGlobalDefault() const; + + /*! + * Returns true if the user checked the "use as instance default" checkbox. + * If the checkbox wasn't shown, this function returns false. + */ + bool useAsInstDefaullt() const; + + public slots: + void on_buttonBox_accepted(); + + void on_buttonBox_rejected(); + + protected: + shared_qobject_ptr<AccountList> m_accounts; + + //! The account that was selected when the user clicked OK. + MinecraftAccountPtr m_selected; + + private: + Ui::ProfileSelectDialog* ui; +}; diff --git a/archived/projt-launcher/launcher/ui/dialogs/ProfileSelectDialog.ui b/archived/projt-launcher/launcher/ui/dialogs/ProfileSelectDialog.ui new file mode 100644 index 0000000000..e779b51bf1 --- /dev/null +++ b/archived/projt-launcher/launcher/ui/dialogs/ProfileSelectDialog.ui @@ -0,0 +1,62 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>ProfileSelectDialog</class> + <widget class="QDialog" name="ProfileSelectDialog"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>465</width> + <height>300</height> + </rect> + </property> + <property name="windowTitle"> + <string>Select an Account</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <widget class="QLabel" name="msgLabel"> + <property name="text"> + <string>Select a profile.</string> + </property> + </widget> + </item> + <item> + <widget class="QTreeWidget" name="listView"> + <column> + <property name="text"> + <string notr="true">1</string> + </property> + </column> + </widget> + </item> + <item> + <layout class="QHBoxLayout" name="horizontalLayout"> + <item> + <widget class="QCheckBox" name="globalDefaultCheck"> + <property name="text"> + <string>Use as default?</string> + </property> + </widget> + </item> + <item> + <widget class="QCheckBox" name="instDefaultCheck"> + <property name="text"> + <string>Use as default for this instance only?</string> + </property> + </widget> + </item> + </layout> + </item> + <item> + <widget class="QDialogButtonBox" name="buttonBox"> + <property name="standardButtons"> + <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set> + </property> + </widget> + </item> + </layout> + </widget> + <resources/> + <connections/> +</ui> diff --git a/archived/projt-launcher/launcher/ui/dialogs/ProfileSetupDialog.cpp b/archived/projt-launcher/launcher/ui/dialogs/ProfileSetupDialog.cpp new file mode 100644 index 0000000000..2c4a523121 --- /dev/null +++ b/archived/projt-launcher/launcher/ui/dialogs/ProfileSetupDialog.cpp @@ -0,0 +1,345 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * 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, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * + * + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> + * Copyright (C) 2023 TheKodeToad <TheKodeToad@proton.me> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * 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, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * 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 "ProfileSetupDialog.h" +#include "net/RawHeaderProxy.h" +#include "ui_ProfileSetupDialog.h" + +#include <QAction> +#include <QDebug> +#include <QJsonDocument> +#include <QPushButton> +#include <QRegularExpressionValidator> + +#include "ui/dialogs/ProgressDialog.h" + +#include <Application.h> +#include "minecraft/auth/Parsers.hpp" +#include "net/Upload.h" + +ProfileSetupDialog::ProfileSetupDialog(MinecraftAccountPtr accountToSetup, QWidget* parent) + : QDialog(parent), + m_accountToSetup(accountToSetup), + ui(new Ui::ProfileSetupDialog) +{ + ui->setupUi(this); + ui->errorLabel->setVisible(false); + + goodIcon = QIcon::fromTheme("status-good"); + yellowIcon = QIcon::fromTheme("status-yellow"); + badIcon = QIcon::fromTheme("status-bad"); + + static const QRegularExpression s_permittedNames("[a-zA-Z0-9_]{3,16}"); + auto nameEdit = ui->nameEdit; + nameEdit->setValidator(new QRegularExpressionValidator(s_permittedNames)); + nameEdit->setClearButtonEnabled(true); + validityAction = nameEdit->addAction(yellowIcon, QLineEdit::LeadingPosition); + connect(nameEdit, &QLineEdit::textEdited, this, &ProfileSetupDialog::nameEdited); + + checkStartTimer.setSingleShot(true); + connect(&checkStartTimer, &QTimer::timeout, this, &ProfileSetupDialog::startCheck); + + setNameStatus(NameStatus::NotSet, QString()); + + ui->buttonBox->button(QDialogButtonBox::Cancel)->setText(tr("Cancel")); + ui->buttonBox->button(QDialogButtonBox::Ok)->setText(tr("OK")); +} + +ProfileSetupDialog::~ProfileSetupDialog() +{ + delete ui; +} + +void ProfileSetupDialog::on_buttonBox_accepted() +{ + setupProfile(currentCheck); +} + +void ProfileSetupDialog::on_buttonBox_rejected() +{ + reject(); +} + +void ProfileSetupDialog::setNameStatus(ProfileSetupDialog::NameStatus status, QString errorString = QString()) +{ + nameStatus = status; + auto okButton = ui->buttonBox->button(QDialogButtonBox::Ok); + switch (nameStatus) + { + case NameStatus::Available: + { + validityAction->setIcon(goodIcon); + okButton->setEnabled(true); + } + break; + case NameStatus::NotSet: + case NameStatus::Pending: + validityAction->setIcon(yellowIcon); + okButton->setEnabled(false); + break; + case NameStatus::Exists: + case NameStatus::Error: + validityAction->setIcon(badIcon); + okButton->setEnabled(false); + break; + } + if (!errorString.isEmpty()) + { + ui->errorLabel->setText(errorString); + ui->errorLabel->setVisible(true); + } + else + { + ui->errorLabel->setVisible(false); + } +} + +void ProfileSetupDialog::nameEdited(const QString& name) +{ + if (!ui->nameEdit->hasAcceptableInput()) + { + setNameStatus(NameStatus::NotSet, tr("Name is too short - must be between 3 and 16 characters long.")); + return; + } + scheduleCheck(name); +} + +void ProfileSetupDialog::scheduleCheck(const QString& name) +{ + queuedCheck = name; + setNameStatus(NameStatus::Pending); + checkStartTimer.start(1000); +} + +void ProfileSetupDialog::startCheck() +{ + if (isChecking) + { + return; + } + if (queuedCheck.isNull()) + { + return; + } + checkName(queuedCheck); +} + +void ProfileSetupDialog::checkName(const QString& name) +{ + if (isChecking) + { + return; + } + + currentCheck = name; + isChecking = true; + + QUrl url(QString("https://api.minecraftservices.com/minecraft/profile/name/%1/available").arg(name)); + auto headers = QList<Net::HeaderPair>{ { "Content-Type", "application/json" }, + { "Accept", "application/json" }, + { "Authorization", + QString("Bearer %1").arg(m_accountToSetup->accessToken()).toUtf8() } }; + + m_check_response.reset(new QByteArray()); + if (m_check_task) + disconnect(m_check_task.get(), nullptr, this, nullptr); + m_check_task = Net::Download::makeByteArray(url, m_check_response); + m_check_task->addHeaderProxy(new Net::RawHeaderProxy(headers)); + + connect(m_check_task.get(), &Task::finished, this, &ProfileSetupDialog::checkFinished); + + m_check_task->setNetwork(APPLICATION->network()); + m_check_task->start(); +} + +void ProfileSetupDialog::checkFinished() +{ + if (m_check_task->error() == QNetworkReply::NoError) + { + auto doc = QJsonDocument::fromJson(*m_check_response); + auto root = doc.object(); + auto statusValue = root.value("status").toString("INVALID"); + if (statusValue == "AVAILABLE") + { + setNameStatus(NameStatus::Available); + } + else if (statusValue == "DUPLICATE") + { + setNameStatus(NameStatus::Exists, tr("Minecraft profile with name %1 already exists.").arg(currentCheck)); + } + else if (statusValue == "NOT_ALLOWED") + { + setNameStatus(NameStatus::Exists, tr("The name %1 is not allowed.").arg(currentCheck)); + } + else + { + setNameStatus(NameStatus::Error, tr("Unhandled profile name status: %1").arg(statusValue)); + } + } + else + { + setNameStatus(NameStatus::Error, tr("Failed to check name availability.")); + } + isChecking = false; +} + +void ProfileSetupDialog::setupProfile(const QString& profileName) +{ + if (isWorking) + { + return; + } + + QString payloadTemplate("{\"profileName\":\"%1\"}"); + + QUrl url("https://api.minecraftservices.com/minecraft/profile"); + auto headers = QList<Net::HeaderPair>{ { "Content-Type", "application/json" }, + { "Accept", "application/json" }, + { "Authorization", + QString("Bearer %1").arg(m_accountToSetup->accessToken()).toUtf8() } }; + + m_profile_response.reset(new QByteArray()); + m_profile_task = Net::Upload::makeByteArray(url, m_profile_response, payloadTemplate.arg(profileName).toUtf8()); + m_profile_task->addHeaderProxy(new Net::RawHeaderProxy(headers)); + + connect(m_profile_task.get(), &Task::finished, this, &ProfileSetupDialog::setupProfileFinished); + + m_profile_task->setNetwork(APPLICATION->network()); + m_profile_task->start(); + + isWorking = true; + + auto button = ui->buttonBox->button(QDialogButtonBox::Cancel); + button->setEnabled(false); +} + +namespace +{ + + struct MojangError + { + static MojangError fromJSON(QByteArray data) + { + MojangError out; + out.rawError = QString::fromUtf8(data); + auto doc = QJsonDocument::fromJson(data, &out.parseError); + + out.fullyParsed = false; + if (!out.parseError.error) + { + auto object = doc.object(); + out.fullyParsed = true; + out.fullyParsed &= Parsers::getString(object.value("path"), out.path); + out.fullyParsed &= Parsers::getString(object.value("error"), out.error); + out.fullyParsed &= Parsers::getString(object.value("errorMessage"), out.errorMessage); + } + + return out; + } + + QString rawError; + QJsonParseError parseError; + bool fullyParsed; + + QString path; + QString error; + QString errorMessage; + }; + +} // namespace + +void ProfileSetupDialog::setupProfileFinished() +{ + isWorking = false; + if (m_profile_task->error() == QNetworkReply::NoError) + { + /* + * data contains the profile in the response + * ... we could parse it and update the account, but let's just return back to the normal login flow instead... + */ + accept(); + } + else + { + auto parsedError = MojangError::fromJSON(*m_profile_response); + ui->errorLabel->setVisible(true); + + QString errorMessage = + tr("Network Error: %1\nHTTP Status: %2") + .arg(m_profile_task->errorString(), QString::number(m_profile_task->replyStatusCode())); + + if (parsedError.fullyParsed) + { + errorMessage += "Path: " + parsedError.path + "\n"; + errorMessage += "Error: " + parsedError.error + "\n"; + errorMessage += "Message: " + parsedError.errorMessage + "\n"; + } + else + { + errorMessage += "Failed to parse error from Mojang API: " + parsedError.parseError.errorString() + "\n"; + errorMessage += "Log:\n" + parsedError.rawError + "\n"; + } + + ui->errorLabel->setText(tr("The server responded with the following error:") + "\n\n" + errorMessage); + qDebug() << parsedError.rawError; + auto button = ui->buttonBox->button(QDialogButtonBox::Cancel); + button->setEnabled(true); + } +} diff --git a/archived/projt-launcher/launcher/ui/dialogs/ProfileSetupDialog.h b/archived/projt-launcher/launcher/ui/dialogs/ProfileSetupDialog.h new file mode 100644 index 0000000000..f46af1e118 --- /dev/null +++ b/archived/projt-launcher/launcher/ui/dialogs/ProfileSetupDialog.h @@ -0,0 +1,109 @@ +// SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0 +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * 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, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * 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 <QIcon> +#include <QNetworkReply> +#include <QTimer> + +#include <minecraft/auth/MinecraftAccount.hpp> +#include <memory> +#include "net/Download.h" +#include "net/Upload.h" + +namespace Ui +{ + class ProfileSetupDialog; +} + +class ProfileSetupDialog : public QDialog +{ + Q_OBJECT + public: + explicit ProfileSetupDialog(MinecraftAccountPtr accountToSetup, QWidget* parent = 0); + ~ProfileSetupDialog(); + + enum class NameStatus + { + NotSet, + Pending, + Available, + Exists, + Error + } nameStatus = NameStatus::NotSet; + + private slots: + void on_buttonBox_accepted(); + void on_buttonBox_rejected(); + + void nameEdited(const QString& name); + void startCheck(); + + void checkFinished(); + void setupProfileFinished(); + + protected: + void scheduleCheck(const QString& name); + void checkName(const QString& name); + void setNameStatus(NameStatus status, QString errorString); + + void setupProfile(const QString& profileName); + + private: + MinecraftAccountPtr m_accountToSetup; + Ui::ProfileSetupDialog* ui; + QIcon goodIcon; + QIcon yellowIcon; + QIcon badIcon; + QAction* validityAction = nullptr; + + QString queuedCheck; + + bool isChecking = false; + bool isWorking = false; + QString currentCheck; + + QTimer checkStartTimer; + + std::shared_ptr<QByteArray> m_check_response; + Net::Download::Ptr m_check_task; + + std::shared_ptr<QByteArray> m_profile_response; + Net::Upload::Ptr m_profile_task; +}; diff --git a/archived/projt-launcher/launcher/ui/dialogs/ProfileSetupDialog.ui b/archived/projt-launcher/launcher/ui/dialogs/ProfileSetupDialog.ui new file mode 100644 index 0000000000..947110da74 --- /dev/null +++ b/archived/projt-launcher/launcher/ui/dialogs/ProfileSetupDialog.ui @@ -0,0 +1,77 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>ProfileSetupDialog</class> + <widget class="QDialog" name="ProfileSetupDialog"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>615</width> + <height>208</height> + </rect> + </property> + <property name="windowTitle"> + <string>Choose Minecraft name</string> + </property> + <layout class="QGridLayout" name="gridLayout"> + <item row="0" column="0" colspan="2"> + <widget class="QLabel" name="descriptionLabel"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Minimum" vsizetype="Minimum"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string>You just need to take one more step to be able to play Minecraft on this account. + +Choose your name carefully:</string> + </property> + <property name="wordWrap"> + <bool>true</bool> + </property> + <property name="textInteractionFlags"> + <set>Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse</set> + </property> + <property name="buddy"> + <cstring>nameEdit</cstring> + </property> + </widget> + </item> + <item row="2" column="0"> + <widget class="QLineEdit" name="nameEdit"/> + </item> + <item row="4" column="0" colspan="2"> + <widget class="QDialogButtonBox" name="buttonBox"> + <property name="standardButtons"> + <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set> + </property> + </widget> + </item> + <item row="3" column="0"> + <widget class="QLabel" name="errorLabel"> + <property name="enabled"> + <bool>true</bool> + </property> + <property name="text"> + <string notr="true">Errors go here</string> + </property> + <property name="wordWrap"> + <bool>true</bool> + </property> + <property name="openExternalLinks"> + <bool>true</bool> + </property> + <property name="textInteractionFlags"> + <set>Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse</set> + </property> + </widget> + </item> + </layout> + </widget> + <tabstops> + <tabstop>nameEdit</tabstop> + </tabstops> + <resources/> + <connections/> +</ui> diff --git a/archived/projt-launcher/launcher/ui/dialogs/ProgressDialog.cpp b/archived/projt-launcher/launcher/ui/dialogs/ProgressDialog.cpp new file mode 100644 index 0000000000..75ee8575a6 --- /dev/null +++ b/archived/projt-launcher/launcher/ui/dialogs/ProgressDialog.cpp @@ -0,0 +1,361 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * 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, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * + * + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2023 Rachel Powers <508861+Ryex@users.noreply.github.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * ======================================================================== */ + +#include "ProgressDialog.h" +#include <QPoint> +#include "ui_ProgressDialog.h" + +#include <QDebug> +#include <QKeyEvent> +#include <limits> + +#include "tasks/Task.h" + +#include "ui/widgets/SubTaskProgressBar.h" + +// map a value in a numeric range of an arbitrary type to between 0 and INT_MAX +// for getting the best precision out of the qt progress bar +template <typename T, std::enable_if_t<std::is_arithmetic_v<T>, bool> = true> +std::tuple<int, int> map_int_zero_max(T current, T range_max, T range_min) +{ + int int_max = std::numeric_limits<int>::max(); + + auto type_range = range_max - range_min; + double percentage = static_cast<double>(current - range_min) / static_cast<double>(type_range); + int mapped_current = percentage * int_max; + + return { mapped_current, int_max }; +} + +ProgressDialog::ProgressDialog(QWidget* parent) : QDialog(parent), ui(new Ui::ProgressDialog) +{ + ui->setupUi(this); + ui->taskProgressScrollArea->setHidden(true); + this->setWindowFlags(this->windowFlags() & ~Qt::WindowContextHelpButtonHint); + setAttribute(Qt::WidgetAttribute::WA_QuitOnClose, true); + changeProgress(0, 100); + updateSize(true); + setSkipButton(false); +} + +void ProgressDialog::setSkipButton(bool present, QString label) +{ + ui->skipButton->setAutoDefault(false); + ui->skipButton->setDefault(false); + ui->skipButton->setFocusPolicy(Qt::ClickFocus); + ui->skipButton->setEnabled(present); + ui->skipButton->setVisible(present); + ui->skipButton->setText(label); + updateSize(); +} + +void ProgressDialog::on_skipButton_clicked(bool checked) +{ + Q_UNUSED(checked); + if (ui->skipButton->isEnabled()) // prevent other triggers from aborting + m_task->abort(); +} + +ProgressDialog::~ProgressDialog() +{ + for (auto conn : this->m_taskConnections) + { + disconnect(conn); + } + delete ui; +} + +void ProgressDialog::updateSize(bool recenterParent) +{ + QSize lastSize = this->size(); + QPoint lastPos = this->pos(); + int minHeight = ui->globalStatusDetailsLabel->minimumSize().height() + (ui->verticalLayout->spacing() * 2); + minHeight += ui->globalProgressBar->minimumSize().height() + ui->verticalLayout->spacing(); + if (!ui->taskProgressScrollArea->isHidden()) + minHeight += ui->taskProgressScrollArea->minimumSizeHint().height() + ui->verticalLayout->spacing(); + if (ui->skipButton->isVisible()) + minHeight += ui->skipButton->height() + ui->verticalLayout->spacing(); + minHeight = std::max(minHeight, 60); + QSize minSize = QSize(480, minHeight); + + setMinimumSize(minSize); + adjustSize(); + + QSize newSize = this->size(); + // if the current window is a different size + auto parent = this->parentWidget(); + if (recenterParent && parent) + { + auto newX = std::max(0, parent->x() + ((parent->width() - newSize.width()) / 2)); + auto newY = std::max(0, parent->y() + ((parent->height() - newSize.height()) / 2)); + this->move(newX, newY); + } + else if (lastSize != newSize) + { + // center on old position after resize + QSize sizeDiff = lastSize - newSize; // last size was smaller, the results should be negative + auto newX = std::max(0, lastPos.x() + (sizeDiff.width() / 2)); + auto newY = std::max(0, lastPos.y() + (sizeDiff.height() / 2)); + this->move(newX, newY); + } +} + +int ProgressDialog::execWithTask(Task* task) +{ + return execWithTaskInternal(task); +} + +int ProgressDialog::execWithTask(Task& task) +{ + return execWithTaskInternal(&task); +} + +// Preferred overloads: Take ownership of the task via unique_ptr +// The task will be automatically deleted when the dialog is destroyed +int ProgressDialog::execWithTask(std::unique_ptr<Task>&& task) +{ + if (task) + { + connect(this, &ProgressDialog::destroyed, task.get(), &Task::deleteLater); + } + return execWithTaskInternal(task.release()); +} + +int ProgressDialog::execWithTask(std::unique_ptr<Task>& task) +{ + if (task) + { + connect(this, &ProgressDialog::destroyed, task.get(), &Task::deleteLater); + } + return execWithTaskInternal(task.release()); +} + +int ProgressDialog::execWithTaskInternal(Task* task) +{ + this->m_task = task; + + if (!task) + { + qDebug() << "Programmer error: Progress dialog created with null task."; + return QDialog::DialogCode::Accepted; + } + + QDialog::DialogCode result{}; + if (handleImmediateResult(result)) + { + return result; + } + + // Connect signals. + this->m_taskConnections.push_back(connect(task, &Task::started, this, &ProgressDialog::onTaskStarted)); + this->m_taskConnections.push_back(connect(task, &Task::failed, this, &ProgressDialog::onTaskFailed)); + this->m_taskConnections.push_back(connect(task, &Task::succeeded, this, &ProgressDialog::onTaskSucceeded)); + this->m_taskConnections.push_back(connect(task, &Task::status, this, &ProgressDialog::changeStatus)); + this->m_taskConnections.push_back(connect(task, &Task::details, this, &ProgressDialog::changeStatus)); + this->m_taskConnections.push_back(connect(task, &Task::stepProgress, this, &ProgressDialog::changeStepProgress)); + this->m_taskConnections.push_back(connect(task, &Task::progress, this, &ProgressDialog::changeProgress)); + this->m_taskConnections.push_back(connect(task, &Task::aborted, this, &ProgressDialog::hide)); + this->m_taskConnections.push_back( + connect(task, &Task::abortStatusChanged, ui->skipButton, &QPushButton::setEnabled)); + this->m_taskConnections.push_back( + connect(task, &Task::abortButtonTextChanged, ui->skipButton, &QPushButton::setText)); + + m_is_multi_step = task->isMultiStep(); + ui->taskProgressScrollArea->setHidden(!m_is_multi_step); + updateSize(); + + // It's a good idea to start the task after we entered the dialog's event loop :^) + if (!task->isRunning()) + { + QMetaObject::invokeMethod(task, &Task::start, Qt::QueuedConnection); + } + else + { + changeStatus(task->getStatus()); + changeProgress(task->getProgress(), task->getTotalProgress()); + } + + return QDialog::exec(); +} + +bool ProgressDialog::handleImmediateResult(QDialog::DialogCode& result) +{ + if (m_task->isFinished()) + { + if (m_task->wasSuccessful()) + { + result = QDialog::Accepted; + } + else + { + result = QDialog::Rejected; + } + return true; + } + return false; +} + +Task* ProgressDialog::getTask() +{ + return m_task; +} + +void ProgressDialog::onTaskStarted() +{} + +void ProgressDialog::onTaskFailed([[maybe_unused]] QString failure) +{ + reject(); + hide(); +} + +void ProgressDialog::onTaskSucceeded() +{ + accept(); + hide(); +} + +void ProgressDialog::changeStatus([[maybe_unused]] const QString& status) +{ + ui->globalStatusLabel->setText(m_task->getStatus()); + ui->globalStatusLabel->adjustSize(); + ui->globalStatusDetailsLabel->setText(m_task->getDetails()); + ui->globalStatusDetailsLabel->adjustSize(); + + updateSize(); +} + +void ProgressDialog::addTaskProgress(TaskStepProgress const& progress) +{ + SubTaskProgressBar* task_bar = new SubTaskProgressBar(this); + taskProgress.insert(progress.uid, task_bar); + ui->taskProgressLayout->addWidget(task_bar); +} + +void ProgressDialog::changeStepProgress(TaskStepProgress const& task_progress) +{ + m_is_multi_step = true; + if (ui->taskProgressScrollArea->isHidden()) + { + ui->taskProgressScrollArea->setHidden(false); + updateSize(); + } + + if (!taskProgress.contains(task_progress.uid)) + addTaskProgress(task_progress); + auto task_bar = taskProgress.value(task_progress.uid); + + auto const [mapped_current, mapped_total] = map_int_zero_max<qint64>(task_progress.current, task_progress.total, 0); + if (task_progress.total <= 0) + { + task_bar->setRange(0, 0); + } + else + { + task_bar->setRange(0, mapped_total); + } + + task_bar->setValue(mapped_current); + task_bar->setStatus(task_progress.status); + task_bar->setDetails(task_progress.details); + + if (task_progress.isDone()) + { + task_bar->setVisible(false); + } +} + +void ProgressDialog::changeProgress(qint64 current, qint64 total) +{ + ui->globalProgressBar->setMaximum(total); + ui->globalProgressBar->setValue(current); +} + +void ProgressDialog::keyPressEvent(QKeyEvent* e) +{ + if (ui->skipButton->isVisible()) + { + if (e->key() == Qt::Key_Escape) + { + on_skipButton_clicked(true); + return; + } + else if (e->key() == Qt::Key_Tab) + { + ui->skipButton->setFocusPolicy(Qt::StrongFocus); + ui->skipButton->setFocus(); + ui->skipButton->setAutoDefault(true); + ui->skipButton->setDefault(true); + return; + } + } + QDialog::keyPressEvent(e); +} + +void ProgressDialog::closeEvent(QCloseEvent* e) +{ + if (m_task && m_task->isRunning()) + { + e->ignore(); + } + else + { + QDialog::closeEvent(e); + } +} diff --git a/archived/projt-launcher/launcher/ui/dialogs/ProgressDialog.h b/archived/projt-launcher/launcher/ui/dialogs/ProgressDialog.h new file mode 100644 index 0000000000..442765ea88 --- /dev/null +++ b/archived/projt-launcher/launcher/ui/dialogs/ProgressDialog.h @@ -0,0 +1,134 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * 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, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * + * + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2023 Rachel Powers <508861+Ryex@users.noreply.github.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * ======================================================================== */ + +#pragma once + +#include <QDialog> +#include <QHash> +#include <QUuid> +#include <memory> + +#include "QObjectPtr.h" +#include "tasks/Task.h" + +#include "ui/widgets/SubTaskProgressBar.h" + +class Task; +class SequentialTask; + +namespace Ui +{ + class ProgressDialog; +} + +class ProgressDialog : public QDialog +{ + Q_OBJECT + + public: + explicit ProgressDialog(QWidget* parent = 0); + ~ProgressDialog(); + + void updateSize(bool recenterParent = false); + + // Deprecated: Use unique_ptr overloads instead for better ownership semantics + [[deprecated("Use execWithTask(std::unique_ptr<Task>&&) instead")]] int execWithTask(Task* task); + + // Non-owning overload for tasks managed elsewhere + int execWithTask(Task& task); + + // Preferred: Takes ownership of the task + int execWithTask(std::unique_ptr<Task>&& task); + int execWithTask(std::unique_ptr<Task>& task); + + void setSkipButton(bool present, QString label = QString()); + + Task* getTask(); + + public slots: + void onTaskStarted(); + void onTaskFailed(QString failure); + void onTaskSucceeded(); + + void changeStatus(const QString& status); + void changeProgress(qint64 current, qint64 total); + void changeStepProgress(TaskStepProgress const& task_progress); + + private slots: + void on_skipButton_clicked(bool checked); + + protected: + virtual void keyPressEvent(QKeyEvent* e); + virtual void closeEvent(QCloseEvent* e); + + private: + bool handleImmediateResult(QDialog::DialogCode& result); + void addTaskProgress(TaskStepProgress const& progress); + int execWithTaskInternal(Task* task); + + private: + Ui::ProgressDialog* ui; + + Task* m_task; + + QList<QMetaObject::Connection> m_taskConnections; + + bool m_is_multi_step = false; + QHash<QUuid, SubTaskProgressBar*> taskProgress; +}; diff --git a/archived/projt-launcher/launcher/ui/dialogs/ProgressDialog.ui b/archived/projt-launcher/launcher/ui/dialogs/ProgressDialog.ui new file mode 100644 index 0000000000..156ff247f8 --- /dev/null +++ b/archived/projt-launcher/launcher/ui/dialogs/ProgressDialog.ui @@ -0,0 +1,144 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>ProgressDialog</class> + <widget class="QDialog" name="ProgressDialog"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>480</width> + <height>210</height> + </rect> + </property> + <property name="sizePolicy"> + <sizepolicy hsizetype="Preferred" vsizetype="Preferred"> + <horstretch>1</horstretch> + <verstretch>1</verstretch> + </sizepolicy> + </property> + <property name="minimumSize"> + <size> + <width>480</width> + <height>210</height> + </size> + </property> + <property name="windowTitle"> + <string>Please wait...</string> + </property> + <property name="sizeGripEnabled"> + <bool>true</bool> + </property> + <layout class="QVBoxLayout" name="verticalLayout" stretch="0,0,0,0"> + <item> + <layout class="QHBoxLayout" name="horizontalLayout" stretch="1,0"> + <item> + <widget class="QLabel" name="globalStatusLabel"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Preferred" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="minimumSize"> + <size> + <width>0</width> + <height>15</height> + </size> + </property> + <property name="text"> + <string>Global Task Status...</string> + </property> + <property name="wordWrap"> + <bool>true</bool> + </property> + </widget> + </item> + <item> + <widget class="QLabel" name="globalStatusDetailsLabel"> + <property name="text"> + <string>Global Status Details...</string> + </property> + <property name="alignment"> + <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set> + </property> + </widget> + </item> + </layout> + </item> + <item> + <widget class="QProgressBar" name="globalProgressBar"> + <property name="enabled"> + <bool>true</bool> + </property> + <property name="minimumSize"> + <size> + <width>0</width> + <height>24</height> + </size> + </property> + <property name="value"> + <number>24</number> + </property> + </widget> + </item> + <item> + <widget class="QScrollArea" name="taskProgressScrollArea"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="MinimumExpanding"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="minimumSize"> + <size> + <width>0</width> + <height>100</height> + </size> + </property> + <property name="frameShape"> + <enum>QFrame::StyledPanel</enum> + </property> + <property name="horizontalScrollBarPolicy"> + <enum>Qt::ScrollBarAsNeeded</enum> + </property> + <property name="sizeAdjustPolicy"> + <enum>QAbstractScrollArea::AdjustToContents</enum> + </property> + <property name="widgetResizable"> + <bool>true</bool> + </property> + <widget class="QWidget" name="taskProgressContainer"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>460</width> + <height>108</height> + </rect> + </property> + <layout class="QVBoxLayout" name="taskProgressLayout"> + <property name="spacing"> + <number>2</number> + </property> + </layout> + </widget> + </widget> + </item> + <item> + <widget class="QPushButton" name="skipButton"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string>Skip</string> + </property> + </widget> + </item> + </layout> + </widget> + <resources/> + <connections/> +</ui> diff --git a/archived/projt-launcher/launcher/ui/dialogs/ResourceDownloadDialog.cpp b/archived/projt-launcher/launcher/ui/dialogs/ResourceDownloadDialog.cpp new file mode 100644 index 0000000000..c75859b6db --- /dev/null +++ b/archived/projt-launcher/launcher/ui/dialogs/ResourceDownloadDialog.cpp @@ -0,0 +1,572 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * 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, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * + * + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> + * Copyright (C) 2023 TheKodeToad <TheKodeToad@proton.me> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * 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, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * ======================================================================== */ + +#include "ResourceDownloadDialog.h" +#include <QList> + +#include <QInputDialog> +#include <QLineEdit> +#include <QPushButton> +#include <algorithm> + +#include "Application.h" +#include "ResourceDownloadTask.h" + +#include "minecraft/PackProfile.h" +#include "minecraft/mod/ModFolderModel.hpp" +#include "minecraft/mod/ResourcePackFolderModel.hpp" +#include "minecraft/mod/ShaderPackFolderModel.hpp" +#include "minecraft/mod/TexturePackFolderModel.hpp" + +#include "minecraft/mod/tasks/GetModDependenciesTask.hpp" +#include "modplatform/ModIndex.h" +#include "modplatform/modrinth/ModrinthCollectionImportTask.h" +#include "ui/dialogs/CustomMessageBox.h" +#include "ui/dialogs/ProgressDialog.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 "modplatform/flame/FlameAPI.h" +#include "modplatform/modrinth/ModrinthAPI.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(static_cast<int>(std::max(0.5 * parent->width(), 400.0)), + static_cast<int>(std::max(0.75 * parent->height(), 400.0))); + + setWindowIcon(QIcon::fromTheme("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(), QString::fromUtf8(saveGeometry().toBase64())); + + QDialog::accept(); + } + + void ResourceDownloadDialog::reject() + { + auto selected = getTasks(); + if (selected.count() > 0) + { + auto reply = CustomMessageBox::selectable(this, + tr("Confirmation Needed"), + tr("You have %1 selected resources.\n" + "Are you sure you want to close this dialog?") + .arg(selected.count()), + QMessageBox::Question, + QMessageBox::Yes | QMessageBox::No, + QMessageBox::No) + ->exec(); + if (reply != QMessageBox::Yes) + { + return; + } + } + + if (!geometrySaveKey().isEmpty()) + APPLICATION->settings()->set(geometrySaveKey(), QString::fromUtf8(saveGeometry().toBase64())); + + QDialog::reject(); + } + + // NOTE: We can't have this in the ctor because PageContainer calls a virtual function, and so + // won't work with subclasses if we put it in this ctor. + void ResourceDownloadDialog::initializeContainer() + { + m_container = new PageContainer(this, {}, this); + m_container->setSizePolicy(QSizePolicy::Policy::Preferred, QSizePolicy::Policy::Expanding); + m_container->layout()->setContentsMargins(0, 0, 0, 0); + m_vertical_layout.addWidget(m_container); + + m_container->addButtons(&m_buttons); + + connect(m_container, &PageContainer::selectedPageChanged, this, &ResourceDownloadDialog::selectedPageChanged); + } + + void ResourceDownloadDialog::connectButtons() + { + auto OkButton = m_buttons.button(QDialogButtonBox::Ok); + OkButton->setToolTip( + tr("Opens a new popup to review your selected %1 and confirm your selection. Shortcut: Ctrl+Return") + .arg(resourcesString())); + connect(OkButton, &QPushButton::clicked, this, &ResourceDownloadDialog::confirm); + + auto CancelButton = m_buttons.button(QDialogButtonBox::Cancel); + connect(CancelButton, &QPushButton::clicked, this, &ResourceDownloadDialog::reject); + + auto HelpButton = m_buttons.button(QDialogButtonBox::Help); + connect(HelpButton, &QPushButton::clicked, m_container, &PageContainer::help); + } + + void ResourceDownloadDialog::confirm() + { + auto confirm_dialog = ReviewMessageBox::create(this, tr("Confirm %1 to download").arg(resourcesString())); + confirm_dialog->retranslateUi(resourcesString()); + + QHash<QString, GetModDependenciesTask::PackDependencyExtraInfo> dependencyExtraInfo; + QStringList depNames; + if (auto task = getModDependenciesTask(); task) + { + connect(task.get(), + &Task::failed, + this, + [this](QString reason) + { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->exec(); }); + + auto weak = task.toWeakRef(); + connect( + task.get(), + &Task::succeeded, + this, + [this, weak]() + { + QStringList warnings; + if (auto task = weak.lock()) + { + warnings = task->warnings(); + } + if (warnings.count()) + { + CustomMessageBox::selectable(this, tr("Warnings"), warnings.join('\n'), QMessageBox::Warning) + ->exec(); + } + }); + + // Check for updates + ProgressDialog progress_dialog(this); + progress_dialog.setSkipButton(true, tr("Abort")); + progress_dialog.setWindowTitle(tr("Checking for dependencies...")); + auto ret = progress_dialog.execWithTask(*task); + + // If the dialog was skipped / some download error happened + if (ret == QDialog::DialogCode::Rejected) + { + QMetaObject::invokeMethod(this, "reject", Qt::QueuedConnection); + return; + } + else + { + for (auto dep : task->getDependecies()) + { + addResource(dep->pack, dep->version); + depNames << dep->pack->name; + } + dependencyExtraInfo = task->getExtraInfo(); + } + } + + auto selected = getTasks(); + std::sort(selected.begin(), + selected.end(), + [](const DownloadTaskPtr& a, const DownloadTaskPtr& b) + { return QString::compare(a->getName(), b->getName(), Qt::CaseInsensitive) < 0; }); + for (auto& task : selected) + { + auto extraInfo = dependencyExtraInfo.value(task->getPack()->addonId.toString()); + confirm_dialog->appendResource({ task->getName(), + task->getFilename(), + task->getCustomPath(), + ModPlatform::ProviderCapabilities::name(task->getProvider()), + extraInfo.required_by, + task->getVersion().version_type.toString(), + !extraInfo.maybe_installed }); + } + + if (confirm_dialog->exec()) + { + auto deselected = confirm_dialog->deselectedResources(); + for (auto page : m_container->getPages()) + { + auto res = static_cast<ResourcePage*>(page); + for (auto name : deselected) + res->removeResourceFromPage(name); + } + + this->accept(); + } + else + { + for (auto name : depNames) + removeResource(name); + } + } + + bool ResourceDownloadDialog::selectPage(QString pageId) + { + return m_container->selectPage(pageId); + } + + ResourcePage* ResourceDownloadDialog::selectedPage() + { + return dynamic_cast<ResourcePage*>(m_container->selectedPage()); + } + + void ResourceDownloadDialog::addResource(ModPlatform::IndexedPack::Ptr pack, ModPlatform::IndexedVersion& ver) + { + removeResource(pack->name); + if (auto* page = selectedPage()) + page->addResourceToPage(pack, ver, getBaseModel()); + setButtonStatus(); + } + + void ResourceDownloadDialog::removeResource(const QString& pack_name) + { + for (auto page : m_container->getPages()) + { + if (auto* resourcePage = dynamic_cast<ResourcePage*>(page)) + resourcePage->removeResourceFromPage(pack_name); + } + setButtonStatus(); + } + + void ResourceDownloadDialog::setButtonStatus() + { + auto selected = false; + for (auto page : m_container->getPages()) + { + if (auto* resourcePage = dynamic_cast<ResourcePage*>(page)) + selected = selected || resourcePage->hasSelectedPacks(); + } + m_buttons.button(QDialogButtonBox::Ok)->setEnabled(selected); + } + + const QList<ResourceDownloadDialog::DownloadTaskPtr> ResourceDownloadDialog::getTasks() + { + QList<DownloadTaskPtr> selected; + for (auto page : m_container->getPages()) + { + if (auto* resourcePage = dynamic_cast<ResourcePage*>(page)) + selected.append(resourcePage->selectedPacks()); + } + return selected; + } + + void ResourceDownloadDialog::selectedPageChanged(BasePage* previous, BasePage* selected) + { + auto* prev_page = dynamic_cast<ResourcePage*>(previous); + if (!prev_page) + { + qCritical() << "Selected previous page in ResourceDownloadDialog is not a ResourcePage!"; + return; + } + + // Same effect as having a global search bar + if (auto* result = dynamic_cast<ResourcePage*>(selected)) + result->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 (modrinthPage()) + { + m_importModrinthCollectionButton = + m_buttons.addButton(tr("Import Modrinth Collection"), QDialogButtonBox::ActionRole); + connect(m_importModrinthCollectionButton, + &QPushButton::clicked, + this, + &ModDownloadDialog::importModrinthCollection); + } + + if (!geometrySaveKey().isEmpty()) + restoreGeometry( + QByteArray::fromBase64(APPLICATION->settings()->get(geometrySaveKey()).toString().toUtf8())); + } + + QList<BasePage*> ModDownloadDialog::getPages() + { + QList<BasePage*> pages; + + auto loaders = static_cast<MinecraftInstance*>(m_instance)->getPackProfile()->getSupportedModLoaders().value(); + + if (ModrinthAPI::validateModLoaders(loaders)) + pages.append(ModrinthModPage::create(this, *m_instance)); + if (APPLICATION->capabilities() & Application::SupportsFlame && FlameAPI::validateModLoaders(loaders)) + pages.append(FlameModPage::create(this, *m_instance)); + + return pages; + } + + GetModDependenciesTask::Ptr ModDownloadDialog::getModDependenciesTask() + { + if (!APPLICATION->settings()->get("ModDependenciesDisabled").toBool()) + { // dependencies + if (auto model = dynamic_cast<ModFolderModel*>(getBaseModel().get()); model) + { + QList<std::shared_ptr<GetModDependenciesTask::PackDependency>> selectedVers; + for (auto& selected : getTasks()) + { + selectedVers.append( + std::make_shared<GetModDependenciesTask::PackDependency>(selected->getPack(), + selected->getVersion())); + } + + return makeShared<GetModDependenciesTask>(m_instance, model, selectedVers); + } + } + return nullptr; + } + + ResourcePage* ModDownloadDialog::modrinthPage() const + { + for (auto* page : m_container->getPages()) + { + if (auto* resource_page = dynamic_cast<ResourcePage*>(page); + resource_page && resource_page->id() == "modrinth") + return resource_page; + } + return nullptr; + } + + void ModDownloadDialog::importModrinthCollection() + { + auto* page = modrinthPage(); + if (!page) + return; + + bool ok = false; + auto input = QInputDialog::getText(this, + tr("Import Modrinth Collection"), + tr("Enter a Modrinth collection URL or collection ID:"), + QLineEdit::Normal, + QString(), + &ok); + if (!ok || input.trimmed().isEmpty()) + return; + + auto task = makeShared<ModrinthCollectionImportTask>(input, static_cast<MinecraftInstance*>(m_instance)); + + ProgressDialog progress_dialog(this); + progress_dialog.setSkipButton(true, tr("Abort")); + progress_dialog.setWindowTitle(tr("Importing Modrinth collection...")); + + if (progress_dialog.execWithTask(*task) == QDialog::Rejected) + return; + + selectPage(page->id()); + auto imported_resources = task->importedResources(); + for (auto& imported : imported_resources) + addResource(imported.pack, imported.version); + + QString message; + if (task->collectionName().isEmpty()) + message = tr("Imported %1 mod(s) from Modrinth collection `%2`.") + .arg(task->importedResources().size()) + .arg(input.trimmed()); + else + message = + tr("Imported %1 mod(s) from `%2`.").arg(task->importedResources().size()).arg(task->collectionName()); + + if (!task->skippedResources().isEmpty()) + { + message += "\n\n" + + tr("Skipped %1 project(s) without a compatible version:").arg(task->skippedResources().size()) + + "\n" + task->skippedResources().join(", "); + } + + CustomMessageBox::selectable(this, tr("Collection imported"), message, QMessageBox::Information)->exec(); + } + + ResourcePackDownloadDialog::ResourcePackDownloadDialog( + QWidget* parent, + const std::shared_ptr<ResourcePackFolderModel>& resource_packs, + BaseInstance* instance) + : ResourceDownloadDialog(parent, resource_packs), + m_instance(instance) + { + setWindowTitle(dialogTitle()); + + initializeContainer(); + connectButtons(); + + if (!geometrySaveKey().isEmpty()) + restoreGeometry( + QByteArray::fromBase64(APPLICATION->settings()->get(geometrySaveKey()).toString().toUtf8())); + } + + QList<BasePage*> ResourcePackDownloadDialog::getPages() + { + QList<BasePage*> pages; + + pages.append(ModrinthResourcePackPage::create(this, *m_instance)); + if (APPLICATION->capabilities() & Application::SupportsFlame) + pages.append(FlameResourcePackPage::create(this, *m_instance)); + + return pages; + } + + TexturePackDownloadDialog::TexturePackDownloadDialog(QWidget* parent, + const std::shared_ptr<TexturePackFolderModel>& resource_packs, + BaseInstance* instance) + : ResourceDownloadDialog(parent, resource_packs), + m_instance(instance) + { + setWindowTitle(dialogTitle()); + + initializeContainer(); + connectButtons(); + + if (!geometrySaveKey().isEmpty()) + restoreGeometry( + QByteArray::fromBase64(APPLICATION->settings()->get(geometrySaveKey()).toString().toUtf8())); + } + + QList<BasePage*> TexturePackDownloadDialog::getPages() + { + QList<BasePage*> pages; + + pages.append(ModrinthTexturePackPage::create(this, *m_instance)); + if (APPLICATION->capabilities() & Application::SupportsFlame) + pages.append(FlameTexturePackPage::create(this, *m_instance)); + + return pages; + } + + ShaderPackDownloadDialog::ShaderPackDownloadDialog(QWidget* parent, + const std::shared_ptr<ShaderPackFolderModel>& shaders, + BaseInstance* instance) + : ResourceDownloadDialog(parent, shaders), + m_instance(instance) + { + setWindowTitle(dialogTitle()); + + initializeContainer(); + connectButtons(); + + if (!geometrySaveKey().isEmpty()) + restoreGeometry( + QByteArray::fromBase64(APPLICATION->settings()->get(geometrySaveKey()).toString().toUtf8())); + } + + QList<BasePage*> ShaderPackDownloadDialog::getPages() + { + QList<BasePage*> pages; + pages.append(ModrinthShaderPackPage::create(this, *m_instance)); + if (APPLICATION->capabilities() & Application::SupportsFlame) + pages.append(FlameShaderPackPage::create(this, *m_instance)); + return pages; + } + + void ResourceDownloadDialog::setResourceMetadata(const std::shared_ptr<Metadata::ModStruct>& meta) + { + switch (meta->provider) + { + case ModPlatform::ResourceProvider::MODRINTH: selectPage(Modrinth::id()); break; + case ModPlatform::ResourceProvider::FLAME: selectPage(Flame::id()); break; + } + setWindowTitle(tr("Change %1 version").arg(meta->name)); + m_container->hidePageList(); + m_buttons.hide(); + auto page = selectedPage(); + page->openProject(meta->project_id); + } + + DataPackDownloadDialog::DataPackDownloadDialog(QWidget* parent, + const std::shared_ptr<DataPackFolderModel>& data_packs, + BaseInstance* instance) + : ResourceDownloadDialog(parent, data_packs), + m_instance(instance) + { + setWindowTitle(dialogTitle()); + + initializeContainer(); + connectButtons(); + + if (!geometrySaveKey().isEmpty()) + restoreGeometry(QByteArray::fromBase64(APPLICATION->settings()->get(geometrySaveKey()).toByteArray())); + } + + QList<BasePage*> DataPackDownloadDialog::getPages() + { + QList<BasePage*> pages; + pages.append(ModrinthDataPackPage::create(this, *m_instance)); + if (APPLICATION->capabilities() & Application::SupportsFlame) + pages.append(FlameDataPackPage::create(this, *m_instance)); + return pages; + } + +} // namespace ResourceDownload diff --git a/archived/projt-launcher/launcher/ui/dialogs/ResourceDownloadDialog.h b/archived/projt-launcher/launcher/ui/dialogs/ResourceDownloadDialog.h new file mode 100644 index 0000000000..c7862a7e2c --- /dev/null +++ b/archived/projt-launcher/launcher/ui/dialogs/ResourceDownloadDialog.h @@ -0,0 +1,278 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * 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, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * + * + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> + * Copyright (C) 2023 TheKodeToad <TheKodeToad@proton.me> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * 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, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * ======================================================================== */ + +#pragma once + +#include <QDialog> +#include <QDialogButtonBox> +#include <QHash> +#include <QLayout> + +#include "QObjectPtr.h" +#include "minecraft/mod/DataPackFolderModel.hpp" +#include "minecraft/mod/tasks/GetModDependenciesTask.hpp" +#include "modplatform/ModIndex.h" +#include "ui/pages/BasePageProvider.h" + +class BaseInstance; +class ModFolderModel; +class PageContainer; +class QVBoxLayout; +class QDialogButtonBox; +class ResourceDownloadTask; +class ResourceFolderModel; +class ResourcePackFolderModel; +class TexturePackFolderModel; +class ShaderPackFolderModel; +class QPushButton; + +namespace ResourceDownload +{ + + class ResourcePage; + + class ResourceDownloadDialog : public QDialog, public BasePageProvider + { + Q_OBJECT + + public: + using DownloadTaskPtr = shared_qobject_ptr<ResourceDownloadTask>; + + ResourceDownloadDialog(QWidget* parent, std::shared_ptr<ResourceFolderModel> base_model); + + void initializeContainer(); + void connectButtons(); + + //: String that gets appended to the download dialog title ("Download " + resourcesString()) + virtual QString resourcesString() const + { + return tr("resources"); + } + + QString dialogTitle() override + { + return tr("Download %1").arg(resourcesString()); + }; + + bool selectPage(QString pageId); + ResourcePage* selectedPage(); + + void addResource(ModPlatform::IndexedPack::Ptr, ModPlatform::IndexedVersion&); + void removeResource(const QString&); + + const QList<DownloadTaskPtr> getTasks(); + const std::shared_ptr<ResourceFolderModel> getBaseModel() const + { + return m_base_model; + } + + void setResourceMetadata(const std::shared_ptr<Metadata::ModStruct>& meta); + + public slots: + void accept() override; + void reject() override; + + protected slots: + void selectedPageChanged(BasePage* previous, BasePage* selected); + + virtual void confirm(); + + protected: + virtual QString geometrySaveKey() const + { + return ""; + } + void setButtonStatus(); + + virtual GetModDependenciesTask::Ptr getModDependenciesTask() + { + return nullptr; + } + + protected: + const std::shared_ptr<ResourceFolderModel> m_base_model; + + PageContainer* m_container = nullptr; + + QDialogButtonBox m_buttons; + QVBoxLayout m_vertical_layout; + }; + + 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()) + QString resourcesString() const override + { + return tr("mods"); + } + QString geometrySaveKey() const override + { + return "ModDownloadGeometry"; + } + + QList<BasePage*> getPages() override; + GetModDependenciesTask::Ptr getModDependenciesTask() override; + + private slots: + void importModrinthCollection(); + + private: + ResourcePage* modrinthPage() const; + + BaseInstance* m_instance; + QPushButton* m_importModrinthCollectionButton = nullptr; + }; + + class ResourcePackDownloadDialog final : public ResourceDownloadDialog + { + Q_OBJECT + + public: + explicit ResourcePackDownloadDialog(QWidget* parent, + const std::shared_ptr<ResourcePackFolderModel>& resource_packs, + BaseInstance* instance); + ~ResourcePackDownloadDialog() override = default; + + //: String that gets appended to the resource pack download dialog title ("Download " + resourcesString()) + QString resourcesString() const override + { + return tr("resource packs"); + } + QString geometrySaveKey() const override + { + return "RPDownloadGeometry"; + } + + QList<BasePage*> getPages() override; + + private: + BaseInstance* m_instance; + }; + + class TexturePackDownloadDialog final : public ResourceDownloadDialog + { + Q_OBJECT + + public: + explicit TexturePackDownloadDialog(QWidget* parent, + const std::shared_ptr<TexturePackFolderModel>& resource_packs, + BaseInstance* instance); + ~TexturePackDownloadDialog() override = default; + + //: String that gets appended to the texture pack download dialog title ("Download " + resourcesString()) + QString resourcesString() const override + { + return tr("texture packs"); + } + QString geometrySaveKey() const override + { + return "TPDownloadGeometry"; + } + + QList<BasePage*> getPages() override; + + private: + BaseInstance* m_instance; + }; + + class ShaderPackDownloadDialog final : public ResourceDownloadDialog + { + Q_OBJECT + + public: + explicit ShaderPackDownloadDialog(QWidget* parent, + const std::shared_ptr<ShaderPackFolderModel>& shader_packs, + BaseInstance* instance); + ~ShaderPackDownloadDialog() override = default; + + //: String that gets appended to the shader pack download dialog title ("Download " + resourcesString()) + QString resourcesString() const override + { + return tr("shader packs"); + } + QString geometrySaveKey() const override + { + return "ShaderDownloadGeometry"; + } + + QList<BasePage*> getPages() override; + + private: + BaseInstance* m_instance; + }; + + class DataPackDownloadDialog final : public ResourceDownloadDialog + { + Q_OBJECT + + public: + explicit DataPackDownloadDialog(QWidget* parent, + const std::shared_ptr<DataPackFolderModel>& data_packs, + BaseInstance* instance); + ~DataPackDownloadDialog() override = default; + + //: String that gets appended to the data pack download dialog title ("Download " + resourcesString()) + QString resourcesString() const override + { + return tr("data packs"); + } + QString geometrySaveKey() const override + { + return "DataPackDownloadGeometry"; + } + + QList<BasePage*> getPages() override; + + private: + BaseInstance* m_instance; + }; + +} // namespace ResourceDownload diff --git a/archived/projt-launcher/launcher/ui/dialogs/ResourceUpdateDialog.cpp b/archived/projt-launcher/launcher/ui/dialogs/ResourceUpdateDialog.cpp new file mode 100644 index 0000000000..3bc49dc519 --- /dev/null +++ b/archived/projt-launcher/launcher/ui/dialogs/ResourceUpdateDialog.cpp @@ -0,0 +1,659 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * 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, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +#include "ResourceUpdateDialog.h" +#include "Application.h" +#include "ChooseProviderDialog.h" +#include "CustomMessageBox.h" +#include "ProgressDialog.h" +#include "ScrollMessageBox.h" +#include "StringUtils.h" +#include "minecraft/mod/tasks/GetModDependenciesTask.hpp" +#include "modplatform/ModIndex.h" +#include "modplatform/flame/FlameAPI.h" +#include "tasks/SequentialTask.h" +#include "ui_ReviewMessageBox.h" + +#include "Markdown.h" + +#include "tasks/ConcurrentTask.h" + +#include "minecraft/MinecraftInstance.h" +#include "minecraft/PackProfile.h" + +#include "modplatform/EnsureMetadataTask.h" +#include "modplatform/flame/FlameCheckUpdate.h" +#include "modplatform/modrinth/ModrinthCheckUpdate.h" + +#include <QClipboard> +#include <QShortcut> +#include <QTextBrowser> +#include <QTreeWidgetItem> + +#include <optional> + +static std::list<Version> mcVersions(BaseInstance* inst) +{ + return { static_cast<MinecraftInstance*>(inst)->getPackProfile()->getComponent("net.minecraft")->getVersion() }; +} + +ResourceUpdateDialog::ResourceUpdateDialog(QWidget* parent, + BaseInstance* instance, + const std::shared_ptr<ResourceFolderModel> resourceModel, + QList<Resource*>& searchFor, + bool includeDeps, + QList<ModPlatform::ModLoaderType> loadersList) + : ReviewMessageBox(parent, tr("Confirm resources to update"), ""), + m_parent(parent), + m_resourceModel(resourceModel), + m_candidates(searchFor), + m_secondTryMetadata(new ConcurrentTask("Second Metadata Search", + APPLICATION->settings()->get("NumberOfConcurrentTasks").toInt())), + m_instance(instance), + m_includeDeps(includeDeps), + m_loadersList(std::move(loadersList)) +{ + ReviewMessageBox::setGeometry(0, 0, 800, 600); + + ui->explainLabel->setText(tr("You're about to update the following resources:")); + ui->onlyCheckedLabel->setText(tr("Only resources with a check will be updated!")); +} + +void ResourceUpdateDialog::checkCandidates() +{ + // Ensure mods have valid metadata + auto went_well = ensureMetadata(); + if (!went_well) + { + m_aborted = true; + return; + } + + // Report failed metadata generation + if (!m_failedMetadata.empty()) + { + QString text; + for (const auto& failed : m_failedMetadata) + { + const auto& mod = std::get<0>(failed); + const auto& reason = std::get<1>(failed); + text += tr("Mod name: %1<br>File name: %2<br>Reason: %3<br><br>") + .arg(mod->name(), mod->fileinfo().fileName(), reason); + } + + ScrollMessageBox message_dialog(m_parent, + tr("Metadata generation failed"), + tr("Could not generate metadata for the following resources:<br>" + "Do you wish to proceed without those resources?"), + text); + message_dialog.setModal(true); + if (message_dialog.exec() == QDialog::Rejected) + { + m_aborted = true; + QMetaObject::invokeMethod(this, "reject", Qt::QueuedConnection); + return; + } + } + + auto versions = mcVersions(m_instance); + + SequentialTask check_task(tr("Checking for updates")); + + if (!m_modrinthToUpdate.empty()) + { + m_modrinthCheckTask.reset( + new ModrinthCheckUpdate(m_modrinthToUpdate, versions, m_loadersList, m_resourceModel)); + connect(m_modrinthCheckTask.get(), + &CheckUpdateTask::checkFailed, + this, + [this](Resource* resource, QString reason, QUrl recover_url) + { m_failedCheckUpdate.append({ resource, reason, recover_url }); }); + check_task.addTask(m_modrinthCheckTask); + } + + if (!m_flameToUpdate.empty()) + { + m_flameCheckTask.reset(new FlameCheckUpdate(m_flameToUpdate, versions, m_loadersList, m_resourceModel)); + connect(m_flameCheckTask.get(), + &CheckUpdateTask::checkFailed, + this, + [this](Resource* resource, QString reason, QUrl recover_url) + { m_failedCheckUpdate.append({ resource, reason, recover_url }); }); + check_task.addTask(m_flameCheckTask); + } + + connect(&check_task, + &Task::failed, + this, + [this](QString reason) + { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->exec(); }); + + connect( + &check_task, + &Task::succeeded, + this, + [this, &check_task]() + { + QStringList warnings = check_task.warnings(); + if (warnings.count()) + { + CustomMessageBox::selectable(this, tr("Warnings"), warnings.join('\n'), QMessageBox::Warning)->exec(); + } + }); + + // Check for updates + ProgressDialog progress_dialog(m_parent); + progress_dialog.setSkipButton(true, tr("Abort")); + progress_dialog.setWindowTitle(tr("Checking for updates...")); + auto ret = progress_dialog.execWithTask(check_task); + + // If the dialog was skipped / some download error happened + if (ret == QDialog::DialogCode::Rejected) + { + m_aborted = true; + QMetaObject::invokeMethod(this, "reject", Qt::QueuedConnection); + return; + } + + QList<std::shared_ptr<GetModDependenciesTask::PackDependency>> selectedVers; + + // Add found updates for Modrinth + if (m_modrinthCheckTask) + { + auto modrinth_updates = m_modrinthCheckTask->getUpdates(); + for (auto& updatable : modrinth_updates) + { + qDebug() << QString("Mod %1 has an update available!").arg(updatable.name); + + appendResource(updatable); + m_tasks.insert(updatable.name, updatable.download); + } + selectedVers.append(m_modrinthCheckTask->getDependencies()); + } + + // Add found updated for Flame + if (m_flameCheckTask) + { + auto flame_updates = m_flameCheckTask->getUpdates(); + for (auto& updatable : flame_updates) + { + qDebug() << QString("Mod %1 has an update available!").arg(updatable.name); + + appendResource(updatable); + m_tasks.insert(updatable.name, updatable.download); + } + selectedVers.append(m_flameCheckTask->getDependencies()); + } + + // Report failed update checking + if (!m_failedCheckUpdate.empty()) + { + QString text; + for (const auto& failed : m_failedCheckUpdate) + { + const auto& mod = std::get<0>(failed); + const auto& reason = std::get<1>(failed); + const auto& recover_url = std::get<2>(failed); + + qDebug() << mod->name() << " failed to check for updates!"; + + text += tr("Mod name: %1").arg(mod->name()) + "<br>"; + if (!reason.isEmpty()) + text += tr("Reason: %1").arg(reason) + "<br>"; + if (!recover_url.isEmpty()) + //: %1 is the link to download it manually + text += tr("Possible solution: Getting the latest version manually:<br>%1<br>") + .arg(QString("<a href='%1'>%1</a>").arg(recover_url.toString())); + text += "<br>"; + } + + ScrollMessageBox message_dialog(m_parent, + tr("Failed to check for updates"), + tr("Could not check or get the following resources for updates:<br>" + "Do you wish to proceed without those resources?"), + text); + message_dialog.setModal(true); + if (message_dialog.exec() == QDialog::Rejected) + { + m_aborted = true; + QMetaObject::invokeMethod(this, "reject", Qt::QueuedConnection); + return; + } + } + + if (m_includeDeps && !APPLICATION->settings()->get("ModDependenciesDisabled").toBool()) + { // dependencies + auto* mod_model = dynamic_cast<ModFolderModel*>(m_resourceModel.get()); + + if (mod_model != nullptr) + { + auto depTask = makeShared<GetModDependenciesTask>(m_instance, mod_model, selectedVers); + + connect(depTask.get(), + &Task::failed, + this, + [this](const QString& reason) + { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->exec(); }); + auto weak = depTask.toWeakRef(); + connect( + depTask.get(), + &Task::succeeded, + this, + [this, weak]() + { + QStringList warnings; + if (auto depTask = weak.lock()) + { + warnings = depTask->warnings(); + } + if (warnings.count()) + { + CustomMessageBox::selectable(this, tr("Warnings"), warnings.join('\n'), QMessageBox::Warning) + ->exec(); + } + }); + + ProgressDialog progress_dialog_deps(m_parent); + progress_dialog_deps.setSkipButton(true, tr("Abort")); + progress_dialog_deps.setWindowTitle(tr("Checking for dependencies...")); + auto dret = progress_dialog_deps.execWithTask(*depTask); + + // If the dialog was skipped / some download error happened + if (dret == QDialog::DialogCode::Rejected) + { + m_aborted = true; + QMetaObject::invokeMethod(this, "reject", Qt::QueuedConnection); + return; + } + static FlameAPI api; + + auto dependencyExtraInfo = depTask->getExtraInfo(); + + for (const auto& dep : depTask->getDependecies()) + { + auto changelog = dep->version.changelog; + if (dep->pack->provider == ModPlatform::ResourceProvider::FLAME) + changelog = api.getModFileChangelog(dep->version.addonId.toInt(), dep->version.fileId.toInt()); + auto download_task = makeShared<ResourceDownloadTask>(dep->pack, dep->version, m_resourceModel); + auto extraInfo = dependencyExtraInfo.value(dep->version.addonId.toString()); + CheckUpdateTask::Update updatable = { dep->pack->name, dep->version.hash, + tr("Not installed"), dep->version.version, + dep->version.version_type, changelog, + dep->pack->provider, download_task, + !extraInfo.maybe_installed }; + + appendResource(updatable, extraInfo.required_by); + m_tasks.insert(updatable.name, updatable.download); + } + } + } + + // If there's no resource to be updated + if (ui->modTreeWidget->topLevelItemCount() == 0) + { + m_noUpdates = true; + } + else + { + // Sort top-level items alphabetically, then sort each item's children in descending order + // We disable sorting during population and enable only for the final sort + ui->modTreeWidget->setSortingEnabled(false); + + // Collect all top-level items and sort them + QList<QTreeWidgetItem*> items; + while (ui->modTreeWidget->topLevelItemCount() > 0) + { + items.append(ui->modTreeWidget->takeTopLevelItem(0)); + } + + // Sort items alphabetically by their text + std::sort(items.begin(), + items.end(), + [](QTreeWidgetItem* a, QTreeWidgetItem* b) + { return a->text(0).compare(b->text(0), Qt::CaseInsensitive) < 0; }); + + // Re-add items and sort each item's children in descending order + for (auto* item : items) + { + // Sort children before adding back + QList<QTreeWidgetItem*> children; + while (item->childCount() > 0) + { + children.append(item->takeChild(0)); + } + std::sort(children.begin(), + children.end(), + [](QTreeWidgetItem* a, QTreeWidgetItem* b) + { + return a->text(0).compare(b->text(0), Qt::CaseInsensitive) > 0; // Descending + }); + for (auto* child : children) + { + item->addChild(child); + } + + ui->modTreeWidget->addTopLevelItem(item); + } + } + + if (m_aborted || m_noUpdates) + QMetaObject::invokeMethod(this, "reject", Qt::QueuedConnection); +} + +// Part 1: Ensure we have a valid metadata +auto ResourceUpdateDialog::ensureMetadata() -> bool +{ + auto index_dir = indexDir(); + + SequentialTask seq(tr("Looking for metadata")); + + // A better use of data structures here could remove the need for this QHash + QHash<QString, bool> should_try_others; + QList<Resource*> modrinth_tmp; + QList<Resource*> flame_tmp; + + bool confirm_rest = false; + bool try_others_rest = false; + bool skip_rest = false; + ModPlatform::ResourceProvider provider_rest = ModPlatform::ResourceProvider::MODRINTH; + + // adds resource to list based on provider + auto addToTmp = [&modrinth_tmp, &flame_tmp](Resource* resource, ModPlatform::ResourceProvider p) + { + switch (p) + { + case ModPlatform::ResourceProvider::MODRINTH: modrinth_tmp.push_back(resource); break; + case ModPlatform::ResourceProvider::FLAME: flame_tmp.push_back(resource); break; + } + }; + + // ask the user on what provider to seach for the mod first + for (auto candidate : m_candidates) + { + if (candidate->status() != ResourceStatus::NO_METADATA) + { + onMetadataEnsured(candidate); + continue; + } + + if (skip_rest) + continue; + + if (candidate->type() == ResourceType::FOLDER) + { + continue; + } + + if (confirm_rest) + { + addToTmp(candidate, provider_rest); + should_try_others.insert(candidate->internal_id(), try_others_rest); + continue; + } + + ChooseProviderDialog chooser(this); + chooser.setDescription( + tr("The resource '%1' does not have a metadata yet. We need to generate it in order to track relevant " + "information on how to update this mod. " + "To do this, please select a mod provider which we can use to check for updates for this mod.") + .arg(candidate->name())); + auto confirmed = chooser.exec() == QDialog::DialogCode::Accepted; + + auto response = chooser.getResponse(); + + if (response.skip_all) + skip_rest = true; + if (response.confirm_all) + { + confirm_rest = true; + provider_rest = response.chosen; + try_others_rest = response.try_others; + } + + should_try_others.insert(candidate->internal_id(), response.try_others); + + if (confirmed) + addToTmp(candidate, response.chosen); + } + + // prepare task for the modrinth mods + if (!modrinth_tmp.empty()) + { + auto modrinth_task = + makeShared<EnsureMetadataTask>(modrinth_tmp, index_dir, ModPlatform::ResourceProvider::MODRINTH); + connect(modrinth_task.get(), + &EnsureMetadataTask::metadataReady, + [this](Resource* candidate) { onMetadataEnsured(candidate); }); + connect(modrinth_task.get(), + &EnsureMetadataTask::metadataFailed, + [this, &should_try_others](Resource* candidate) + { + onMetadataFailed(candidate, + should_try_others.find(candidate->internal_id()).value(), + ModPlatform::ResourceProvider::MODRINTH); + }); + connect(modrinth_task.get(), + &EnsureMetadataTask::failed, + [this](QString reason) + { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->exec(); }); + + if (modrinth_task->getHashingTask()) + seq.addTask(modrinth_task->getHashingTask()); + + seq.addTask(modrinth_task); + } + + // prepare task for the flame mods + if (!flame_tmp.empty()) + { + auto flame_task = makeShared<EnsureMetadataTask>(flame_tmp, index_dir, ModPlatform::ResourceProvider::FLAME); + connect(flame_task.get(), + &EnsureMetadataTask::metadataReady, + [this](Resource* candidate) { onMetadataEnsured(candidate); }); + connect(flame_task.get(), + &EnsureMetadataTask::metadataFailed, + [this, &should_try_others](Resource* candidate) + { + onMetadataFailed(candidate, + should_try_others.find(candidate->internal_id()).value(), + ModPlatform::ResourceProvider::FLAME); + }); + connect(flame_task.get(), + &EnsureMetadataTask::failed, + [this](QString reason) + { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->exec(); }); + + if (flame_task->getHashingTask()) + seq.addTask(flame_task->getHashingTask()); + + seq.addTask(flame_task); + } + + seq.addTask(m_secondTryMetadata); + + // execute all the tasks + ProgressDialog checking_dialog(m_parent); + checking_dialog.setSkipButton(true, tr("Abort")); + checking_dialog.setWindowTitle(tr("Generating metadata...")); + auto ret_metadata = checking_dialog.execWithTask(seq); + + return (ret_metadata != QDialog::DialogCode::Rejected); +} + +void ResourceUpdateDialog::onMetadataEnsured(Resource* resource) +{ + // When the mod is a folder, for instance + if (!resource->metadata()) + return; + + switch (resource->metadata()->provider) + { + case ModPlatform::ResourceProvider::MODRINTH: m_modrinthToUpdate.push_back(resource); break; + case ModPlatform::ResourceProvider::FLAME: m_flameToUpdate.push_back(resource); break; + } +} + +ModPlatform::ResourceProvider next(ModPlatform::ResourceProvider p) +{ + switch (p) + { + case ModPlatform::ResourceProvider::MODRINTH: return ModPlatform::ResourceProvider::FLAME; + case ModPlatform::ResourceProvider::FLAME: return ModPlatform::ResourceProvider::MODRINTH; + } + + return ModPlatform::ResourceProvider::FLAME; +} + +void ResourceUpdateDialog::onMetadataFailed(Resource* resource, + bool try_others, + ModPlatform::ResourceProvider first_choice) +{ + if (try_others) + { + auto index_dir = indexDir(); + + auto task = makeShared<EnsureMetadataTask>(resource, index_dir, next(first_choice)); + connect(task.get(), + &EnsureMetadataTask::metadataReady, + [this](Resource* candidate) { onMetadataEnsured(candidate); }); + connect(task.get(), + &EnsureMetadataTask::metadataFailed, + [this](Resource* candidate) { onMetadataFailed(candidate, false); }); + connect(task.get(), + &EnsureMetadataTask::failed, + [this](const QString& reason) + { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->exec(); }); + if (task->getHashingTask()) + { + auto seq = makeShared<SequentialTask>(); + seq->addTask(task->getHashingTask()); + seq->addTask(task); + m_secondTryMetadata->addTask(seq); + } + else + { + m_secondTryMetadata->addTask(task); + } + } + else + { + QString reason{ tr("Couldn't find a valid version on the selected mod provider(s)") }; + + m_failedMetadata.append({ resource, reason }); + } +} + +void ResourceUpdateDialog::appendResource(CheckUpdateTask::Update const& info, QStringList requiredBy) +{ + auto item_top = new QTreeWidgetItem(ui->modTreeWidget); + item_top->setCheckState(0, info.enabled ? Qt::CheckState::Checked : Qt::CheckState::Unchecked); + if (!info.enabled) + { + item_top->setToolTip(0, tr("Mod was disabled as it may be already installed.")); + } + item_top->setText(0, info.name); + item_top->setExpanded(true); + + auto provider_item = new QTreeWidgetItem(item_top); + QString provider_name = ModPlatform::ProviderCapabilities::readableName(info.provider); + provider_item->setText(0, tr("Provider: %1").arg(provider_name)); + provider_item->setData(0, Qt::UserRole, provider_name); + + auto old_version_item = new QTreeWidgetItem(item_top); + old_version_item->setText(0, tr("Old version: %1").arg(info.old_version)); + old_version_item->setData(0, Qt::UserRole, info.old_version); + + auto new_version_item = new QTreeWidgetItem(item_top); + new_version_item->setText(0, tr("New version: %1").arg(info.new_version)); + new_version_item->setData(0, Qt::UserRole, info.new_version); + + if (info.new_version_type.has_value()) + { + auto new_version_type_item = new QTreeWidgetItem(item_top); + new_version_type_item->setText(0, tr("New Version Type: %1").arg(info.new_version_type.value().toString())); + new_version_type_item->setData(0, Qt::UserRole, info.new_version_type.value().toString()); + } + + if (!requiredBy.isEmpty()) + { + auto requiredByItem = new QTreeWidgetItem(item_top); + if (requiredBy.length() == 1) + { + requiredByItem->setText(0, tr("Required by: %1").arg(requiredBy.back())); + } + else + { + requiredByItem->setText(0, tr("Required by:")); + auto i = 0; + for (auto req : requiredBy) + { + auto reqItem = new QTreeWidgetItem(requiredByItem); + reqItem->setText(0, req); + reqItem->insertChildren(i++, { reqItem }); + } + } + + ui->toggleDepsButton->show(); + m_deps << item_top; + } + + auto changelog_item = new QTreeWidgetItem(item_top); + changelog_item->setText(0, tr("Changelog of the latest version")); + + auto changelog = new QTreeWidgetItem(changelog_item); + auto changelog_area = new QTextBrowser(); + + QString text = info.changelog; + changelog->setData(0, Qt::UserRole, text); + if (info.provider == ModPlatform::ResourceProvider::MODRINTH) + { + text = markdownToHTML(info.changelog.toUtf8()); + } + + changelog_area->setHtml(StringUtils::htmlListPatch(text)); + changelog_area->setOpenExternalLinks(true); + changelog_area->setLineWrapMode(QTextBrowser::LineWrapMode::WidgetWidth); + changelog_area->setVerticalScrollBarPolicy(Qt::ScrollBarPolicy::ScrollBarAsNeeded); + + ui->modTreeWidget->setItemWidget(changelog, 0, changelog_area); + + ui->modTreeWidget->addTopLevelItem(item_top); +} + +auto ResourceUpdateDialog::getTasks() -> const QList<ResourceDownloadTask::Ptr> +{ + QList<ResourceDownloadTask::Ptr> list; + + auto* item = ui->modTreeWidget->topLevelItem(0); + + for (int i = 1; item != nullptr; ++i) + { + if (item->checkState(0) == Qt::CheckState::Checked) + { + auto taskIt = m_tasks.find(item->text(0)); + if (taskIt != m_tasks.end() && taskIt.value()) + list.push_back(taskIt.value()); + } + + item = ui->modTreeWidget->topLevelItem(i); + } + + return list; +} diff --git a/archived/projt-launcher/launcher/ui/dialogs/ResourceUpdateDialog.h b/archived/projt-launcher/launcher/ui/dialogs/ResourceUpdateDialog.h new file mode 100644 index 0000000000..6b784fe192 --- /dev/null +++ b/archived/projt-launcher/launcher/ui/dialogs/ResourceUpdateDialog.h @@ -0,0 +1,98 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * 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, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +#pragma once + +#include "BaseInstance.h" +#include "ResourceDownloadTask.h" +#include "ReviewMessageBox.h" + +#include "minecraft/mod/ModFolderModel.hpp" + +#include "modplatform/CheckUpdateTask.h" + +class Mod; +class ModrinthCheckUpdate; +class FlameCheckUpdate; +class ConcurrentTask; + +class ResourceUpdateDialog final : public ReviewMessageBox +{ + Q_OBJECT + public: + explicit ResourceUpdateDialog(QWidget* parent, + BaseInstance* instance, + std::shared_ptr<ResourceFolderModel> resourceModel, + QList<Resource*>& searchFor, + bool includeDeps, + QList<ModPlatform::ModLoaderType> loadersList = {}); + + void checkCandidates(); + + void appendResource(const CheckUpdateTask::Update& info, QStringList requiredBy = {}); + + const QList<ResourceDownloadTask::Ptr> getTasks(); + auto indexDir() const -> QDir + { + return m_resourceModel->indexDir(); + } + + auto noUpdates() const -> bool + { + return m_noUpdates; + }; + auto aborted() const -> bool + { + return m_aborted; + }; + + private: + auto ensureMetadata() -> bool; + + private slots: + void onMetadataEnsured(Resource* resource); + void onMetadataFailed(Resource* resource, + bool try_others = false, + ModPlatform::ResourceProvider firstChoice = ModPlatform::ResourceProvider::MODRINTH); + + private: + QWidget* m_parent; + + shared_qobject_ptr<ModrinthCheckUpdate> m_modrinthCheckTask; + shared_qobject_ptr<FlameCheckUpdate> m_flameCheckTask; + + const std::shared_ptr<ResourceFolderModel> m_resourceModel; + + QList<Resource*>& m_candidates; + QList<Resource*> m_modrinthToUpdate; + QList<Resource*> m_flameToUpdate; + + ConcurrentTask::Ptr m_secondTryMetadata; + QList<std::tuple<Resource*, QString>> m_failedMetadata; + QList<std::tuple<Resource*, QString, QUrl>> m_failedCheckUpdate; + + QHash<QString, ResourceDownloadTask::Ptr> m_tasks; + BaseInstance* m_instance; + + bool m_noUpdates = false; + bool m_aborted = false; + bool m_includeDeps = false; + QList<ModPlatform::ModLoaderType> m_loadersList; +}; diff --git a/archived/projt-launcher/launcher/ui/dialogs/ReviewMessageBox.cpp b/archived/projt-launcher/launcher/ui/dialogs/ReviewMessageBox.cpp new file mode 100644 index 0000000000..a3a018fb3a --- /dev/null +++ b/archived/projt-launcher/launcher/ui/dialogs/ReviewMessageBox.cpp @@ -0,0 +1,188 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * 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, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +#include "ReviewMessageBox.h" +#include "ui_ReviewMessageBox.h" + +#include <QClipboard> +#include <QPushButton> +#include <QShortcut> + +ReviewMessageBox::ReviewMessageBox(QWidget* parent, + [[maybe_unused]] QString const& title, + [[maybe_unused]] QString const& icon) + : QDialog(parent), + ui(new Ui::ReviewMessageBox) +{ + ui->setupUi(this); + + auto back_button = ui->buttonBox->button(QDialogButtonBox::Cancel); + back_button->setText(tr("Back")); + + ui->toggleDepsButton->hide(); + 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); + + ui->buttonBox->button(QDialogButtonBox::Cancel)->setText(tr("Cancel")); + ui->buttonBox->button(QDialogButtonBox::Ok)->setText(tr("OK")); + + // Overwrite Ctrl+C functionality to exclude the label when copying text from tree + auto shortcut = new QShortcut(QKeySequence::Copy, ui->modTreeWidget); + connect(shortcut, + &QShortcut::activated, + [this]() + { + auto currentItem = this->ui->modTreeWidget->currentItem(); + if (!currentItem) + return; + auto currentColumn = this->ui->modTreeWidget->currentColumn(); + + auto data = currentItem->data(currentColumn, Qt::UserRole); + QString txt; + + if (data.isValid()) + { + txt = data.toString(); + } + else + { + txt = currentItem->text(currentColumn); + } + + QApplication::clipboard()->setText(txt); + }); +} + +ReviewMessageBox::~ReviewMessageBox() +{ + delete ui; +} + +auto ReviewMessageBox::create(QWidget* parent, QString&& title, QString&& icon) -> ReviewMessageBox* +{ + return new ReviewMessageBox(parent, title, icon); +} + +void ReviewMessageBox::appendResource(ResourceInformation&& info) +{ + auto itemTop = new QTreeWidgetItem(ui->modTreeWidget); + itemTop->setCheckState(0, info.enabled ? Qt::CheckState::Checked : Qt::CheckState::Unchecked); + itemTop->setText(0, info.name); + if (!info.enabled) + { + itemTop->setToolTip(0, tr("Mod was disabled as it may be already installed.")); + } + + auto filenameItem = new QTreeWidgetItem(itemTop); + filenameItem->setText(0, tr("Filename: %1").arg(info.filename)); + filenameItem->setData(0, Qt::UserRole, info.filename); + + auto childIndx = 0; + itemTop->insertChildren(childIndx++, { 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(QIcon::fromTheme("status-yellow"))); + itemTop->setToolTip(childIndx++, + tr("This file will be downloaded to a folder location different from the default, possibly " + "due to its loader requiring it.")); + } + + auto providerItem = new QTreeWidgetItem(itemTop); + providerItem->setText(0, tr("Provider: %1").arg(info.provider)); + providerItem->setData(0, Qt::UserRole, info.provider); + + itemTop->insertChildren(childIndx++, { providerItem }); + + if (!info.required_by.isEmpty()) + { + auto requiredByItem = new QTreeWidgetItem(itemTop); + if (info.required_by.length() == 1) + { + requiredByItem->setText(0, tr("Required by: %1").arg(info.required_by.back())); + } + else + { + requiredByItem->setText(0, tr("Required by:")); + auto i = 0; + for (auto req : info.required_by) + { + auto reqItem = new QTreeWidgetItem(requiredByItem); + reqItem->setText(0, req); + reqItem->insertChildren(i++, { reqItem }); + } + } + + itemTop->insertChildren(childIndx++, { requiredByItem }); + ui->toggleDepsButton->show(); + m_deps << itemTop; + } + + auto versionTypeItem = new QTreeWidgetItem(itemTop); + versionTypeItem->setText(0, tr("Version Type: %1").arg(info.version_type)); + versionTypeItem->setData(0, Qt::UserRole, info.version_type); + + itemTop->insertChildren(childIndx++, { versionTypeItem }); + + ui->modTreeWidget->addTopLevelItem(itemTop); +} + +auto ReviewMessageBox::deselectedResources() -> QStringList +{ + QStringList list; + + auto* item = ui->modTreeWidget->topLevelItem(0); + + for (int i = 1; item != nullptr; ++i) + { + if (item->checkState(0) == Qt::CheckState::Unchecked) + { + list.append(item->text(0)); + } + + item = ui->modTreeWidget->topLevelItem(i); + } + + 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)); +} +void ReviewMessageBox::on_toggleDepsButton_clicked() +{ + m_deps_checked = !m_deps_checked; + auto state = m_deps_checked ? Qt::Checked : Qt::Unchecked; + for (auto dep : m_deps) + dep->setCheckState(0, state); +};
\ No newline at end of file diff --git a/archived/projt-launcher/launcher/ui/dialogs/ReviewMessageBox.h b/archived/projt-launcher/launcher/ui/dialogs/ReviewMessageBox.h new file mode 100644 index 0000000000..3a95da46c4 --- /dev/null +++ b/archived/projt-launcher/launcher/ui/dialogs/ReviewMessageBox.h @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * 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, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +#pragma once + +#include <QDialog> +#include <QTreeWidgetItem> + +namespace Ui +{ + class ReviewMessageBox; +} + +class ReviewMessageBox : public QDialog +{ + Q_OBJECT + + public: + static auto create(QWidget* parent, QString&& title, QString&& icon = "") -> ReviewMessageBox*; + + using ResourceInformation = struct res_info + { + QString name; + QString filename; + QString custom_file_path{}; + QString provider; + QStringList required_by; + QString version_type; + bool enabled = true; + }; + + void appendResource(ResourceInformation&& info); + auto deselectedResources() -> QStringList; + + void retranslateUi(QString resources_name); + + ~ReviewMessageBox() override; + + protected slots: + void on_toggleDepsButton_clicked(); + + protected: + ReviewMessageBox(QWidget* parent, const QString& title, const QString& icon); + + Ui::ReviewMessageBox* ui; + + QList<QTreeWidgetItem*> m_deps; + bool m_deps_checked = true; +}; diff --git a/archived/projt-launcher/launcher/ui/dialogs/ReviewMessageBox.ui b/archived/projt-launcher/launcher/ui/dialogs/ReviewMessageBox.ui new file mode 100644 index 0000000000..dbe351019c --- /dev/null +++ b/archived/projt-launcher/launcher/ui/dialogs/ReviewMessageBox.ui @@ -0,0 +1,74 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>ReviewMessageBox</class> + <widget class="QDialog" name="ReviewMessageBox"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>500</width> + <height>350</height> + </rect> + </property> + <property name="sizeGripEnabled"> + <bool>true</bool> + </property> + <property name="modal"> + <bool>true</bool> + </property> + <layout class="QGridLayout" name="gridLayout"> + <item row="2" column="0"> + <widget class="QTreeWidget" name="modTreeWidget"> + <property name="alternatingRowColors"> + <bool>true</bool> + </property> + <property name="selectionMode"> + <enum>QAbstractItemView::NoSelection</enum> + </property> + <property name="selectionBehavior"> + <enum>QAbstractItemView::SelectItems</enum> + </property> + <attribute name="headerVisible"> + <bool>false</bool> + </attribute> + <column> + <property name="text"> + <string/> + </property> + </column> + <column> + <property name="text"> + <string/> + </property> + </column> + </widget> + </item> + <item row="1" column="0"> + <widget class="QLabel" name="explainLabel"/> + </item> + <item row="5" column="0" rowspan="2"> + <layout class="QHBoxLayout" name="horizontalLayout"> + <item> + <widget class="QPushButton" name="toggleDepsButton"> + <property name="text"> + <string>Toggle Dependencies</string> + </property> + </widget> + </item> + <item> + <widget class="QLabel" name="onlyCheckedLabel"/> + </item> + <item> + <widget class="QDialogButtonBox" name="buttonBox"> + <property name="standardButtons"> + <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set> + </property> + </widget> + </item> + </layout> + </item> + </layout> + </widget> + <resources/> + <connections/> +</ui> diff --git a/archived/projt-launcher/launcher/ui/dialogs/ScrollMessageBox.cpp b/archived/projt-launcher/launcher/ui/dialogs/ScrollMessageBox.cpp new file mode 100644 index 0000000000..807eb0af12 --- /dev/null +++ b/archived/projt-launcher/launcher/ui/dialogs/ScrollMessageBox.cpp @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * 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, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +#include "ScrollMessageBox.h" +#include <QPushButton> +#include "ui_ScrollMessageBox.h" + +ScrollMessageBox::ScrollMessageBox(QWidget* parent, const QString& title, const QString& text, const QString& body) + : QDialog(parent), + ui(new Ui::ScrollMessageBox) +{ + ui->setupUi(this); + this->setWindowTitle(title); + ui->label->setText(text); + ui->textBrowser->setText(body); + + ui->buttonBox->button(QDialogButtonBox::Cancel)->setText(tr("Cancel")); + ui->buttonBox->button(QDialogButtonBox::Ok)->setText(tr("OK")); +} + +ScrollMessageBox::~ScrollMessageBox() +{ + delete ui; +} diff --git a/archived/projt-launcher/launcher/ui/dialogs/ScrollMessageBox.h b/archived/projt-launcher/launcher/ui/dialogs/ScrollMessageBox.h new file mode 100644 index 0000000000..5967c65788 --- /dev/null +++ b/archived/projt-launcher/launcher/ui/dialogs/ScrollMessageBox.h @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * 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, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +#pragma once + +#include <QDialog> + +QT_BEGIN_NAMESPACE +namespace Ui +{ + class ScrollMessageBox; +} +QT_END_NAMESPACE + +class ScrollMessageBox : public QDialog +{ + Q_OBJECT + + public: + ScrollMessageBox(QWidget* parent, const QString& title, const QString& text, const QString& body); + + ~ScrollMessageBox() override; + + private: + Ui::ScrollMessageBox* ui; +}; diff --git a/archived/projt-launcher/launcher/ui/dialogs/ScrollMessageBox.ui b/archived/projt-launcher/launcher/ui/dialogs/ScrollMessageBox.ui new file mode 100644 index 0000000000..e684185f24 --- /dev/null +++ b/archived/projt-launcher/launcher/ui/dialogs/ScrollMessageBox.ui @@ -0,0 +1,84 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>ScrollMessageBox</class> + <widget class="QDialog" name="ScrollMessageBox"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>500</width> + <height>455</height> + </rect> + </property> + <property name="windowTitle"> + <string notr="true">ScrollMessageBox</string> + </property> + <layout class="QGridLayout" name="gridLayout"> + <item row="0" column="0"> + <widget class="QLabel" name="label"> + <property name="text"> + <string notr="true"/> + </property> + <property name="textFormat"> + <enum>Qt::RichText</enum> + </property> + </widget> + </item> + <item row="2" column="0"> + <widget class="QDialogButtonBox" name="buttonBox"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="standardButtons"> + <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set> + </property> + </widget> + </item> + <item row="1" column="0"> + <widget class="QTextBrowser" name="textBrowser"> + <property name="acceptRichText"> + <bool>true</bool> + </property> + <property name="openExternalLinks"> + <bool>true</bool> + </property> + </widget> + </item> + </layout> + </widget> + <resources/> + <connections> + <connection> + <sender>buttonBox</sender> + <signal>accepted()</signal> + <receiver>ScrollMessageBox</receiver> + <slot>accept()</slot> + <hints> + <hint type="sourcelabel"> + <x>199</x> + <y>425</y> + </hint> + <hint type="destinationlabel"> + <x>199</x> + <y>227</y> + </hint> + </hints> + </connection> + <connection> + <sender>buttonBox</sender> + <signal>rejected()</signal> + <receiver>ScrollMessageBox</receiver> + <slot>reject()</slot> + <hints> + <hint type="sourcelabel"> + <x>199</x> + <y>425</y> + </hint> + <hint type="destinationlabel"> + <x>199</x> + <y>227</y> + </hint> + </hints> + </connection> + </connections> +</ui> diff --git a/archived/projt-launcher/launcher/ui/dialogs/UpdateAvailableDialog.cpp b/archived/projt-launcher/launcher/ui/dialogs/UpdateAvailableDialog.cpp new file mode 100644 index 0000000000..3a793d0afa --- /dev/null +++ b/archived/projt-launcher/launcher/ui/dialogs/UpdateAvailableDialog.cpp @@ -0,0 +1,115 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * 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, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * + * + * + * + * + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2023 Rachel Powers <508861+Ryex@users.noreply.github.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * + * + * ======================================================================== */ + +#include "UpdateAvailableDialog.h" +#include <QPushButton> +#include "BuildConfig.h" +#include "Markdown.h" +#include "StringUtils.h" +#include "ui_UpdateAvailableDialog.h" + +UpdateAvailableDialog::UpdateAvailableDialog(const QString& currentVersion, + const QString& availableVersion, + const QString& releaseNotes, + Mode mode, + QWidget* parent) + : QDialog(parent), + ui(new Ui::UpdateAvailableDialog) +{ + ui->setupUi(this); + + QString launcherName = BuildConfig.LAUNCHER_DISPLAYNAME; + + if (mode == Mode::Migration) + { + ui->headerLabel->setText(tr("A new release line of %1 is available!").arg(launcherName)); + ui->versionAvailableLabel->setText(tr("Version %1 is part of a new release line.\n" + "You are currently on %2. Would you like to migrate now?") + .arg(availableVersion) + .arg(currentVersion)); + } + else + { + ui->headerLabel->setText(tr("A new version of %1 is available!").arg(launcherName)); + ui->versionAvailableLabel->setText( + tr("Version %1 is now available - you have %2 . Would you like to download it now?") + .arg(availableVersion) + .arg(currentVersion)); + } + ui->icon->setPixmap(QIcon::fromTheme("checkupdate").pixmap(64)); + + auto releaseNotesHtml = markdownToHTML(releaseNotes); + ui->releaseNotes->setHtml(StringUtils::htmlListPatch(releaseNotesHtml)); + ui->releaseNotes->setOpenExternalLinks(true); + + connect(ui->skipButton, + &QPushButton::clicked, + this, + [this]() + { + setResult(ResultCode::Skip); + done(ResultCode::Skip); + }); + + connect(ui->delayButton, + &QPushButton::clicked, + this, + [this]() + { + setResult(ResultCode::DontInstall); + done(ResultCode::DontInstall); + }); + + connect(ui->installButton, + &QPushButton::clicked, + this, + [this]() + { + setResult(ResultCode::Install); + done(ResultCode::Install); + }); +} diff --git a/archived/projt-launcher/launcher/ui/dialogs/UpdateAvailableDialog.h b/archived/projt-launcher/launcher/ui/dialogs/UpdateAvailableDialog.h new file mode 100644 index 0000000000..27ba508261 --- /dev/null +++ b/archived/projt-launcher/launcher/ui/dialogs/UpdateAvailableDialog.h @@ -0,0 +1,83 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * 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, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * + * + * + * + * + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2023 Rachel Powers <508861+Ryex@users.noreply.github.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * + * + * ======================================================================== */ +#pragma once + +#include <QDialog> + +namespace Ui +{ + class UpdateAvailableDialog; +} + +class UpdateAvailableDialog : public QDialog +{ + Q_OBJECT + + public: + enum class Mode + { + Update, + Migration + }; + + enum ResultCode + { + Install = 10, + DontInstall = 11, + Skip = 12, + }; + + explicit UpdateAvailableDialog(const QString& currentVersion, + const QString& availableVersion, + const QString& releaseNotes, + Mode mode = Mode::Update, + QWidget* parent = 0); + ~UpdateAvailableDialog() = default; + + private: + Ui::UpdateAvailableDialog* ui; +}; diff --git a/archived/projt-launcher/launcher/ui/dialogs/UpdateAvailableDialog.ui b/archived/projt-launcher/launcher/ui/dialogs/UpdateAvailableDialog.ui new file mode 100644 index 0000000000..b0d85f6f00 --- /dev/null +++ b/archived/projt-launcher/launcher/ui/dialogs/UpdateAvailableDialog.ui @@ -0,0 +1,155 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>UpdateAvailableDialog</class> + <widget class="QDialog" name="UpdateAvailableDialog"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>636</width> + <height>352</height> + </rect> + </property> + <property name="windowTitle"> + <string>Update Available</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout_3"> + <item> + <layout class="QHBoxLayout" name="horizontalLayout" stretch="0,1"> + <item> + <layout class="QVBoxLayout" name="leftsideLayout"> + <item> + <widget class="QLabel" name="icon"> + <property name="minimumSize"> + <size> + <width>64</width> + <height>64</height> + </size> + </property> + <property name="text"> + <string/> + </property> + </widget> + </item> + <item> + <spacer name="verticalSpacer"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>40</height> + </size> + </property> + </spacer> + </item> + </layout> + </item> + <item> + <layout class="QVBoxLayout" name="mainLayout"> + <property name="leftMargin"> + <number>9</number> + </property> + <property name="topMargin"> + <number>9</number> + </property> + <property name="rightMargin"> + <number>9</number> + </property> + <property name="bottomMargin"> + <number>9</number> + </property> + <item> + <widget class="QLabel" name="headerLabel"> + <property name="font"> + <font> + <pointsize>11</pointsize> + <weight>75</weight> + <bold>true</bold> + </font> + </property> + <property name="text"> + <string>A new version is available!</string> + </property> + </widget> + </item> + <item> + <widget class="QLabel" name="versionAvailableLabel"> + <property name="text"> + <string>Version %1 is now available - you have %2 . Would you like to download it now?</string> + </property> + </widget> + </item> + <item> + <widget class="QLabel" name="releaseNotesLabel"> + <property name="font"> + <font> + <weight>75</weight> + <bold>true</bold> + </font> + </property> + <property name="text"> + <string>Release Notes:</string> + </property> + </widget> + </item> + <item> + <widget class="QTextBrowser" name="releaseNotes"/> + </item> + </layout> + </item> + </layout> + </item> + <item> + <layout class="QHBoxLayout" name="horizontalLayout_2"> + <item> + <widget class="QPushButton" name="skipButton"> + <property name="text"> + <string>Skip This Version</string> + </property> + </widget> + </item> + <item> + <spacer name="horizontalSpacer"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item> + <widget class="QPushButton" name="delayButton"> + <property name="text"> + <string>Remind Me Later</string> + </property> + <property name="checkable"> + <bool>false</bool> + </property> + <property name="checked"> + <bool>false</bool> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="installButton"> + <property name="text"> + <string>Install Update</string> + </property> + <property name="default"> + <bool>true</bool> + </property> + </widget> + </item> + </layout> + </item> + </layout> + </widget> + <resources/> + <connections/> +</ui> diff --git a/archived/projt-launcher/launcher/ui/dialogs/VersionSelectDialog.cpp b/archived/projt-launcher/launcher/ui/dialogs/VersionSelectDialog.cpp new file mode 100644 index 0000000000..e0ed670ed6 --- /dev/null +++ b/archived/projt-launcher/launcher/ui/dialogs/VersionSelectDialog.cpp @@ -0,0 +1,190 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * 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, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * + * + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2023 Rachel Powers <508861+Ryex@users.noreply.github.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * 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 "VersionSelectDialog.h" + +#include <QDebug> +#include <QtWidgets/QButtonGroup> +#include <QtWidgets/QDialogButtonBox> +#include <QtWidgets/QHBoxLayout> +#include <QtWidgets/QPushButton> +#include <QtWidgets/QVBoxLayout> + +#include "ui/widgets/VersionSelectWidget.h" + +#include "BaseVersion.h" +#include "BaseVersionList.h" + +VersionSelectDialog::VersionSelectDialog(BaseVersionList* vlist, QString title, QWidget* parent, bool cancelable) + : QDialog(parent) +{ + setObjectName(QStringLiteral("VersionSelectDialog")); + resize(400, 347); + m_title = title; // Store title for retranslation + m_verticalLayout = new QVBoxLayout(this); + m_verticalLayout->setObjectName(QStringLiteral("verticalLayout")); + + m_versionWidget = new VersionSelectWidget(parent); + m_verticalLayout->addWidget(m_versionWidget); + + m_horizontalLayout = new QHBoxLayout(); + m_horizontalLayout->setObjectName(QStringLiteral("horizontalLayout")); + + m_refreshButton = new QPushButton(this); + m_refreshButton->setObjectName(QStringLiteral("refreshButton")); + m_horizontalLayout->addWidget(m_refreshButton); + + m_buttonBox = new QDialogButtonBox(this); + m_buttonBox->setObjectName(QStringLiteral("buttonBox")); + m_buttonBox->setOrientation(Qt::Horizontal); + m_buttonBox->setStandardButtons(QDialogButtonBox::Cancel | QDialogButtonBox::Ok); + + m_buttonBox->button(QDialogButtonBox::Ok)->setText(tr("Ok")); + m_buttonBox->button(QDialogButtonBox::Cancel)->setText(tr("Cancel")); + m_horizontalLayout->addWidget(m_buttonBox); + + m_verticalLayout->addLayout(m_horizontalLayout); + + retranslate(); + + connect(m_buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept); + connect(m_versionWidget->view(), &QAbstractItemView::doubleClicked, this, &QDialog::accept); + connect(m_buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); + + QMetaObject::connectSlotsByName(this); + setWindowModality(Qt::WindowModal); + setWindowTitle(title); + + m_vlist = vlist; + + if (!cancelable) + { + m_buttonBox->button(QDialogButtonBox::Cancel)->setEnabled(false); + } +} + +void VersionSelectDialog::retranslate() +{ + setWindowTitle(m_title.isEmpty() ? tr("Choose Version") : m_title); + m_refreshButton->setToolTip(tr("Reloads the version list.")); + m_refreshButton->setText(tr("&Refresh")); +} + +void VersionSelectDialog::setCurrentVersion(const QString& version) +{ + m_currentVersion = version; + m_versionWidget->setCurrentVersion(version); +} + +void VersionSelectDialog::setEmptyString(QString emptyString) +{ + m_versionWidget->setEmptyString(emptyString); +} + +void VersionSelectDialog::setEmptyErrorString(QString emptyErrorString) +{ + m_versionWidget->setEmptyErrorString(emptyErrorString); +} + +void VersionSelectDialog::setResizeOn(int column) +{ + resizeOnColumn = column; +} + +int VersionSelectDialog::exec() +{ + QDialog::open(); + m_versionWidget->initialize(m_vlist, true); + m_versionWidget->selectSearch(); + if (resizeOnColumn != -1) + { + m_versionWidget->setResizeOn(resizeOnColumn); + } + return QDialog::exec(); +} + +void VersionSelectDialog::selectRecommended() +{ + m_versionWidget->selectRecommended(); +} + +BaseVersion::Ptr VersionSelectDialog::selectedVersion() const +{ + return m_versionWidget->selectedVersion(); +} + +void VersionSelectDialog::on_refreshButton_clicked() +{ + m_versionWidget->loadList(); +} + +void VersionSelectDialog::setExactFilter(BaseVersionList::ModelRoles role, QString filter) +{ + m_versionWidget->setExactFilter(role, filter); +} + +void VersionSelectDialog::setExactIfPresentFilter(BaseVersionList::ModelRoles role, QString filter) +{ + m_versionWidget->setExactIfPresentFilter(role, filter); +} + +void VersionSelectDialog::setFuzzyFilter(BaseVersionList::ModelRoles role, QString filter) +{ + m_versionWidget->setFuzzyFilter(role, filter); +} diff --git a/archived/projt-launcher/launcher/ui/dialogs/VersionSelectDialog.h b/archived/projt-launcher/launcher/ui/dialogs/VersionSelectDialog.h new file mode 100644 index 0000000000..ce74dff647 --- /dev/null +++ b/archived/projt-launcher/launcher/ui/dialogs/VersionSelectDialog.h @@ -0,0 +1,107 @@ +// SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0 +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * 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, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * + * + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2023 Rachel Powers <508861+Ryex@users.noreply.github.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * ======================================================================== */ + +#pragma once + +#include <QDialog> + +class VersionSelectWidget; +class QDialogButtonBox; +class QVBoxLayout; +class QHBoxLayout; +class QPushButton; +class BaseVersionList; +#include "BaseVersion.h" +#include "BaseVersionList.h" + +class VersionSelectDialog : public QDialog +{ + Q_OBJECT + public: + VersionSelectDialog(BaseVersionList* vlist, QString title, QWidget* parent = 0, bool cancelable = true); + + int exec() override; + BaseVersion::Ptr selectedVersion() const; + void setResizeOn(int column); + void setEmptyString(QString emptyString); + void setEmptyErrorString(QString emptyErrorString); + void setCurrentVersion(const QString& version); + void selectRecommended(); + + void setExactFilter(BaseVersionList::ModelRoles role, QString filter); + void setExactIfPresentFilter(BaseVersionList::ModelRoles role, QString filter); + void setFuzzyFilter(BaseVersionList::ModelRoles role, QString filter); + + public slots: + virtual void retranslate(); + + private slots: + void on_refreshButton_clicked(); + + private: + QVBoxLayout* m_verticalLayout; + QHBoxLayout* m_horizontalLayout; + VersionSelectWidget* m_versionWidget; + QPushButton* m_refreshButton; + QDialogButtonBox* m_buttonBox; + BaseVersionList* m_vlist = nullptr; + QString m_currentVersion; + QString m_title; // Added member to store the title + int resizeOnColumn = -1; +}; diff --git a/archived/projt-launcher/launcher/ui/dialogs/skins/SkinManageDialog.cpp b/archived/projt-launcher/launcher/ui/dialogs/skins/SkinManageDialog.cpp new file mode 100644 index 0000000000..d060ec8117 --- /dev/null +++ b/archived/projt-launcher/launcher/ui/dialogs/skins/SkinManageDialog.cpp @@ -0,0 +1,715 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * 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, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * + * + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2023-2024 Trial97 <alexandru.tripon97@gmail.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * ======================================================================== */ + +#include "SkinManageDialog.h" +#include "ui/dialogs/skins/draw/SkinOpenGLWindow.h" +#include "ui_SkinManageDialog.h" + +#include <FileSystem.h> +#include <QAction> +#include <QDialog> +#include <QEventLoop> +#include <QFileDialog> +#include <QFileInfo> +#include <QKeyEvent> +#include <QListView> +#include <QMimeDatabase> +#include <QPainter> +#include <QUrl> + +#include "Application.h" +#include "DesktopServices.h" +#include "Json.h" +#include "QObjectPtr.h" + +#include "minecraft/auth/Parsers.hpp" +#include "minecraft/skins/CapeChange.h" +#include "minecraft/skins/CapeListModel.h" +#include "minecraft/skins/SkinDelete.h" +#include "minecraft/skins/SkinList.h" +#include "minecraft/skins/SkinModel.h" +#include "minecraft/skins/SkinUpload.h" + +#include "net/Download.h" +#include "net/NetJob.h" +#include "tasks/Task.h" + +#include "ui/dialogs/CustomMessageBox.h" +#include "ui/dialogs/ProgressDialog.h" +#include "ui/instanceview/InstanceDelegate.h" + +SkinManageDialog::SkinManageDialog(QWidget* parent, MinecraftAccountPtr acct) + : QDialog(parent), + m_acct(acct), + m_ui(new Ui::SkinManageDialog), + m_list(this, APPLICATION->settings()->get("SkinsDir").toString(), acct) +{ + m_ui->setupUi(this); + + m_skinPreview = new SkinOpenGLWindow(this, palette().color(QPalette::Normal, QPalette::Base)); + + setWindowModality(Qt::WindowModal); + + auto contentsWidget = m_ui->listView; + contentsWidget->setViewMode(QListView::IconMode); + contentsWidget->setFlow(QListView::LeftToRight); + contentsWidget->setIconSize(QSize(48, 48)); + contentsWidget->setMovement(QListView::Static); + contentsWidget->setResizeMode(QListView::Adjust); + contentsWidget->setSelectionMode(QAbstractItemView::SingleSelection); + contentsWidget->setSpacing(5); + contentsWidget->setWordWrap(false); + contentsWidget->setWrapping(true); + contentsWidget->setUniformItemSizes(true); + contentsWidget->setTextElideMode(Qt::ElideRight); + contentsWidget->setVerticalScrollMode(QAbstractItemView::ScrollPerPixel); + contentsWidget->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + contentsWidget->installEventFilter(this); + contentsWidget->setItemDelegate(new ListViewDelegate(this)); + + contentsWidget->setAcceptDrops(true); + contentsWidget->setDropIndicatorShown(true); + contentsWidget->viewport()->setAcceptDrops(true); + contentsWidget->setDragDropMode(QAbstractItemView::DropOnly); + contentsWidget->setDefaultDropAction(Qt::CopyAction); + + contentsWidget->installEventFilter(this); + contentsWidget->setModel(&m_list); + + connect(contentsWidget, &QAbstractItemView::doubleClicked, this, &SkinManageDialog::activated); + + connect(contentsWidget->selectionModel(), + &QItemSelectionModel::selectionChanged, + this, + &SkinManageDialog::selectionChanged); + connect(m_ui->listView, &QListView::customContextMenuRequested, this, &SkinManageDialog::show_context_menu); + connect(m_ui->elytraCB, + &QCheckBox::stateChanged, + this, + [this]() + { + m_skinPreview->setElytraVisible(m_ui->elytraCB->isChecked()); + on_capeCombo_currentIndexChanged(0); + }); + + setupCapes(); + + m_ui->listView->setCurrentIndex(m_list.index(m_list.getSelectedAccountSkin())); + + m_ui->buttonBox->button(QDialogButtonBox::Cancel)->setText(tr("Cancel")); + m_ui->buttonBox->button(QDialogButtonBox::Ok)->setText(tr("OK")); + + m_ui->skinLayout->insertWidget(0, QWidget::createWindowContainer(m_skinPreview, this)); +} + +SkinManageDialog::~SkinManageDialog() +{ + delete m_ui; + delete m_skinPreview; +} + +void SkinManageDialog::activated(QModelIndex index) +{ + m_selectedSkinKey = index.data(Qt::UserRole).toString(); + accept(); +} + +void SkinManageDialog::selectionChanged(QItemSelection selected, [[maybe_unused]] QItemSelection deselected) +{ + if (selected.empty()) + return; + + QString key = selected.first().indexes().first().data(Qt::UserRole).toString(); + if (key.isEmpty()) + return; + m_selectedSkinKey = key; + auto skin = getSelectedSkin(); + if (!skin) + return; + + m_skinPreview->updateScene(skin); + + QString capeId = skin->getCapeId(); + int capeIndex = 0; + if (capeId.isEmpty()) + { + capeIndex = m_capeModel->findCapeIndex("no_cape"); + } + else + { + capeIndex = m_capeModel->findCapeIndex(capeId); + } + + if (capeIndex >= 0) + { + m_ui->capeCombo->setCurrentIndex(capeIndex); + } + + m_ui->steveBtn->setChecked(skin->getModel() == SkinModel::CLASSIC); + m_ui->alexBtn->setChecked(skin->getModel() == SkinModel::SLIM); +} + +void SkinManageDialog::delayed_scroll(QModelIndex model_index) +{ + auto contentsWidget = m_ui->listView; + contentsWidget->scrollTo(model_index); +} + +void SkinManageDialog::on_openDirBtn_clicked() +{ + DesktopServices::openPath(m_list.getDir(), true); +} + +void SkinManageDialog::on_fileBtn_clicked() +{ + auto filter = QMimeDatabase().mimeTypeForName("image/png").filterString(); + QString raw_path = QFileDialog::getOpenFileName(this, tr("Select Skin Texture"), QString(), filter); + if (raw_path.isNull()) + { + return; + } + auto message = m_list.installSkin(raw_path, {}); + if (!message.isEmpty()) + { + CustomMessageBox::selectable(this, tr("Selected file is not a valid skin"), message, QMessageBox::Critical) + ->show(); + return; + } +} + +QPixmap previewCape(QImage capeImage, bool elytra = false) +{ + if (elytra) + { + auto wing = capeImage.copy(34, 0, 12, 22); + QImage mirrored = wing.flipped(Qt::Horizontal); + + QImage combined(wing.width() * 2 - 2, wing.height(), capeImage.format()); + combined.fill(Qt::transparent); + + QPainter painter(&combined); + painter.drawImage(0, 0, wing); + painter.drawImage(wing.width() - 2, 0, mirrored); + painter.end(); + return QPixmap::fromImage(combined.scaled(96, 176, Qt::IgnoreAspectRatio, Qt::FastTransformation)); + } + return QPixmap::fromImage( + capeImage.copy(1, 1, 10, 16).scaled(80, 128, Qt::IgnoreAspectRatio, Qt::FastTransformation)); +} + +void SkinManageDialog::setupCapes() +{ + // Create the cape model with on-demand loading support + m_capeModel = new CapeListModel(this); + m_ui->capeCombo->setModel(m_capeModel); + + // Connect signals for async loading + connect(m_capeModel, &CapeListModel::loadingFinished, this, &SkinManageDialog::onCapesLoaded); + connect(m_capeModel, + &CapeListModel::capeLoaded, + this, + [this](const QString& capeId) + { + // Cache image for instant preview if needed + QImage capeImage = m_capeModel->getCapeImage(capeId); + if (!capeImage.isNull()) + { + m_capes[capeId] = capeImage; + } + }); + + // Load capes from account (this triggers download if needed) + m_capeModel->loadFromAccount(m_acct, m_list.getDir()); +} + +void SkinManageDialog::onCapesLoaded() +{ + auto& accountData = *m_acct->accountData(); + auto currentCape = accountData.minecraftProfile.currentCape; + + // Populate local cache for all available capes from model + int rowCount = m_capeModel->rowCount(); + for (int i = 0; i < rowCount; i++) + { + QString id = m_capeModel->data(m_capeModel->index(i), CapeListModel::CapeIdRole).toString(); + QImage image = m_capeModel->data(m_capeModel->index(i), CapeListModel::CapeImageRole).value<QImage>(); + if (!image.isNull()) + { + m_capes[id] = image; + } + } + + // Set initial selection + int currentIndex = 0; + if (currentCape.isEmpty()) + { + currentIndex = m_capeModel->findCapeIndex("no_cape"); + } + else + { + currentIndex = m_capeModel->findCapeIndex(currentCape); + } + + if (currentIndex >= 0) + { + m_ui->capeCombo->setCurrentIndex(currentIndex); + } +} + +void SkinManageDialog::onCapeLoadError(const QString& error) +{ + qWarning() << "Failed to load capes:" << error; + // Fall back to showing at least "No Cape" option + if (m_ui->capeCombo->count() == 0) + { + m_ui->capeCombo->addItem(tr("No Cape"), QVariant()); + } +} + +void SkinManageDialog::on_capeCombo_currentIndexChanged(int index) +{ + if (index < 0 || !m_capeModel) + return; + + QModelIndex modelIdx = m_capeModel->index(index); + QString id = m_capeModel->data(modelIdx, CapeListModel::CapeIdRole).toString(); + QImage cape = m_capes.value(id, QImage()); + + if (!cape.isNull()) + { + m_ui->capeImage->setPixmap(previewCape(cape, m_ui->elytraCB->isChecked()) + .scaled(size() * (1. / 3), Qt::KeepAspectRatio, Qt::FastTransformation)); + } + else + { + m_ui->capeImage->clear(); + } + + // For preview, use empty string for 'no_cape' + QString displayId = (id == "no_cape") ? "" : id; + m_skinPreview->updateCape(cape); + + if (auto skin = getSelectedSkin(); skin) + { + skin->setCapeId(displayId); + m_skinPreview->updateScene(skin); + } +} + +void SkinManageDialog::on_steveBtn_toggled(bool checked) +{ + if (auto skin = getSelectedSkin(); skin) + { + skin->setModel(checked ? SkinModel::CLASSIC : SkinModel::SLIM); + m_skinPreview->updateScene(skin); + } +} + +void SkinManageDialog::accept() +{ + auto skin = m_list.skin(m_selectedSkinKey); + if (!skin) + { + reject(); + return; + } + auto path = skin->getPath(); + + ProgressDialog prog(this); + NetJob::Ptr skinUpload{ new NetJob(tr("Change skin"), APPLICATION->network(), 1) }; + + if (!QFile::exists(path)) + { + CustomMessageBox::selectable(this, tr("Skin Upload"), tr("Skin file does not exist!"), QMessageBox::Warning) + ->exec(); + reject(); + return; + } + + skinUpload->addNetAction(SkinUpload::make(m_acct->accessToken(), skin->getPath(), skin->getModelString())); + + auto selectedCape = skin->getCapeId(); + if (selectedCape != m_acct->accountData()->minecraftProfile.currentCape) + { + skinUpload->addNetAction(CapeChange::make(m_acct->accessToken(), selectedCape)); + } + + skinUpload->addTask(m_acct->refresh().staticCast<Task>()); + if (prog.execWithTask(*skinUpload) != QDialog::Accepted) + { + CustomMessageBox::selectable(this, tr("Skin Upload"), tr("Failed to upload skin!"), QMessageBox::Warning) + ->exec(); + reject(); + return; + } + skin->setURL(m_acct->accountData()->minecraftProfile.skin.url); + QDialog::accept(); +} + +void SkinManageDialog::on_resetBtn_clicked() +{ + ProgressDialog prog(this); + NetJob::Ptr skinReset{ new NetJob(tr("Reset skin"), APPLICATION->network(), 1) }; + skinReset->addNetAction(SkinDelete::make(m_acct->accessToken())); + skinReset->addTask(m_acct->refresh().staticCast<Task>()); + if (prog.execWithTask(*skinReset) != QDialog::Accepted) + { + CustomMessageBox::selectable(this, + tr("Skin Delete"), + tr("Failed to delete current skin!"), + QMessageBox::Warning) + ->exec(); + reject(); + return; + } + QDialog::accept(); +} + +void SkinManageDialog::show_context_menu(const QPoint& pos) +{ + QMenu myMenu(tr("Context menu"), this); + myMenu.addAction(m_ui->action_Rename_Skin); + myMenu.addAction(m_ui->action_Delete_Skin); + + myMenu.exec(m_ui->listView->mapToGlobal(pos)); +} + +bool SkinManageDialog::eventFilter(QObject* obj, QEvent* ev) +{ + if (obj == m_ui->listView) + { + if (ev->type() == QEvent::KeyPress) + { + QKeyEvent* keyEvent = static_cast<QKeyEvent*>(ev); + switch (keyEvent->key()) + { + case Qt::Key_Delete: on_action_Delete_Skin_triggered(false); return true; + case Qt::Key_F2: on_action_Rename_Skin_triggered(false); return true; + default: break; + } + } + } + return QDialog::eventFilter(obj, ev); +} + +void SkinManageDialog::on_action_Rename_Skin_triggered(bool) +{ + if (!m_selectedSkinKey.isEmpty()) + { + m_ui->listView->edit(m_ui->listView->currentIndex()); + } +} + +void SkinManageDialog::on_action_Delete_Skin_triggered(bool) +{ + if (m_selectedSkinKey.isEmpty()) + return; + + if (m_list.getSkinIndex(m_selectedSkinKey) == m_list.getSelectedAccountSkin()) + { + CustomMessageBox::selectable(this, + tr("Delete error"), + tr("Can not delete skin that is in use."), + QMessageBox::Warning) + ->exec(); + return; + } + + auto skin = m_list.skin(m_selectedSkinKey); + if (!skin) + return; + + auto response = CustomMessageBox::selectable(this, + tr("Confirm Deletion"), + tr("You are about to delete \"%1\".\n" + "Are you sure?") + .arg(skin->name()), + QMessageBox::Warning, + QMessageBox::Yes | QMessageBox::No, + QMessageBox::No) + ->exec(); + + if (response == QMessageBox::Yes) + { + if (!m_list.deleteSkin(m_selectedSkinKey, true)) + { + m_list.deleteSkin(m_selectedSkinKey, false); + } + } +} + +void SkinManageDialog::on_urlBtn_clicked() +{ + auto url = QUrl(m_ui->urlLine->text()); + if (!url.isValid()) + { + CustomMessageBox::selectable(this, tr("Invalid url"), tr("Invalid url"), QMessageBox::Critical)->show(); + return; + } + + NetJob::Ptr job{ new NetJob(tr("Download skin"), APPLICATION->network()) }; + job->setAskRetry(false); + + auto path = FS::PathCombine(m_list.getDir(), url.fileName()); + job->addNetAction(Net::Download::makeFile(url, path)); + ProgressDialog dlg(this); + dlg.execWithTask(*job); + SkinModel s(path); + if (!s.isValid()) + { + CustomMessageBox::selectable(this, + tr("URL is not a valid skin"), + QFileInfo::exists(path) + ? tr("Skin images must be 64x64 or 64x32 pixel PNG files.") + : tr("Unable to download the skin: '%1'.").arg(m_ui->urlLine->text()), + QMessageBox::Critical) + ->show(); + QFile::remove(path); + return; + } + m_ui->urlLine->setText(""); + if (QFileInfo(path).suffix().isEmpty()) + { + QFile::rename(path, path + ".png"); + } +} + +class WaitTask : public Task +{ + public: + WaitTask() : m_loop(), m_done(false) {}; + virtual ~WaitTask() = default; + + public slots: + void quit() + { + m_done = true; + m_loop.quit(); + } + + protected: + virtual void executeTask() + { + if (!m_done) + m_loop.exec(); + emitSucceeded(); + }; + + private: + QEventLoop m_loop; + bool m_done; +}; + +void SkinManageDialog::on_userBtn_clicked() +{ + auto user = m_ui->urlLine->text(); + if (user.isEmpty()) + { + return; + } + MinecraftProfile mcProfile; + auto path = FS::PathCombine(m_list.getDir(), user + ".png"); + + NetJob::Ptr job{ new NetJob(tr("Download user skin"), APPLICATION->network(), 1) }; + job->setAskRetry(false); + + auto uuidOut = std::make_shared<QByteArray>(); + auto profileOut = std::make_shared<QByteArray>(); + + auto uuidLoop = makeShared<WaitTask>(); + auto profileLoop = makeShared<WaitTask>(); + + auto getUUID = + Net::Download::makeByteArray("https://api.minecraftservices.com/minecraft/profile/lookup/name/" + user, + uuidOut); + auto getProfile = Net::Download::makeByteArray(QUrl(), profileOut); + auto downloadSkin = Net::Download::makeFile(QUrl(), path); + + QString failReason; + + connect(getUUID.get(), &Task::aborted, uuidLoop.get(), &WaitTask::quit); + connect(getUUID.get(), + &Task::failed, + this, + [&failReason](QString reason) + { + qCritical() << "Couldn't get user UUID:" << reason; + failReason = tr("failed to get user UUID"); + }); + connect(getUUID.get(), &Task::failed, uuidLoop.get(), &WaitTask::quit); + connect(getProfile.get(), &Task::aborted, profileLoop.get(), &WaitTask::quit); + connect(getProfile.get(), &Task::failed, profileLoop.get(), &WaitTask::quit); + connect(getProfile.get(), + &Task::failed, + this, + [&failReason](QString reason) + { + qCritical() << "Couldn't get user profile:" << reason; + failReason = tr("failed to get user profile"); + }); + connect(downloadSkin.get(), + &Task::failed, + this, + [&failReason](QString reason) + { + qCritical() << "Couldn't download skin:" << reason; + failReason = tr("failed to download skin"); + }); + + connect(getUUID.get(), + &Task::succeeded, + this, + [uuidLoop, uuidOut, job, getProfile, &failReason] + { + try + { + QJsonParseError parse_error{}; + QJsonDocument doc = QJsonDocument::fromJson(*uuidOut, &parse_error); + if (parse_error.error != QJsonParseError::NoError) + { + qWarning() << "Error while parsing JSON response from Minecraft skin service at " + << parse_error.offset << " reason: " << parse_error.errorString(); + failReason = tr("failed to parse get user UUID response"); + uuidLoop->quit(); + return; + } + const auto root = doc.object(); + auto id = Json::ensureString(root, "id"); + if (!id.isEmpty()) + { + getProfile->setUrl("https://sessionserver.mojang.com/session/minecraft/profile/" + id); + } + else + { + failReason = tr("user id is empty"); + job->abort(); + } + } + catch (const Exception& e) + { + qCritical() << "Couldn't load skin json:" << e.cause(); + failReason = tr("failed to parse get user UUID response"); + } + uuidLoop->quit(); + }); + + connect(getProfile.get(), + &Task::succeeded, + this, + [profileLoop, profileOut, job, getProfile, &mcProfile, downloadSkin, &failReason] + { + if (Parsers::parseMinecraftProfileMojang(*profileOut, mcProfile)) + { + downloadSkin->setUrl(mcProfile.skin.url); + } + else + { + failReason = tr("failed to parse get user profile response"); + job->abort(); + } + profileLoop->quit(); + }); + + job->addNetAction(getUUID); + job->addTask(uuidLoop); + job->addNetAction(getProfile); + job->addTask(profileLoop); + job->addNetAction(downloadSkin); + ProgressDialog dlg(this); + dlg.execWithTask(*job); + + SkinModel s(path); + if (!s.isValid()) + { + if (failReason.isEmpty()) + { + failReason = tr("the skin is invalid"); + } + CustomMessageBox::selectable(this, + tr("Username not found"), + tr("Unable to find the skin for '%1'\n because: %2.").arg(user, failReason), + QMessageBox::Critical) + ->show(); + QFile::remove(path); + return; + } + m_ui->urlLine->setText(""); + s.setModel(mcProfile.skin.variant.toUpper() == "SLIM" ? SkinModel::SLIM : SkinModel::CLASSIC); + s.setURL(mcProfile.skin.url); + if (m_capes.contains(mcProfile.currentCape)) + { + s.setCapeId(mcProfile.currentCape); + } + m_list.updateSkin(&s); +} + +void SkinManageDialog::resizeEvent(QResizeEvent* event) +{ + QWidget::resizeEvent(event); + QSize s = size() * (1. / 3); + + auto id = m_ui->capeCombo->currentData(); + auto cape = m_capes.value(id.toString(), {}); + if (!cape.isNull()) + { + m_ui->capeImage->setPixmap( + previewCape(cape, m_ui->elytraCB->isChecked()).scaled(s, Qt::KeepAspectRatio, Qt::FastTransformation)); + } + else + { + m_ui->capeImage->clear(); + } +} + +SkinModel* SkinManageDialog::getSelectedSkin() +{ + if (auto skin = m_list.skin(m_selectedSkinKey); skin && skin->isValid()) + { + return skin; + } + return nullptr; +} + +QHash<QString, QImage> SkinManageDialog::capes() +{ + return m_capes; +} diff --git a/archived/projt-launcher/launcher/ui/dialogs/skins/SkinManageDialog.h b/archived/projt-launcher/launcher/ui/dialogs/skins/SkinManageDialog.h new file mode 100644 index 0000000000..85b09133f4 --- /dev/null +++ b/archived/projt-launcher/launcher/ui/dialogs/skins/SkinManageDialog.h @@ -0,0 +1,100 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * 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, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * + * + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2023-2024 Trial97 <alexandru.tripon97@gmail.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * ======================================================================== */ + +#pragma once + +#include <QDialog> +#include <QItemSelection> +#include <QPixmap> + +#include "minecraft/auth/MinecraftAccount.hpp" +#include "minecraft/skins/CapeListModel.h" +#include "minecraft/skins/SkinList.h" +#include "minecraft/skins/SkinModel.h" +#include "ui/dialogs/skins/draw/SkinOpenGLWindow.h" + +namespace Ui +{ + class SkinManageDialog; +} +class SkinManageDialog : public QDialog, public SkinProvider +{ + Q_OBJECT + public: + explicit SkinManageDialog(QWidget* parent, MinecraftAccountPtr acct); + virtual ~SkinManageDialog(); + void resizeEvent(QResizeEvent* event) override; + + virtual SkinModel* getSelectedSkin() override; + virtual QHash<QString, QImage> capes() override; + + public slots: + void selectionChanged(QItemSelection, QItemSelection); + void activated(QModelIndex); + void delayed_scroll(QModelIndex); + void on_openDirBtn_clicked(); + void on_fileBtn_clicked(); + void on_urlBtn_clicked(); + void on_userBtn_clicked(); + void accept() override; + void on_capeCombo_currentIndexChanged(int index); + void on_steveBtn_toggled(bool checked); + void on_resetBtn_clicked(); + void show_context_menu(const QPoint& pos); + bool eventFilter(QObject* obj, QEvent* ev) override; + void on_action_Rename_Skin_triggered(bool checked); + void on_action_Delete_Skin_triggered(bool checked); + + private: + void setupCapes(); + void onCapesLoaded(); + void onCapeLoadError(const QString& error); + + private: + MinecraftAccountPtr m_acct; + Ui::SkinManageDialog* m_ui; + SkinList m_list; + QString m_selectedSkinKey; + QHash<QString, QImage> m_capes; + SkinOpenGLWindow* m_skinPreview = nullptr; + CapeListModel* m_capeModel = nullptr; +}; diff --git a/archived/projt-launcher/launcher/ui/dialogs/skins/SkinManageDialog.ui b/archived/projt-launcher/launcher/ui/dialogs/skins/SkinManageDialog.ui new file mode 100644 index 0000000000..aeb5168549 --- /dev/null +++ b/archived/projt-launcher/launcher/ui/dialogs/skins/SkinManageDialog.ui @@ -0,0 +1,223 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>SkinManageDialog</class> + <widget class="QDialog" name="SkinManageDialog"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>968</width> + <height>757</height> + </rect> + </property> + <property name="windowTitle"> + <string>Skin Upload</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <layout class="QHBoxLayout" name="mainHlLayout" stretch="3,8"> + <item> + <layout class="QVBoxLayout" name="selectedVLayout" stretch="2,1,3"> + <item> + <layout class="QVBoxLayout" name="skinLayout"/> + </item> + <item> + <widget class="QGroupBox" name="modelBox"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Preferred" vsizetype="Maximum"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="title"> + <string>Model</string> + </property> + <layout class="QHBoxLayout" name="horizontalLayout_2"> + <item> + <widget class="QRadioButton" name="steveBtn"> + <property name="text"> + <string>Classic</string> + </property> + <property name="checked"> + <bool>true</bool> + </property> + </widget> + </item> + <item> + <widget class="QRadioButton" name="alexBtn"> + <property name="text"> + <string>Slim</string> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item> + <widget class="QGroupBox" name="capeBox"> + <property name="title"> + <string>Cape</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout_4"> + <item> + <widget class="QCheckBox" name="elytraCB"> + <property name="text"> + <string>Preview Elytra</string> + </property> + </widget> + </item> + <item> + <widget class="QComboBox" name="capeCombo"/> + </item> + <item> + <widget class="QLabel" name="capeImage"> + <property name="text"> + <string/> + </property> + <property name="scaledContents"> + <bool>false</bool> + </property> + <property name="alignment"> + <set>Qt::AlignCenter</set> + </property> + </widget> + </item> + </layout> + </widget> + </item> + </layout> + </item> + <item> + <widget class="QListView" name="listView"> + <property name="contextMenuPolicy"> + <enum>Qt::CustomContextMenu</enum> + </property> + <property name="acceptDrops"> + <bool>false</bool> + </property> + <property name="modelColumn"> + <number>0</number> + </property> + </widget> + </item> + </layout> + </item> + <item> + <layout class="QHBoxLayout" name="buttonsHLayout" stretch="0,0,3,0,0,0,1"> + <item> + <widget class="QPushButton" name="openDirBtn"> + <property name="text"> + <string>Open Folder</string> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="resetBtn"> + <property name="text"> + <string>Reset Skin</string> + </property> + </widget> + </item> + <item> + <widget class="QLineEdit" name="urlLine"> + <property name="placeholderText"> + <string extracomment="URL or username"/> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="urlBtn"> + <property name="text"> + <string>Import URL</string> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="userBtn"> + <property name="text"> + <string>Import user</string> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="fileBtn"> + <property name="text"> + <string>Import File</string> + </property> + </widget> + </item> + <item> + <widget class="QDialogButtonBox" name="buttonBox"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Minimum" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="standardButtons"> + <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set> + </property> + </widget> + </item> + </layout> + </item> + </layout> + <action name="action_Delete_Skin"> + <property name="text"> + <string>&Delete Skin</string> + </property> + <property name="toolTip"> + <string>Deletes selected skin</string> + </property> + <property name="shortcut"> + <string>Del</string> + </property> + </action> + <action name="action_Rename_Skin"> + <property name="text"> + <string>&Rename Skin</string> + </property> + <property name="toolTip"> + <string>Rename selected skin</string> + </property> + <property name="shortcut"> + <string>F2</string> + </property> + </action> + </widget> + <resources/> + <connections> + <connection> + <sender>buttonBox</sender> + <signal>rejected()</signal> + <receiver>SkinManageDialog</receiver> + <slot>reject()</slot> + <hints> + <hint type="sourcelabel"> + <x>617</x> + <y>736</y> + </hint> + <hint type="destinationlabel"> + <x>483</x> + <y>378</y> + </hint> + </hints> + </connection> + <connection> + <sender>buttonBox</sender> + <signal>accepted()</signal> + <receiver>SkinManageDialog</receiver> + <slot>accept()</slot> + <hints> + <hint type="sourcelabel"> + <x>617</x> + <y>736</y> + </hint> + <hint type="destinationlabel"> + <x>483</x> + <y>378</y> + </hint> + </hints> + </connection> + </connections> +</ui> diff --git a/archived/projt-launcher/launcher/ui/dialogs/skins/draw/BoxGeometry.cpp b/archived/projt-launcher/launcher/ui/dialogs/skins/draw/BoxGeometry.cpp new file mode 100644 index 0000000000..b453d734a8 --- /dev/null +++ b/archived/projt-launcher/launcher/ui/dialogs/skins/draw/BoxGeometry.cpp @@ -0,0 +1,342 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * 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, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * + * + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2024 Trial97 <alexandru.tripon97@gmail.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * ======================================================================== */ + +#include "BoxGeometry.h" + +#include <QList> +#include <QMatrix4x4> +#include <QVector2D> +#include <QVector3D> + +struct VertexData +{ + QVector4D position; + QVector2D texCoord; + VertexData(const QVector4D& pos, const QVector2D& tex) : position(pos), texCoord(tex) + {} +}; + +// For cube we would need only 8 vertices but we have to +// duplicate vertex for each face because texture coordinate +// is different. +static const QList<QVector4D> vertices = { + // Vertex data for face 0 + QVector4D(-0.5f, -0.5f, 0.5f, 1.0f), // v0 + QVector4D(0.5f, -0.5f, 0.5f, 1.0f), // v1 + QVector4D(-0.5f, 0.5f, 0.5f, 1.0f), // v2 + QVector4D(0.5f, 0.5f, 0.5f, 1.0f), // v3 + // Vertex data for face 1 + QVector4D(0.5f, -0.5f, 0.5f, 1.0f), // v4 + QVector4D(0.5f, -0.5f, -0.5f, 1.0f), // v5 + QVector4D(0.5f, 0.5f, 0.5f, 1.0f), // v6 + QVector4D(0.5f, 0.5f, -0.5f, 1.0f), // v7 + + // Vertex data for face 2 + QVector4D(0.5f, -0.5f, -0.5f, 1.0f), // v8 + QVector4D(-0.5f, -0.5f, -0.5f, 1.0f), // v9 + QVector4D(0.5f, 0.5f, -0.5f, 1.0f), // v10 + QVector4D(-0.5f, 0.5f, -0.5f, 1.0f), // v11 + + // Vertex data for face 3 + QVector4D(-0.5f, -0.5f, -0.5f, 1.0f), // v12 + QVector4D(-0.5f, -0.5f, 0.5f, 1.0f), // v13 + QVector4D(-0.5f, 0.5f, -0.5f, 1.0f), // v14 + QVector4D(-0.5f, 0.5f, 0.5f, 1.0f), // v15 + + // Vertex data for face 4 + QVector4D(-0.5f, -0.5f, -0.5f, 1.0f), // v16 + QVector4D(0.5f, -0.5f, -0.5f, 1.0f), // v17 + QVector4D(-0.5f, -0.5f, 0.5f, 1.0f), // v18 + QVector4D(0.5f, -0.5f, 0.5f, 1.0f), // v19 + + // Vertex data for face 5 + QVector4D(-0.5f, 0.5f, 0.5f, 1.0f), // v20 + QVector4D(0.5f, 0.5f, 0.5f, 1.0f), // v21 + QVector4D(-0.5f, 0.5f, -0.5f, 1.0f), // v22 + QVector4D(0.5f, 0.5f, -0.5f, 1.0f), // v23 +}; + +// Indices for drawing cube faces using triangle strips. +// Triangle strips can be connected by duplicating indices +// between the strips. If connecting strips have opposite +// vertex order then last index of the first strip and first +// index of the second strip needs to be duplicated. If +// connecting strips have same vertex order then only last +// index of the first strip needs to be duplicated. +static const QList<GLushort> indices = { + 0, 1, 2, 3, 3, // Face 0 - triangle strip ( v0, v1, v2, v3) + 4, 4, 5, 6, 7, 7, // Face 1 - triangle strip ( v4, v5, v6, v7) + 8, 8, 9, 10, 11, 11, // Face 2 - triangle strip ( v8, v9, v10, v11) + 12, 12, 13, 14, 15, 15, // Face 3 - triangle strip (v12, v13, v14, v15) + 16, 16, 17, 18, 19, 19, // Face 4 - triangle strip (v16, v17, v18, v19) + 20, 20, 21, 22, 23 // Face 5 - triangle strip (v20, v21, v22, v23) +}; + +static const QList<VertexData> planeVertices = { + { QVector4D(-1.0f, -1.0f, -0.5f, 1.0f), QVector2D(0.0f, 0.0f) }, // Bottom-left + { QVector4D(1.0f, -1.0f, -0.5f, 1.0f), QVector2D(1.0f, 0.0f) }, // Bottom-right + { QVector4D(-1.0f, 1.0f, -0.5f, 1.0f), QVector2D(0.0f, 1.0f) }, // Top-left + { QVector4D(1.0f, 1.0f, -0.5f, 1.0f), QVector2D(1.0f, 1.0f) }, // Top-right +}; +static const QList<GLushort> planeIndices = { + 0, + 1, + 2, + 3, + 3 // Face 0 - triangle strip ( v0, v1, v2, v3) +}; + +QList<QVector4D> transformVectors(const QMatrix4x4& matrix, const QList<QVector4D>& vectors) +{ + QList<QVector4D> transformedVectors; + transformedVectors.reserve(vectors.size()); + + for (const QVector4D& vec : vectors) + { + if (!matrix.isIdentity()) + { + transformedVectors.append(matrix * vec); + } + else + { + transformedVectors.append(vec); + } + } + + return transformedVectors; +} + +// Function to calculate UV coordinates +// this is pure magic (if something is wrong with textures this is at fault) +QList<QVector2D> getCubeUVs(float u, + float v, + float width, + float height, + float depth, + float textureWidth, + float textureHeight) +{ + auto toFaceVertices = [textureHeight, textureWidth](float x1, float y1, float x2, float y2) -> QList<QVector2D> + { + return { + QVector2D(x1 / textureWidth, 1.0 - y2 / textureHeight), + QVector2D(x2 / textureWidth, 1.0 - y2 / textureHeight), + QVector2D(x2 / textureWidth, 1.0 - y1 / textureHeight), + QVector2D(x1 / textureWidth, 1.0 - y1 / textureHeight), + }; + }; + + auto top = toFaceVertices(u + depth, v, u + width + depth, v + depth); + auto bottom = toFaceVertices(u + width + depth, v, u + width * 2 + depth, v + depth); + auto left = toFaceVertices(u, v + depth, u + depth, v + depth + height); + auto front = toFaceVertices(u + depth, v + depth, u + width + depth, v + depth + height); + auto right = toFaceVertices(u + width + depth, v + depth, u + width + depth * 2, v + height + depth); + auto back = toFaceVertices(u + width + depth * 2, v + depth, u + width * 2 + depth * 2, v + height + depth); + + auto uvRight = { + right[0], + right[1], + right[3], + right[2], + }; + auto uvLeft = { + left[0], + left[1], + left[3], + left[2], + }; + auto uvTop = { + top[0], + top[1], + top[3], + top[2], + }; + auto uvBottom = { + bottom[3], + bottom[2], + bottom[0], + bottom[1], + }; + auto uvFront = { + front[0], + front[1], + front[3], + front[2], + }; + auto uvBack = { + back[0], + back[1], + back[3], + back[2], + }; + // Create a new array to hold the modified UV data + QList<QVector2D> uvData; + uvData.reserve(24); + + // Iterate over the arrays and copy the data to newUVData + for (const auto& uvArray : { uvFront, uvRight, uvBack, uvLeft, uvBottom, uvTop }) + { + uvData.append(uvArray); + } + + return uvData; +} + +namespace opengl +{ + BoxGeometry::BoxGeometry(QVector3D size, QVector3D position) + : QOpenGLFunctions(), + m_indexBuf(QOpenGLBuffer::IndexBuffer), + m_size(size), + m_position(position) + { + initializeOpenGLFunctions(); + + // Generate 2 VBOs + m_vertexBuf.create(); + m_indexBuf.create(); + } + + BoxGeometry::BoxGeometry(QVector3D size, QVector3D position, QPoint uv, QVector3D textureDim, QSize textureSize) + : BoxGeometry(size, position) + { + initGeometry(uv.x(), + uv.y(), + textureDim.x(), + textureDim.y(), + textureDim.z(), + textureSize.width(), + textureSize.height()); + } + + BoxGeometry::~BoxGeometry() + { + m_vertexBuf.destroy(); + m_indexBuf.destroy(); + } + + void BoxGeometry::draw(QOpenGLShaderProgram* program) + { + // Tell OpenGL which VBOs to use + program->setUniformValue("model_matrix", m_matrix); + m_vertexBuf.bind(); + m_indexBuf.bind(); + + // Offset for position + quintptr offset = 0; + + // Tell OpenGL programmable pipeline how to locate vertex position data + int vertexLocation = program->attributeLocation("a_position"); + program->enableAttributeArray(vertexLocation); + program->setAttributeBuffer(vertexLocation, GL_FLOAT, offset, 4, sizeof(VertexData)); + + // Offset for texture coordinate + offset += sizeof(QVector4D); + // Tell OpenGL programmable pipeline how to locate vertex texture coordinate data + int texcoordLocation = program->attributeLocation("a_texcoord"); + program->enableAttributeArray(texcoordLocation); + program->setAttributeBuffer(texcoordLocation, GL_FLOAT, offset, 2, sizeof(VertexData)); + + // Draw cube geometry using indices from VBO 1 + glDrawElements(GL_TRIANGLE_STRIP, m_indecesCount, GL_UNSIGNED_SHORT, nullptr); + } + + void BoxGeometry::initGeometry(float u, + float v, + float width, + float height, + float depth, + float textureWidth, + float textureHeight) + { + auto textureCord = getCubeUVs(u, v, width, height, depth, textureWidth, textureHeight); + + // this should not be needed to be done on each render for most of the objects + QMatrix4x4 transformation; + transformation.translate(m_position); + transformation.scale(m_size); + auto positions = transformVectors(transformation, vertices); + + QList<VertexData> verticesData; + verticesData.reserve(positions.size()); // Reserve space for efficiency + + for (int i = 0; i < positions.size(); ++i) + { + verticesData.append(VertexData(positions[i], textureCord[i])); + } + + // Transfer vertex data to VBO 0 + m_vertexBuf.bind(); + m_vertexBuf.allocate(verticesData.constData(), static_cast<int>(verticesData.size() * sizeof(VertexData))); + + // Transfer index data to VBO 1 + m_indexBuf.bind(); + m_indexBuf.allocate(indices.constData(), static_cast<int>(indices.size() * sizeof(GLushort))); + m_indecesCount = indices.size(); + } + + void BoxGeometry::rotate(float angle, const QVector3D& vector) + { + m_matrix.rotate(angle, vector); + } + + BoxGeometry* BoxGeometry::Plane() + { + auto b = new BoxGeometry(QVector3D(), QVector3D()); + + // Transfer vertex data to VBO 0 + b->m_vertexBuf.bind(); + b->m_vertexBuf.allocate(planeVertices.constData(), static_cast<int>(planeVertices.size() * sizeof(VertexData))); + + // Transfer index data to VBO 1 + b->m_indexBuf.bind(); + b->m_indexBuf.allocate(planeIndices.constData(), static_cast<int>(planeIndices.size() * sizeof(GLushort))); + b->m_indecesCount = planeIndices.size(); + + return b; + } + + void BoxGeometry::scale(const QVector3D& vector) + { + m_matrix.scale(vector); + } +} // namespace opengl diff --git a/archived/projt-launcher/launcher/ui/dialogs/skins/draw/BoxGeometry.h b/archived/projt-launcher/launcher/ui/dialogs/skins/draw/BoxGeometry.h new file mode 100644 index 0000000000..9b762ffc00 --- /dev/null +++ b/archived/projt-launcher/launcher/ui/dialogs/skins/draw/BoxGeometry.h @@ -0,0 +1,85 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * 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, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * + * + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2024 Trial97 <alexandru.tripon97@gmail.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * ======================================================================== */ + +#pragma once + +#include <QMatrix4x4> +#include <QOpenGLBuffer> +#include <QOpenGLFunctions> +#include <QOpenGLShaderProgram> +#include <QVector3D> + +namespace opengl +{ + class BoxGeometry : protected QOpenGLFunctions + { + public: + BoxGeometry(QVector3D size, QVector3D position); + BoxGeometry(QVector3D size, + QVector3D position, + QPoint uv, + QVector3D textureDim, + QSize textureSize = { 64, 64 }); + static BoxGeometry* Plane(); + virtual ~BoxGeometry(); + + void draw(QOpenGLShaderProgram* program); + + void initGeometry(float u, + float v, + float width, + float height, + float depth, + float textureWidth = 64, + float textureHeight = 64); + void rotate(float angle, const QVector3D& vector); + void scale(const QVector3D& vector); + + private: + QOpenGLBuffer m_vertexBuf; + QOpenGLBuffer m_indexBuf; + QVector3D m_size; + QVector3D m_position; + QMatrix4x4 m_matrix; + GLsizei m_indecesCount; + }; +} // namespace opengl diff --git a/archived/projt-launcher/launcher/ui/dialogs/skins/draw/Scene.cpp b/archived/projt-launcher/launcher/ui/dialogs/skins/draw/Scene.cpp new file mode 100644 index 0000000000..c95f585080 --- /dev/null +++ b/archived/projt-launcher/launcher/ui/dialogs/skins/draw/Scene.cpp @@ -0,0 +1,238 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * 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, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * + * + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2024 Trial97 <alexandru.tripon97@gmail.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * ======================================================================== */ + +#include "ui/dialogs/skins/draw/Scene.h" + +#include <QOpenGLFunctions> +#include <QOpenGLShaderProgram> +#include <QOpenGLTexture> +#include <QOpenGLWindow> + +namespace opengl +{ + Scene::Scene(const QImage& skin, bool slim, const QImage& cape) + : QOpenGLFunctions(), + m_slim(slim), + m_capeVisible(!cape.isNull()) + { + initializeOpenGLFunctions(); + m_staticComponents = { + // head + new opengl::BoxGeometry(QVector3D(8, 8, 8), QVector3D(0, 4, 0), QPoint(0, 0), QVector3D(8, 8, 8)), + new opengl::BoxGeometry(QVector3D(9, 9, 9), QVector3D(0, 4, 0), QPoint(32, 0), QVector3D(8, 8, 8)), + // body + new opengl::BoxGeometry(QVector3D(8, 12, 4), QVector3D(0, -6, 0), QPoint(16, 16), QVector3D(8, 12, 4)), + new opengl::BoxGeometry(QVector3D(8.5, 12.5, 4.5), + QVector3D(0, -6, 0), + QPoint(16, 32), + QVector3D(8, 12, 4)), + // right leg + new opengl::BoxGeometry(QVector3D(4, 12, 4), + QVector3D(-1.9f, -18, -0.1f), + QPoint(0, 16), + QVector3D(4, 12, 4)), + new opengl::BoxGeometry(QVector3D(4.5, 12.5, 4.5), + QVector3D(-1.9f, -18, -0.1f), + QPoint(0, 32), + QVector3D(4, 12, 4)), + // left leg + new opengl::BoxGeometry(QVector3D(4, 12, 4), + QVector3D(1.9f, -18, -0.1f), + QPoint(16, 48), + QVector3D(4, 12, 4)), + new opengl::BoxGeometry(QVector3D(4.5, 12.5, 4.5), + QVector3D(1.9f, -18, -0.1f), + QPoint(0, 48), + QVector3D(4, 12, 4)), + }; + m_normalArms = { + // Right Arm + new opengl::BoxGeometry(QVector3D(4, 12, 4), QVector3D(-6, -6, 0), QPoint(40, 16), QVector3D(4, 12, 4)), + new opengl::BoxGeometry(QVector3D(4.5, 12.5, 4.5), + QVector3D(-6, -6, 0), + QPoint(40, 32), + QVector3D(4, 12, 4)), + // Left Arm + new opengl::BoxGeometry(QVector3D(4, 12, 4), QVector3D(6, -6, 0), QPoint(32, 48), QVector3D(4, 12, 4)), + new opengl::BoxGeometry(QVector3D(4.5, 12.5, 4.5), + QVector3D(6, -6, 0), + QPoint(48, 48), + QVector3D(4, 12, 4)), + }; + + m_slimArms = { + // Right Arm + new opengl::BoxGeometry(QVector3D(3, 12, 4), QVector3D(-5.5, -6, 0), QPoint(40, 16), QVector3D(3, 12, 4)), + new opengl::BoxGeometry(QVector3D(3.5, 12.5, 4.5), + QVector3D(-5.5, -6, 0), + QPoint(40, 32), + QVector3D(3, 12, 4)), + // Left Arm + new opengl::BoxGeometry(QVector3D(3, 12, 4), QVector3D(5.5, -6, 0), QPoint(32, 48), QVector3D(3, 12, 4)), + new opengl::BoxGeometry(QVector3D(3.5, 12.5, 4.5), + QVector3D(5.5, -6, 0), + QPoint(48, 48), + QVector3D(3, 12, 4)), + }; + + m_cape = new opengl::BoxGeometry(QVector3D(10, 16, 1), + QVector3D(0, -8, 2.5), + QPoint(0, 0), + QVector3D(10, 16, 1), + QSize(64, 32)); + m_cape->rotate(10.8f, QVector3D(1, 0, 0)); + m_cape->rotate(180, QVector3D(0, 1, 0)); + + auto leftWing = new opengl::BoxGeometry(QVector3D(12, 22, 4), + QVector3D(0, -13, -2), + QPoint(22, 0), + QVector3D(10, 20, 2), + QSize(64, 32)); + leftWing->rotate(15, QVector3D(1, 0, 0)); + leftWing->rotate(15, QVector3D(0, 0, 1)); + leftWing->rotate(1, QVector3D(1, 0, 0)); + auto rightWing = new opengl::BoxGeometry(QVector3D(12, 22, 4), + QVector3D(0, -13, -2), + QPoint(22, 0), + QVector3D(10, 20, 2), + QSize(64, 32)); + rightWing->scale(QVector3D(-1, 1, 1)); + rightWing->rotate(15, QVector3D(1, 0, 0)); + rightWing->rotate(15, QVector3D(0, 0, 1)); + rightWing->rotate(1, QVector3D(1, 0, 0)); + m_elytra << leftWing << rightWing; + + // texture init + m_skinTexture = new QOpenGLTexture(skin.mirrored()); + m_skinTexture->setMinificationFilter(QOpenGLTexture::Nearest); + m_skinTexture->setMagnificationFilter(QOpenGLTexture::Nearest); + + m_capeTexture = new QOpenGLTexture(cape.mirrored()); + m_capeTexture->setMinificationFilter(QOpenGLTexture::Nearest); + m_capeTexture->setMagnificationFilter(QOpenGLTexture::Nearest); + } + Scene::~Scene() + { + for (auto array : { m_staticComponents, m_normalArms, m_slimArms, m_elytra }) + { + for (auto g : array) + { + delete g; + } + } + delete m_cape; + + m_skinTexture->destroy(); + delete m_skinTexture; + + m_capeTexture->destroy(); + delete m_capeTexture; + } + + void Scene::draw(QOpenGLShaderProgram* program) + { + m_skinTexture->bind(); + program->setUniformValue("texture", 0); + for (auto toDraw : { m_staticComponents, m_slim ? m_slimArms : m_normalArms }) + { + for (auto g : toDraw) + { + g->draw(program); + } + } + m_skinTexture->release(); + if (m_capeVisible) + { + m_capeTexture->bind(); + program->setUniformValue("texture", 0); + if (!m_elytraVisible) + { + m_cape->draw(program); + } + else + { + for (auto e : m_elytra) + { + e->draw(program); + } + } + m_capeTexture->release(); + } + } + + void updateTexture(QOpenGLTexture* texture, const QImage& img) + { + if (texture) + { + if (texture->isBound()) + texture->release(); + texture->destroy(); + texture->create(); + texture->setSize(img.width(), img.height()); + texture->setData(img); + texture->setMinificationFilter(QOpenGLTexture::Nearest); + texture->setMagnificationFilter(QOpenGLTexture::Nearest); + } + } + + void Scene::setSkin(const QImage& skin) + { + updateTexture(m_skinTexture, skin.mirrored()); + } + + void Scene::setMode(bool slim) + { + m_slim = slim; + } + void Scene::setCape(const QImage& cape) + { + updateTexture(m_capeTexture, cape.mirrored()); + } + void Scene::setCapeVisible(bool visible) + { + m_capeVisible = visible; + } + void Scene::setElytraVisible(bool elytraVisible) + { + m_elytraVisible = elytraVisible; + } +} // namespace opengl diff --git a/archived/projt-launcher/launcher/ui/dialogs/skins/draw/Scene.h b/archived/projt-launcher/launcher/ui/dialogs/skins/draw/Scene.h new file mode 100644 index 0000000000..4b7755c17f --- /dev/null +++ b/archived/projt-launcher/launcher/ui/dialogs/skins/draw/Scene.h @@ -0,0 +1,75 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * 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, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * + * + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2024 Trial97 <alexandru.tripon97@gmail.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * ======================================================================== */ + +#pragma once + +#include "ui/dialogs/skins/draw/BoxGeometry.h" + +#include <QOpenGLTexture> +namespace opengl +{ + class Scene : protected QOpenGLFunctions + { + public: + Scene(const QImage& skin, bool slim, const QImage& cape); + virtual ~Scene(); + + void draw(QOpenGLShaderProgram* program); + void setSkin(const QImage& skin); + void setCape(const QImage& cape); + void setMode(bool slim); + void setCapeVisible(bool visible); + void setElytraVisible(bool elytraVisible); + + private: + QList<BoxGeometry*> m_staticComponents; + QList<BoxGeometry*> m_normalArms; + QList<BoxGeometry*> m_slimArms; + BoxGeometry* m_cape = nullptr; + QList<BoxGeometry*> m_elytra; + QOpenGLTexture* m_skinTexture = nullptr; + QOpenGLTexture* m_capeTexture = nullptr; + bool m_slim = false; + bool m_capeVisible = false; + bool m_elytraVisible = false; + }; +} // namespace opengl
\ No newline at end of file diff --git a/archived/projt-launcher/launcher/ui/dialogs/skins/draw/SkinOpenGLWindow.cpp b/archived/projt-launcher/launcher/ui/dialogs/skins/draw/SkinOpenGLWindow.cpp new file mode 100644 index 0000000000..3937e80d65 --- /dev/null +++ b/archived/projt-launcher/launcher/ui/dialogs/skins/draw/SkinOpenGLWindow.cpp @@ -0,0 +1,374 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * 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, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * + * + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2024 Trial97 <alexandru.tripon97@gmail.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * ======================================================================== */ + +#include "ui/dialogs/skins/draw/SkinOpenGLWindow.h" + +#include <QMouseEvent> +#include <QOpenGLBuffer> +#include <QVector2D> +#include <QVector3D> +#include <QtMath> + +#include "minecraft/skins/SkinModel.h" +#include "rainbow.h" +#include "ui/dialogs/skins/draw/BoxGeometry.h" +#include "ui/dialogs/skins/draw/Scene.h" + +SkinOpenGLWindow::SkinOpenGLWindow(SkinProvider* parent, QColor color) + : QOpenGLWindow(), + QOpenGLFunctions(), + m_baseColor(color), + m_parent(parent) +{ + QSurfaceFormat format = QSurfaceFormat::defaultFormat(); + format.setDepthBufferSize(24); + setFormat(format); +} + +SkinOpenGLWindow::~SkinOpenGLWindow() +{ + // Make sure the context is current when deleting the texture + // and the buffers. + makeCurrent(); + // double check if resources were initialized because they are not + // initialized together with the object + if (m_scene) + { + delete m_scene; + } + if (m_background) + { + delete m_background; + } + if (m_backgroundTexture) + { + if (m_backgroundTexture->isCreated()) + { + m_backgroundTexture->destroy(); + } + delete m_backgroundTexture; + } + if (m_modelProgram) + { + if (m_modelProgram->isLinked()) + { + m_modelProgram->release(); + } + m_modelProgram->removeAllShaders(); + delete m_modelProgram; + } + if (m_backgroundProgram) + { + if (m_backgroundProgram->isLinked()) + { + m_backgroundProgram->release(); + } + m_backgroundProgram->removeAllShaders(); + delete m_backgroundProgram; + } + doneCurrent(); +} + +void SkinOpenGLWindow::mousePressEvent(QMouseEvent* e) +{ + // Save mouse press position + m_mousePosition = QVector2D(e->pos()); + m_isMousePressed = true; +} + +void SkinOpenGLWindow::mouseMoveEvent(QMouseEvent* event) +{ + // Prevents mouse sticking on Wayland compositors + if (!(event->buttons() & Qt::MouseButton::LeftButton)) + { + m_isMousePressed = false; + return; + } + + if (m_isMousePressed) + { + int dx = event->position().x() - m_mousePosition.x(); + int dy = event->position().y() - m_mousePosition.y(); + + m_yaw += dx * 0.5f; + m_pitch += dy * 0.5f; + + // Normalize yaw to keep it manageable + if (m_yaw > 360.0f) + m_yaw -= 360.0f; + else if (m_yaw < 0.0f) + m_yaw += 360.0f; + + m_mousePosition = QVector2D(event->pos()); + update(); // Trigger a repaint + } +} + +void SkinOpenGLWindow::mouseReleaseEvent([[maybe_unused]] QMouseEvent* e) +{ + m_isMousePressed = false; +} + +void SkinOpenGLWindow::initializeGL() +{ + initializeOpenGLFunctions(); + + glClearColor(0, 0, 1, 1); + + initShaders(); + + generateBackgroundTexture(32, 32, 1); + + QImage skin, cape; + bool slim = false; + if (m_parent) + { + if (auto s = m_parent->getSelectedSkin()) + { + skin = s->getTexture(); + slim = s->getModel() == SkinModel::SLIM; + cape = m_parent->capes().value(s->getCapeId(), {}); + } + } + + m_scene = new opengl::Scene(skin, slim, cape); + m_background = opengl::BoxGeometry::Plane(); + glEnable(GL_TEXTURE_2D); +} + +void SkinOpenGLWindow::initShaders() +{ + // Skin model shaders + m_modelProgram = new QOpenGLShaderProgram(this); + // Compile vertex shader + if (!m_modelProgram->addCacheableShaderFromSourceFile(QOpenGLShader::Vertex, ":/shaders/vshader_skin_model.glsl")) + close(); + + // Compile fragment shader + if (!m_modelProgram->addCacheableShaderFromSourceFile(QOpenGLShader::Fragment, ":/shaders/fshader.glsl")) + close(); + + // Link shader pipeline + if (!m_modelProgram->link()) + close(); + + // Bind shader pipeline for use + if (!m_modelProgram->bind()) + close(); + + // Background shaders + m_backgroundProgram = new QOpenGLShaderProgram(this); + // Compile vertex shader + if (!m_backgroundProgram->addCacheableShaderFromSourceFile(QOpenGLShader::Vertex, + ":/shaders/vshader_skin_background.glsl")) + close(); + + // Compile fragment shader + if (!m_backgroundProgram->addCacheableShaderFromSourceFile(QOpenGLShader::Fragment, ":/shaders/fshader.glsl")) + close(); + + // Link shader pipeline + if (!m_backgroundProgram->link()) + close(); + + // Bind shader pipeline for use (verification) + if (!m_backgroundProgram->bind()) + close(); +} + +void SkinOpenGLWindow::resizeGL(int w, int h) +{ + // Calculate aspect ratio + qreal aspect = qreal(w) / qreal(h ? h : 1); + + const qreal zNear = 15., fov = 45; + + // Reset projection + m_projection.setToIdentity(); + + // Build the reverse z perspective projection matrix + double radians = qDegreesToRadians(fov / 2.); + double sine = std::sin(radians); + if (sine == 0) + return; + double cotan = std::cos(radians) / sine; + + m_projection(0, 0) = cotan / aspect; + m_projection(1, 1) = cotan; + m_projection(2, 2) = 0.; + m_projection(3, 2) = -1.; + m_projection(2, 3) = zNear; + m_projection(3, 3) = 0.; +} + +void SkinOpenGLWindow::paintGL() +{ + // Adjust the viewport to account for fractional scaling + qreal dpr = devicePixelRatio(); + if (dpr != 1.f) + { + QSize scaledSize = size() * dpr; + glViewport(0, 0, scaledSize.width(), scaledSize.height()); + } + + // Clear color and depth buffer + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + + // Enable depth buffer + glEnable(GL_DEPTH_TEST); + glDepthFunc(GL_LESS); + + glEnable(GL_BLEND); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + + m_backgroundProgram->bind(); + renderBackground(); + m_backgroundProgram->release(); + + // Calculate model view transformation + QMatrix4x4 matrix; + float yawRad = qDegreesToRadians(m_yaw); + float pitchRad = qDegreesToRadians(m_pitch); + matrix.lookAt(QVector3D( // + m_distance * qCos(pitchRad) * qCos(yawRad), // + m_distance * qSin(pitchRad) - 8, // + m_distance * qCos(pitchRad) * qSin(yawRad)), + QVector3D(0, -8, 0), + QVector3D(0, 1, 0)); + + // Set modelview-projection matrix + m_modelProgram->bind(); + m_modelProgram->setUniformValue("mvp_matrix", m_projection * matrix); + + m_scene->draw(m_modelProgram); + m_modelProgram->release(); + + // Redraw the first frame; this is necessary because the pixel ratio for Wayland fractional scaling is not + // negotiated properly on the first frame + if (m_isFirstFrame) + { + m_isFirstFrame = false; + update(); + } +} + +void SkinOpenGLWindow::updateScene(SkinModel* skin) +{ + if (skin && m_scene) + { + m_scene->setMode(skin->getModel() == SkinModel::SLIM); + m_scene->setSkin(skin->getTexture()); + update(); + } +} +void SkinOpenGLWindow::updateCape(const QImage& cape) +{ + if (m_scene) + { + m_scene->setCapeVisible(!cape.isNull()); + m_scene->setCape(cape); + update(); + } +} + +QColor calculateContrastingColor(const QColor& color) +{ + constexpr float contrast = 0.2f; + auto luma = Rainbow::luma(color); + if (luma < 0.5) + { + return Rainbow::lighten(color, contrast); + } + else + { + return Rainbow::darken(color, contrast); + } +} + +QImage generateChessboardImage(int width, int height, int tileSize, QColor baseColor) +{ + QImage image(width, height, QImage::Format_RGB888); + auto white = baseColor; + auto black = calculateContrastingColor(baseColor); + for (int y = 0; y < height; ++y) + { + for (int x = 0; x < width; ++x) + { + bool isWhite = ((x / tileSize) % 2) == ((y / tileSize) % 2); + image.setPixelColor(x, y, isWhite ? white : black); + } + } + return image; +} + +void SkinOpenGLWindow::generateBackgroundTexture(int width, int height, int tileSize) +{ + m_backgroundTexture = new QOpenGLTexture(generateChessboardImage(width, height, tileSize, m_baseColor)); + m_backgroundTexture->setMinificationFilter(QOpenGLTexture::Nearest); + m_backgroundTexture->setMagnificationFilter(QOpenGLTexture::Nearest); +} + +void SkinOpenGLWindow::renderBackground() +{ + glDisable(GL_DEPTH_TEST); + glDepthMask(GL_FALSE); // Disable depth buffer writing + m_backgroundTexture->bind(); + m_backgroundProgram->setUniformValue("texture", 0); + m_background->draw(m_backgroundProgram); + m_backgroundTexture->release(); + glDepthMask(GL_TRUE); // Re-enable depth buffer writing + glEnable(GL_DEPTH_TEST); +} + +void SkinOpenGLWindow::wheelEvent(QWheelEvent* event) +{ + // Adjust distance based on scroll + int delta = event->angleDelta().y(); // Positive for scroll up, negative for scroll down + m_distance -= delta * 0.01f; // Adjust sensitivity factor + m_distance = qMax(16.f, m_distance); // Clamp distance + update(); // Trigger a repaint +} +void SkinOpenGLWindow::setElytraVisible(bool visible) +{ + if (m_scene) + m_scene->setElytraVisible(visible); +} diff --git a/archived/projt-launcher/launcher/ui/dialogs/skins/draw/SkinOpenGLWindow.h b/archived/projt-launcher/launcher/ui/dialogs/skins/draw/SkinOpenGLWindow.h new file mode 100644 index 0000000000..77a7cce731 --- /dev/null +++ b/archived/projt-launcher/launcher/ui/dialogs/skins/draw/SkinOpenGLWindow.h @@ -0,0 +1,109 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * 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, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * + * + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2024 Trial97 <alexandru.tripon97@gmail.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * ======================================================================== */ + +#pragma once + +#include <QMatrix4x4> +#include <QOpenGLFunctions> +#include <QOpenGLShaderProgram> +#include <QOpenGLTexture> +#include <QOpenGLWindow> +#include <QVector2D> +#include "minecraft/skins/SkinModel.h" +#include "ui/dialogs/skins/draw/BoxGeometry.h" +#include "ui/dialogs/skins/draw/Scene.h" + +class SkinProvider +{ + public: + virtual ~SkinProvider() = default; + virtual SkinModel* getSelectedSkin() = 0; + virtual QHash<QString, QImage> capes() = 0; +}; +class SkinOpenGLWindow : public QOpenGLWindow, protected QOpenGLFunctions +{ + Q_OBJECT + + public: + SkinOpenGLWindow(SkinProvider* parent, QColor color); + virtual ~SkinOpenGLWindow(); + + void updateScene(SkinModel* skin); + void updateCape(const QImage& cape); + void setElytraVisible(bool visible); + + protected: + void mousePressEvent(QMouseEvent* e) override; + void mouseReleaseEvent(QMouseEvent* e) override; + void mouseMoveEvent(QMouseEvent* event) override; + void wheelEvent(QWheelEvent* event) override; + + void initializeGL() override; + void resizeGL(int w, int h) override; + void paintGL() override; + + void initShaders(); + + void generateBackgroundTexture(int width, int height, int tileSize); + void renderBackground(); + + private: + QOpenGLShaderProgram* m_modelProgram; + QOpenGLShaderProgram* m_backgroundProgram; + opengl::Scene* m_scene = nullptr; + + QMatrix4x4 m_projection; + + QVector2D m_mousePosition; + + bool m_isMousePressed = false; + float m_distance = 48; + float m_yaw = 90; // Horizontal rotation angle + float m_pitch = 0; // Vertical rotation angle + + bool m_isFirstFrame = true; + + opengl::BoxGeometry* m_background = nullptr; + QOpenGLTexture* m_backgroundTexture = nullptr; + QColor m_baseColor; + SkinProvider* m_parent = nullptr; +}; |
