/* SPDX-FileCopyrightText: 2026 Project Tick
* SPDX-FileContributor: Project Tick
* SPDX-License-Identifier: GPL-3.0-or-later
*
* MeshMC - A Custom Launcher for Minecraft
* 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, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
#include "JavaDownloadDialog.h"
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include "Application.h"
#include "BuildConfig.h"
#include "FileSystem.h"
#include "Json.h"
#include "java/JavaUtils.h"
#include "net/Download.h"
JavaDownloadDialog::JavaDownloadDialog(QWidget* parent) : QDialog(parent)
{
setWindowTitle(tr("Download Java"));
setMinimumSize(700, 450);
resize(800, 500);
m_providers = JavaDownload::JavaProviderInfo::availableProviders();
setupUi();
}
void JavaDownloadDialog::setupUi()
{
auto* mainLayout = new QVBoxLayout(this);
// --- Three-column selection area ---
auto* columnsLayout = new QHBoxLayout();
// Left: Provider list
auto* providerGroup = new QGroupBox(tr("Provider"), this);
auto* providerLayout = new QVBoxLayout(providerGroup);
m_providerList = new QListWidget(this);
m_providerList->setIconSize(QSize(24, 24));
for (const auto& provider : m_providers) {
auto* item = new QListWidgetItem(
APPLICATION->getThemedIcon(provider.iconName), provider.name);
m_providerList->addItem(item);
}
providerLayout->addWidget(m_providerList);
columnsLayout->addWidget(providerGroup, 1);
// Center: Major version list
auto* versionGroup = new QGroupBox(tr("Version"), this);
auto* versionLayout = new QVBoxLayout(versionGroup);
m_versionList = new QListWidget(this);
versionLayout->addWidget(m_versionList);
columnsLayout->addWidget(versionGroup, 1);
// Right: Sub-version / build list
auto* subVersionGroup = new QGroupBox(tr("Build"), this);
auto* subVersionLayout = new QVBoxLayout(subVersionGroup);
m_subVersionList = new QListWidget(this);
subVersionLayout->addWidget(m_subVersionList);
columnsLayout->addWidget(subVersionGroup, 1);
mainLayout->addLayout(columnsLayout, 1);
// Info label
m_infoLabel = new QLabel(this);
m_infoLabel->setWordWrap(true);
m_infoLabel->setText(
tr("Select a Java provider, version, and build to download."));
mainLayout->addWidget(m_infoLabel);
// Progress bar
m_progressBar = new QProgressBar(this);
m_progressBar->setVisible(false);
m_progressBar->setRange(0, 100);
mainLayout->addWidget(m_progressBar);
// Status label
m_statusLabel = new QLabel(this);
m_statusLabel->setVisible(false);
mainLayout->addWidget(m_statusLabel);
// Buttons
auto* buttonLayout = new QHBoxLayout();
buttonLayout->addStretch();
m_downloadBtn = new QPushButton(tr("Download"), this);
m_downloadBtn->setEnabled(false);
buttonLayout->addWidget(m_downloadBtn);
m_cancelBtn = new QPushButton(tr("Cancel"), this);
buttonLayout->addWidget(m_cancelBtn);
mainLayout->addLayout(buttonLayout);
// Connections
connect(m_providerList, &QListWidget::currentRowChanged, this,
&JavaDownloadDialog::providerChanged);
connect(m_versionList, &QListWidget::currentRowChanged, this,
&JavaDownloadDialog::majorVersionChanged);
connect(m_subVersionList, &QListWidget::currentRowChanged, this,
&JavaDownloadDialog::subVersionChanged);
connect(m_downloadBtn, &QPushButton::clicked, this,
&JavaDownloadDialog::onDownloadClicked);
connect(m_cancelBtn, &QPushButton::clicked, this,
&JavaDownloadDialog::onCancelClicked);
// Select first provider
if (m_providerList->count() > 0) {
m_providerList->setCurrentRow(0);
}
}
void JavaDownloadDialog::providerChanged(int index)
{
if (index < 0 || index >= m_providers.size())
return;
m_versionList->clear();
m_subVersionList->clear();
m_downloadBtn->setEnabled(false);
m_versions.clear();
m_runtimes.clear();
const auto& provider = m_providers[index];
m_infoLabel->setText(tr("Loading versions for %1...").arg(provider.name));
fetchVersionList(provider.uid);
}
void JavaDownloadDialog::majorVersionChanged(int index)
{
if (index < 0 || index >= m_versions.size())
return;
m_subVersionList->clear();
m_downloadBtn->setEnabled(false);
m_runtimes.clear();
const auto& version = m_versions[index];
m_infoLabel->setText(tr("Loading builds for %1...").arg(version.versionId));
fetchRuntimes(version.uid, version.versionId);
}
void JavaDownloadDialog::subVersionChanged(int index)
{
if (index < 0 || index >= m_runtimes.size()) {
m_downloadBtn->setEnabled(false);
return;
}
const auto& rt = m_runtimes[index];
m_infoLabel->setText(tr("Ready to download: %1\n"
"Version: %2\n"
"Platform: %3\n"
"Checksum: %4")
.arg(rt.name, rt.version.toString(), rt.runtimeOS,
rt.checksumHash.isEmpty()
? tr("None")
: tr("Yes (%1)").arg(rt.checksumType)));
m_downloadBtn->setEnabled(true);
}
void JavaDownloadDialog::fetchVersionList(const QString& uid)
{
m_fetchJob.reset();
m_fetchData.clear();
QString url = QString("%1%2/index.json").arg(BuildConfig.META_URL, uid);
m_fetchJob = new NetJob(tr("Fetch Java versions"), APPLICATION->network());
auto dl = Net::Download::makeByteArray(QUrl(url), &m_fetchData);
m_fetchJob->addNetAction(dl);
connect(m_fetchJob.get(), &NetJob::succeeded, this, [this, uid]() {
m_fetchJob.reset();
QJsonDocument doc;
try {
doc = Json::requireDocument(m_fetchData);
} catch (const Exception& e) {
m_infoLabel->setText(
tr("Failed to parse version list: %1").arg(e.cause()));
return;
}
if (!doc.isObject()) {
m_infoLabel->setText(tr("Failed to parse version list."));
return;
}
m_versions = JavaDownload::parseVersionIndex(doc.object(), uid);
m_versionList->clear();
for (const auto& ver : m_versions) {
QString displayName = ver.versionId;
if (displayName.startsWith("java")) {
displayName = "Java " + displayName.mid(4);
}
m_versionList->addItem(displayName);
}
if (m_versions.size() > 0) {
m_infoLabel->setText(tr("Select a version."));
} else {
m_infoLabel->setText(
tr("No versions available for this provider."));
}
});
connect(m_fetchJob.get(), &NetJob::failed, this, [this](QString reason) {
m_fetchJob.reset();
m_infoLabel->setText(tr("Failed to load versions: %1").arg(reason));
});
m_fetchJob->start();
}
void JavaDownloadDialog::fetchRuntimes(const QString& uid,
const QString& versionId)
{
m_fetchJob.reset();
m_fetchData.clear();
QString url =
QString("%1%2/%3.json").arg(BuildConfig.META_URL, uid, versionId);
m_fetchJob =
new NetJob(tr("Fetch Java runtime details"), APPLICATION->network());
auto dl = Net::Download::makeByteArray(QUrl(url), &m_fetchData);
m_fetchJob->addNetAction(dl);
connect(m_fetchJob.get(), &NetJob::succeeded, this, [this]() {
m_fetchJob.reset();
QJsonDocument doc;
try {
doc = Json::requireDocument(m_fetchData);
} catch (const Exception& e) {
m_infoLabel->setText(
tr("Failed to parse runtime details: %1").arg(e.cause()));
return;
}
if (!doc.isObject()) {
m_infoLabel->setText(tr("Failed to parse runtime details."));
return;
}
auto allRuntimes = JavaDownload::parseRuntimes(doc.object());
QString myOS = JavaDownload::currentRuntimeOS();
m_runtimes.clear();
m_subVersionList->clear();
for (const auto& rt : allRuntimes) {
if (rt.runtimeOS == myOS) {
m_runtimes.append(rt);
m_subVersionList->addItem(rt.version.toString());
}
}
if (m_runtimes.isEmpty()) {
m_infoLabel->setText(
tr("No builds available for your platform (%1).").arg(myOS));
m_downloadBtn->setEnabled(false);
} else {
m_infoLabel->setText(tr("Select a build to download."));
m_subVersionList->setCurrentRow(0);
}
});
connect(m_fetchJob.get(), &NetJob::failed, this, [this](QString reason) {
m_fetchJob.reset();
m_infoLabel->setText(
tr("Failed to load runtime details: %1").arg(reason));
});
m_fetchJob->start();
}
QString JavaDownloadDialog::javaInstallDir() const
{
return JavaUtils::managedJavaRoot();
}
void JavaDownloadDialog::onDownloadClicked()
{
int idx = m_subVersionList->currentRow();
if (idx < 0 || idx >= m_runtimes.size())
return;
const auto& runtime = m_runtimes[idx];
// Build target directory path: {dataPath}/java/{vendor}/{name}-{version}/
QString dirName =
QString("%1-%2").arg(runtime.name, runtime.version.toString());
QString targetDir =
FS::PathCombine(javaInstallDir(), runtime.vendor, dirName);
// Check if already installed
if (QDir(targetDir).exists()) {
auto result = QMessageBox::question(
this, tr("Already Installed"),
tr("This Java version appears to be already installed at:\n%1\n\n"
"Do you want to reinstall it?")
.arg(targetDir),
QMessageBox::Yes | QMessageBox::No);
if (result != QMessageBox::Yes) {
return;
}
// Remove existing installation
QDir(targetDir).removeRecursively();
}
m_downloadBtn->setEnabled(false);
m_providerList->setEnabled(false);
m_versionList->setEnabled(false);
m_subVersionList->setEnabled(false);
m_progressBar->setVisible(true);
m_progressBar->setValue(0);
m_statusLabel->setVisible(true);
m_downloadTask =
std::make_unique(runtime, targetDir, this);
connect(m_downloadTask.get(), &Task::progress, this,
[this](qint64 current, qint64 total) {
if (total > 0) {
m_progressBar->setValue(
static_cast(current * 100 / total));
}
});
connect(m_downloadTask.get(), &Task::status, this,
[this](const QString& status) { m_statusLabel->setText(status); });
connect(m_downloadTask.get(), &Task::succeeded, this, [this]() {
m_installedJavaPath = m_downloadTask->installedJavaPath();
m_progressBar->setValue(100);
m_statusLabel->setText(tr("Java installed successfully!"));
QMessageBox::information(
this, tr("Download Complete"),
tr("Java has been downloaded and installed successfully.\n\n"
"Java binary: %1")
.arg(m_installedJavaPath));
accept();
});
connect(m_downloadTask.get(), &Task::failed, this,
[this](const QString& reason) {
m_progressBar->setVisible(false);
m_statusLabel->setText(tr("Download failed: %1").arg(reason));
m_downloadBtn->setEnabled(true);
m_providerList->setEnabled(true);
m_versionList->setEnabled(true);
m_subVersionList->setEnabled(true);
QMessageBox::warning(
this, tr("Download Failed"),
tr("Failed to download Java:\n%1").arg(reason));
});
m_downloadTask->start();
}
void JavaDownloadDialog::onCancelClicked()
{
if (m_fetchJob) {
m_fetchJob->abort();
m_fetchJob.reset();
}
if (m_downloadTask) {
m_downloadTask->abort();
m_downloadTask.reset();
}
reject();
}