Check available and required size when installing a package (#17829)

Show and check available and required size when installing a package.
Installation is not allowed, by disabling `Yes` and/or `Install` button,
if not enough disk space is detected.
On multi installation dialog box, installation size for each package is
reported in the list.

fixes #14440

</br>
</br>

### Single PKG installation - Not enough space available

![pr_single_no_space](https://github.com/user-attachments/assets/3fc83d3e-8219-4dca-a4ae-a351f3c4c662)

</br>
</br>

### Single PKG installation - Required space available

![pr_single_space](https://github.com/user-attachments/assets/84b6b4a0-ee4a-430e-b84f-60a754e0a4fa)

</br>
</br>

### Multi PKG installation - Not enough space available

![pr_no_space](https://github.com/user-attachments/assets/56af51a1-cd78-4ad5-818d-7707c0c45de4)

</br>
</br>

### Multi PKG installation - Required space available

![pr_space](https://github.com/user-attachments/assets/43cc66dd-7e98-46de-8dd3-859f8ebb2b05)

---------

Co-authored-by: Megamouse <studienricky89@googlemail.com>
This commit is contained in:
Antonino Di Guardo 2025-12-07 14:38:13 +01:00 committed by GitHub
parent 3f6529fecb
commit 0f1d516d9a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 82 additions and 16 deletions

View File

@ -347,6 +347,7 @@ public:
};
bool is_valid() const { return m_is_valid; }
const PKGHeader& get_header() const { return m_header; }
package_install_result check_target_app_version() const;
static package_install_result extract_data(std::deque<package_reader>& readers, std::deque<std::string>& bootable_paths);
const psf::registry& get_psf() const { return m_psf; }

View File

@ -184,6 +184,11 @@ namespace rpcs3::utils
return g_cfg_vfs.get(g_cfg_vfs.dev_bdvd, get_emu_dir());
}
std::string get_hdd0_game_dir()
{
return get_hdd0_dir() + "game/";
}
u64 get_cache_disk_usage()
{
if (const u64 data_size = fs::get_dir_size(rpcs3::utils::get_cache_dir(), 1); data_size != umax)

View File

@ -34,6 +34,8 @@ namespace rpcs3::utils
std::string get_flash3_dir();
std::string get_bdvd_dir();
std::string get_hdd0_game_dir();
// Cache directories and disk usage
u64 get_cache_disk_usage();
std::string get_cache_dir();

View File

@ -272,6 +272,10 @@ compat::package_info game_compatibility::GetPkgInfo(const QString& pkg_path, gam
info.category = QString::fromStdString(std::string(psf::get_string(psf, "CATEGORY")));
info.version = QString::fromStdString(std::string(psf::get_string(psf, "APP_VER")));
// Technically, there is no specific package's header info providing its installation size on disk.
// We use "data_size" header as an approximation (a bit larger) for this purpose
info.data_size = reader.get_header().data_size.value();
if (!info.category.isEmpty())
{
const Localized localized;

View File

@ -1,5 +1,7 @@
#pragma once
#include "util/types.hpp"
#include <memory>
#include <QJsonObject>
@ -108,6 +110,7 @@ namespace compat
QString version; // May be empty
QString category; // HG, DG, GD etc.
QString local_cat; // Localized category
u64 data_size = 0; // Installation size
package_type type = package_type::other; // The type of package (Update, DLC or other)
};

View File

@ -860,11 +860,28 @@ bool main_window::InstallPackages(QStringList file_paths, bool from_boot)
info.changelog = tr("Changelog:\n%0", "Block for Changelog").arg(info.changelog);
}
const QString info_string = QStringLiteral("%0\n\n%1%2%3%4").arg(file_info.fileName()).arg(info.title).arg(info.local_cat).arg(info.title_id).arg(info.version);
QString message = tr("Do you want to install this package?\n\n%0").arg(info_string);
u64 free_space = 0;
QMessageBox mb(QMessageBox::Icon::Question, tr("PKG Decrypter / Installer"), message, QMessageBox::Yes | QMessageBox::No, this);
// Retrieve disk space info on data path's drive
if (fs::device_stat stat{}; fs::statfs(rpcs3::utils::get_hdd0_dir(), stat))
{
free_space = stat.avail_free;
}
const QString installation_info =
tr("Installation path: %0\nAvailable disk space: %1%2\nRequired disk space: %3")
.arg(rpcs3::utils::get_hdd0_game_dir())
.arg(gui::utils::format_byte_size(free_space))
.arg(info.data_size <= free_space ? QString() : tr(" - <b>NOT ENOUGH SPACE</b>"))
.arg(gui::utils::format_byte_size(info.data_size));
const QString info_string = QStringLiteral("%0\n\n%1%2%3%4").arg(file_info.fileName()).arg(info.title).arg(info.local_cat).arg(info.title_id).arg(info.version);
QString message = tr("Do you want to install this package?\n\n%0\n\n%1").arg(info_string).arg(installation_info);
QMessageBox mb(QMessageBox::Icon::Question, tr("PKG Decrypter / Installer"), gui::utils::make_paragraph(message), QMessageBox::Yes | QMessageBox::No, this);
mb.setDefaultButton(QMessageBox::No);
mb.setTextFormat(Qt::RichText); // Support HTML tags
mb.button(QMessageBox::Yes)->setEnabled(info.data_size <= free_space);
if (!info.changelog.isEmpty())
{

View File

@ -2,6 +2,10 @@
#include "game_compatibility.h"
#include "numbered_widget_item.h"
#include "richtext_item_delegate.h"
#include "qt_utils.h"
#include "Emu/system_utils.hpp"
#include "Utilities/File.h"
#include <QDialogButtonBox>
#include <QPushButton>
@ -17,6 +21,7 @@ enum Roles
TitleRole = Qt::UserRole + 2,
TitleIdRole = Qt::UserRole + 3,
VersionRole = Qt::UserRole + 4,
DataSizeRole = Qt::UserRole + 5,
};
pkg_install_dialog::pkg_install_dialog(const QStringList& paths, game_compatibility* compat, QWidget* parent)
@ -89,7 +94,8 @@ pkg_install_dialog::pkg_install_dialog(const QStringList& paths, game_compatibil
append_comma();
accumulated_info += file_info.fileName();
const QString text = tr("<b>%0</b> (%2)", "Package text").arg(info.title.simplified()).arg(accumulated_info);
const QString text = tr("<b>%0</b> (%1) - %2", "Package text").arg(info.title.simplified())
.arg(accumulated_info).arg(gui::utils::format_byte_size(info.data_size));
QListWidgetItem* item = new numbered_widget_item(text, m_dir_list);
item->setData(Roles::FullPathRole, info.path);
@ -97,6 +103,7 @@ pkg_install_dialog::pkg_install_dialog(const QStringList& paths, game_compatibil
item->setData(Roles::TitleRole, info.title);
item->setData(Roles::TitleIdRole, info.title_id);
item->setData(Roles::VersionRole, info.version);
item->setData(Roles::DataSizeRole, static_cast<qulonglong>(info.data_size));
item->setToolTip(tooltip);
item->setFlags(item->flags() | Qt::ItemIsUserCheckable);
item->setCheckState(Qt::Checked);
@ -106,6 +113,10 @@ pkg_install_dialog::pkg_install_dialog(const QStringList& paths, game_compatibil
m_dir_list->setCurrentRow(0);
m_dir_list->setMinimumWidth((m_dir_list->sizeHintForColumn(0) * 125) / 100);
// Create contextual label (updated in connect(m_dir_list, &QListWidget::itemChanged ...))
QLabel* installation_info = new QLabel();
installation_info->setTextFormat(Qt::RichText); // Support HTML tags
// Create buttons
QDialogButtonBox* buttons = new QDialogButtonBox(QDialogButtonBox::Cancel | QDialogButtonBox::Ok);
buttons->button(QDialogButtonBox::Ok)->setText(tr("Install"));
@ -123,19 +134,9 @@ pkg_install_dialog::pkg_install_dialog(const QStringList& paths, game_compatibil
}
});
connect(m_dir_list, &QListWidget::itemChanged, this, [this, buttons](QListWidgetItem*)
connect(m_dir_list, &QListWidget::itemChanged, this, [this, installation_info, buttons](QListWidgetItem*)
{
bool any_checked = false;
for (int i = 0; i < m_dir_list->count(); i++)
{
if (m_dir_list->item(i)->checkState() == Qt::Checked)
{
any_checked = true;
break;
}
}
buttons->button(QDialogButtonBox::Ok)->setEnabled(any_checked);
UpdateInfo(installation_info, buttons);
});
QToolButton* move_up = new QToolButton;
@ -159,11 +160,41 @@ pkg_install_dialog::pkg_install_dialog(const QStringList& paths, game_compatibil
vbox->addWidget(description);
vbox->addLayout(hbox);
vbox->addWidget(m_dir_list);
vbox->addWidget(installation_info);
vbox->addWidget(buttons);
setLayout(vbox);
setWindowTitle(tr("Batch PKG Installation"));
setObjectName("pkg_install_dialog");
UpdateInfo(installation_info, buttons); // Just to show and check available and required size
}
void pkg_install_dialog::UpdateInfo(QLabel* installation_info, QDialogButtonBox* buttons) const
{
u64 data_size = 0;
u64 free_space = 0;
// Retrieve disk space info on data path's drive
if (fs::device_stat stat{}; fs::statfs(rpcs3::utils::get_hdd0_game_dir(), stat))
{
free_space = stat.avail_free;
}
for (int i = 0; i < m_dir_list->count(); i++)
{
if (m_dir_list->item(i)->checkState() == Qt::Checked)
{
data_size += m_dir_list->item(i)->data(Roles::DataSizeRole).toULongLong();
}
}
installation_info->setText(gui::utils::make_paragraph(
tr("Installation path: %0\nAvailable disk space: %1%2\nRequired disk space: %3")
.arg(rpcs3::utils::get_hdd0_game_dir())
.arg(gui::utils::format_byte_size(free_space))
.arg(data_size <= free_space ? QString() : tr(" - <b>NOT ENOUGH SPACE</b>"))
.arg(gui::utils::format_byte_size(data_size))));
buttons->button(QDialogButtonBox::Ok)->setEnabled(data_size && (data_size <= free_space));
}
void pkg_install_dialog::MoveItem(int offset) const

View File

@ -2,6 +2,8 @@
#include <QDialog>
#include <QListWidget>
#include <QLabel>
#include <QDialogButtonBox>
namespace compat
{
@ -19,6 +21,7 @@ public:
std::vector<compat::package_info> GetPathsToInstall() const;
private:
void UpdateInfo(QLabel* installation_info, QDialogButtonBox* buttons) const;
void MoveItem(int offset) const;
QListWidget* m_dir_list;