Merge branch 'PrismLauncher:develop' into data-packs

This commit is contained in:
TheKodeToad
2025-06-01 07:54:16 +00:00
committed by GitHub
125 changed files with 3925 additions and 1596 deletions

View File

@@ -0,0 +1,223 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* 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, see <https://www.gnu.org/licenses/>.
*
* This file incorporates work covered by the following copyright and
* permission notice:
*
* Copyright 2013-2021 MultiMC Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include <QLayout>
#include <QPushButton>
#include "Application.h"
#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.h"
#include "minecraft/MinecraftInstance.h"
#include "minecraft/ShortcutUtils.h"
#include "minecraft/WorldList.h"
#include "minecraft/auth/AccountList.h"
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(SaveTarget::Desktop));
if (!applicationDir.isEmpty())
ui->saveTargetSelectionBox->addItem(tr("Applications"), QVariant::fromValue(SaveTarget::Applications));
}
ui->saveTargetSelectionBox->addItem(tr("Other..."), QVariant::fromValue(SaveTarget::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() ? APPLICATION->getThemedIcon("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<SaveTarget>();
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 };
if (target == SaveTarget::Desktop)
ShortcutUtils::createInstanceShortcutOnDesktop(args);
else if (target == SaveTarget::Applications)
ShortcutUtils::createInstanceShortcutInApplications(args);
else
ShortcutUtils::createInstanceShortcutInOther(args);
}

View File

@@ -0,0 +1,62 @@
/* 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;
// Index representations
enum class SaveTarget { Desktop, Applications, Other };
// Functions
void stateChanged();
};

View File

@@ -0,0 +1,250 @@
<?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="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>

View File

@@ -17,7 +17,7 @@
*/
#include "ExportPackDialog.h"
#include "minecraft/mod/ModFolderModel.h"
#include "minecraft/mod/ResourceFolderModel.h"
#include "modplatform/ModIndex.h"
#include "modplatform/flame/FlamePackExportTask.h"
#include "ui/dialogs/CustomMessageBox.h"
@@ -33,7 +33,7 @@
#include "MMCZip.h"
#include "modplatform/modrinth/ModrinthPackExportTask.h"
ExportPackDialog::ExportPackDialog(InstancePtr instance, QWidget* parent, ModPlatform::ResourceProvider provider)
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);
@@ -44,12 +44,16 @@ ExportPackDialog::ExportPackDialog(InstancePtr instance, QWidget* parent, ModPla
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"));
@@ -57,6 +61,19 @@ ExportPackDialog::ExportPackDialog(InstancePtr instance, QWidget* parent, ModPla
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());
}
@@ -120,9 +137,15 @@ void ExportPackDialog::done(int result)
if (m_provider == ModPlatform::ResourceProvider::MODRINTH)
settings->set("ExportSummary", m_ui->summary->toPlainText());
else
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);
@@ -149,8 +172,18 @@ void ExportPackDialog::done(int result)
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 {
task = new FlamePackExportTask(name, m_ui->version->text(), m_ui->author->text(), m_ui->optionalFiles->isChecked(), m_instance,
output, std::bind(&FileIgnoreProxy::filterFile, m_proxy, std::placeholders::_1));
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,

View File

@@ -22,6 +22,7 @@
#include "BaseInstance.h"
#include "FastFileIconProvider.h"
#include "FileIgnoreProxy.h"
#include "minecraft/MinecraftInstance.h"
#include "modplatform/ModIndex.h"
namespace Ui {
@@ -32,7 +33,7 @@ class ExportPackDialog : public QDialog {
Q_OBJECT
public:
explicit ExportPackDialog(InstancePtr instance,
explicit ExportPackDialog(MinecraftInstancePtr instance,
QWidget* parent = nullptr,
ModPlatform::ResourceProvider provider = ModPlatform::ResourceProvider::MODRINTH);
~ExportPackDialog();
@@ -44,7 +45,7 @@ class ExportPackDialog : public QDialog {
QString ignoreFileName();
private:
const InstancePtr m_instance;
const MinecraftInstancePtr m_instance;
Ui::ExportPackDialog* m_ui;
FileIgnoreProxy* m_proxy;
FastFileIconProvider m_icons;

View File

@@ -19,36 +19,56 @@
<property name="title">
<string>&amp;Description</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_3">
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QLabel" name="nameLabel">
<property name="text">
<string>&amp;Name</string>
<layout class="QFormLayout" name="formLayout">
<property name="labelAlignment">
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter</set>
</property>
<property name="buddy">
<cstring>name</cstring>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="name"/>
</item>
<item>
<widget class="QLabel" name="versionLabel">
<property name="text">
<string>&amp;Version</string>
</property>
<property name="buddy">
<cstring>version</cstring>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="version">
<property name="text">
<string>1.0.0</string>
</property>
</widget>
<item row="0" column="0">
<widget class="QLabel" name="nameLabel">
<property name="text">
<string>&amp;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>&amp;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>&amp;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">
@@ -62,24 +82,29 @@
</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>
<item>
<widget class="QLabel" name="authorLabel">
<property name="text">
<string>&amp;Author</string>
</property>
<property name="buddy">
<cstring>author</cstring>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="author"/>
</item>
</layout>
</widget>
</item>
@@ -88,7 +113,70 @@
<property name="title">
<string>&amp;Options</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<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>&amp;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">
@@ -138,10 +226,6 @@
</layout>
</widget>
<tabstops>
<tabstop>name</tabstop>
<tabstop>version</tabstop>
<tabstop>summary</tabstop>
<tabstop>author</tabstop>
<tabstop>files</tabstop>
<tabstop>optionalFiles</tabstop>
</tabstops>

View File

@@ -36,7 +36,6 @@
#include "MSALoginDialog.h"
#include "Application.h"
#include "qr.h"
#include "ui_MSALoginDialog.h"
#include "DesktopServices.h"
@@ -44,10 +43,15 @@
#include <QApplication>
#include <QClipboard>
#include <QColor>
#include <QPainter>
#include <QPixmap>
#include <QSize>
#include <QUrl>
#include <QtWidgets/QPushButton>
#include "qrcodegen.hpp"
MSALoginDialog::MSALoginDialog(QWidget* parent) : QDialog(parent), ui(new Ui::MSALoginDialog)
{
ui->setupUi(this);
@@ -139,6 +143,33 @@ void MSALoginDialog::authorizeWithBrowser(const QUrl& url)
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)
{
// NOTE: At this point you will use the API to get the encoding and format you want, instead of my hardcoded stuff:
qrcodegen::QrCode qr = qrcodegen::QrCode::encodeText(data.toUtf8().constData(), qrcodegen::QrCode::Ecc::LOW);
const int s = qr.getSize() > 0 ? qr.getSize() : 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);
// NOTE: For performance reasons my implementation only draws the foreground parts in supplied color.
// It expects background to be prepared already (in white or whatever is preferred).
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.getModule(x, y); // 0 for white, 1 for black
if (0 != color) {
const double rx1 = (x + 1) * scale, ry1 = (y + 1) * scale;
QRectF r(rx1, ry1, scale, scale);
painter.drawRects(&r, 1);
}
}
}
}
void MSALoginDialog::authorizeWithBrowserWithExtra(QString url, QString code, [[maybe_unused]] int expiresIn)
{
ui->stackedWidget->setCurrentIndex(1);

View File

@@ -92,6 +92,10 @@ SkinManageDialog::SkinManageDialog(QWidget* parent, MinecraftAccountPtr acct)
connect(contentsWidget->selectionModel(), SIGNAL(selectionChanged(QItemSelection, QItemSelection)),
SLOT(selectionChanged(QItemSelection, QItemSelection)));
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();
@@ -159,10 +163,24 @@ void SkinManageDialog::on_fileBtn_clicked()
}
}
QPixmap previewCape(QImage capeImage)
QPixmap previewCape(QImage capeImage, bool elytra = false)
{
if (elytra) {
auto wing = capeImage.copy(34, 0, 12, 22);
QImage mirrored = wing.mirrored(true, false);
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()
{
// FIXME: add a model for this, download/refresh the capes on demand
@@ -208,7 +226,7 @@ void SkinManageDialog::setupCapes()
}
}
if (!capeImage.isNull()) {
m_ui->capeCombo->addItem(previewCape(capeImage), cape.alias, cape.id);
m_ui->capeCombo->addItem(previewCape(capeImage, m_ui->elytraCB->isChecked()), cape.alias, cape.id);
} else {
m_ui->capeCombo->addItem(cape.alias, cape.id);
}
@@ -222,7 +240,8 @@ void SkinManageDialog::on_capeCombo_currentIndexChanged(int index)
auto id = m_ui->capeCombo->currentData();
auto cape = m_capes.value(id.toString(), {});
if (!cape.isNull()) {
m_ui->capeImage->setPixmap(previewCape(cape).scaled(size() * (1. / 3), Qt::KeepAspectRatio, Qt::FastTransformation));
m_ui->capeImage->setPixmap(
previewCape(cape, m_ui->elytraCB->isChecked()).scaled(size() * (1. / 3), Qt::KeepAspectRatio, Qt::FastTransformation));
} else {
m_ui->capeImage->clear();
}
@@ -319,14 +338,14 @@ bool SkinManageDialog::eventFilter(QObject* obj, QEvent* ev)
return QDialog::eventFilter(obj, ev);
}
void SkinManageDialog::on_action_Rename_Skin_triggered(bool checked)
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 checked)
void SkinManageDialog::on_action_Delete_Skin_triggered(bool)
{
if (m_selectedSkinKey.isEmpty())
return;
@@ -523,7 +542,7 @@ void SkinManageDialog::resizeEvent(QResizeEvent* event)
auto id = m_ui->capeCombo->currentData();
auto cape = m_capes.value(id.toString(), {});
if (!cape.isNull()) {
m_ui->capeImage->setPixmap(previewCape(cape).scaled(s, Qt::KeepAspectRatio, Qt::FastTransformation));
m_ui->capeImage->setPixmap(previewCape(cape, m_ui->elytraCB->isChecked()).scaled(s, Qt::KeepAspectRatio, Qt::FastTransformation));
} else {
m_ui->capeImage->clear();
}

View File

@@ -59,6 +59,13 @@
<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>

View File

@@ -180,7 +180,8 @@ QList<QVector2D> getCubeUVs(float u, float v, float width, float height, float d
}
namespace opengl {
BoxGeometry::BoxGeometry(QVector3D size, QVector3D position) : m_indexBuf(QOpenGLBuffer::IndexBuffer), m_size(size), m_position(position)
BoxGeometry::BoxGeometry(QVector3D size, QVector3D position)
: QOpenGLFunctions(), m_indexBuf(QOpenGLBuffer::IndexBuffer), m_size(size), m_position(position)
{
initializeOpenGLFunctions();
@@ -274,4 +275,9 @@ BoxGeometry* BoxGeometry::Plane()
return b;
}
void BoxGeometry::scale(const QVector3D& vector)
{
m_matrix.scale(vector);
}
} // namespace opengl

View File

@@ -36,6 +36,7 @@ class BoxGeometry : protected QOpenGLFunctions {
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;

View File

@@ -18,9 +18,16 @@
*/
#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) : m_slim(slim), m_capeVisible(!cape.isNull())
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)),
@@ -57,6 +64,19 @@ Scene::Scene(const QImage& skin, bool slim, const QImage& cape) : m_slim(slim),
m_cape->rotate(10.8, 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);
@@ -68,7 +88,7 @@ Scene::Scene(const QImage& skin, bool slim, const QImage& cape) : m_slim(slim),
}
Scene::~Scene()
{
for (auto array : { m_staticComponents, m_normalArms, m_slimArms }) {
for (auto array : { m_staticComponents, m_normalArms, m_slimArms, m_elytra }) {
for (auto g : array) {
delete g;
}
@@ -95,7 +115,15 @@ void Scene::draw(QOpenGLShaderProgram* program)
if (m_capeVisible) {
m_capeTexture->bind();
program->setUniformValue("texture", 0);
m_cape->draw(program);
if (!m_elytraVisible) {
m_cape->draw(program);
} else {
glDisable(GL_CULL_FACE);
for (auto e : m_elytra) {
e->draw(program);
}
glEnable(GL_CULL_FACE);
}
m_capeTexture->release();
}
}
@@ -131,4 +159,8 @@ void Scene::setCapeVisible(bool visible)
{
m_capeVisible = visible;
}
void Scene::setElytraVisible(bool elytraVisible)
{
m_elytraVisible = elytraVisible;
}
} // namespace opengl

View File

@@ -22,7 +22,7 @@
#include <QOpenGLTexture>
namespace opengl {
class Scene {
class Scene : protected QOpenGLFunctions {
public:
Scene(const QImage& skin, bool slim, const QImage& cape);
virtual ~Scene();
@@ -32,15 +32,18 @@ class Scene {
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

View File

@@ -263,3 +263,8 @@ void SkinOpenGLWindow::wheelEvent(QWheelEvent* event)
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);
}

View File

@@ -43,6 +43,7 @@ class SkinOpenGLWindow : public QOpenGLWindow, protected QOpenGLFunctions {
void updateScene(SkinModel* skin);
void updateCape(const QImage& cape);
void setElytraVisible(bool visible);
protected:
void mousePressEvent(QMouseEvent* e) override;