verison deletion feature
This commit is contained in:
+206
-3
@@ -108,6 +108,183 @@ Dialog {
|
||||
picker.close()
|
||||
}
|
||||
|
||||
function catalogHas(versionId) {
|
||||
const source = picker.backend.versionCatalog
|
||||
for (var i = 0; i < source.length; ++i) {
|
||||
if (source[i].id === versionId)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Спрашиваем до удаления: версию придётся качать заново, а профили
|
||||
// модлоадеров поверх неё — переустанавливать. Id запоминаем здесь:
|
||||
// к моменту ответа строка под курсором может быть уже другой.
|
||||
function askRemove(versionId) {
|
||||
const info = picker.backend.versionRemovalInfo(versionId)
|
||||
if (!info || info.installed !== true)
|
||||
return
|
||||
removeConfirm.versionId = versionId
|
||||
removeConfirm.sizeMb = info.sizeMb || 0
|
||||
removeConfirm.dependents = info.dependents || []
|
||||
removeConfirm.builds = info.builds || []
|
||||
removeConfirm.open()
|
||||
}
|
||||
|
||||
function performRemove(versionId) {
|
||||
picker.backend.removeVersion(versionId)
|
||||
// Профиль модлоадера был в каталоге только потому, что установлен:
|
||||
// после удаления строка исчезает совсем, и выбор надо снять.
|
||||
if (picker.selectedId === versionId && !picker.catalogHas(versionId))
|
||||
picker.selectedId = ""
|
||||
picker.revealSelected()
|
||||
}
|
||||
|
||||
Dialog {
|
||||
id: removeConfirm
|
||||
|
||||
property string versionId: ""
|
||||
property int sizeMb: 0
|
||||
property var dependents: []
|
||||
property var builds: []
|
||||
|
||||
modal: true
|
||||
padding: 0
|
||||
width: 420
|
||||
parent: picker.parent
|
||||
x: (picker.parent.width - width) / 2
|
||||
y: (picker.parent.height - height) / 2
|
||||
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
|
||||
|
||||
background: Rectangle {
|
||||
color: "#1e1e1e"
|
||||
radius: 10
|
||||
border.color: "#cc6666"
|
||||
border.width: 1
|
||||
}
|
||||
|
||||
header: Item {
|
||||
implicitHeight: 52
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: qsTr("Удалить версию?")
|
||||
color: "#ffffff"
|
||||
font.pixelSize: 17
|
||||
font.bold: true
|
||||
}
|
||||
Rectangle {
|
||||
anchors.bottom: parent.bottom
|
||||
width: parent.width
|
||||
height: 1
|
||||
color: "#333333"
|
||||
}
|
||||
}
|
||||
|
||||
contentItem: Column {
|
||||
spacing: 10
|
||||
topPadding: 18
|
||||
bottomPadding: 18
|
||||
|
||||
Text {
|
||||
x: 20
|
||||
width: parent.width - 40
|
||||
wrapMode: Text.Wrap
|
||||
color: "#ffffff"
|
||||
font.pixelSize: 13
|
||||
text: removeConfirm.sizeMb > 0
|
||||
? qsTr("Файлы версии «%1» будут удалены из versions/ — освободится около %2 МБ. Скачать её заново можно в любой момент.")
|
||||
.arg(removeConfirm.versionId).arg(removeConfirm.sizeMb)
|
||||
: qsTr("Файлы версии «%1» будут удалены из versions/. Скачать её заново можно в любой момент.")
|
||||
.arg(removeConfirm.versionId)
|
||||
}
|
||||
|
||||
Text {
|
||||
x: 20
|
||||
width: parent.width - 40
|
||||
visible: removeConfirm.dependents.length > 0
|
||||
wrapMode: Text.Wrap
|
||||
color: "#cc6666"
|
||||
font.pixelSize: 12
|
||||
text: qsTr("На эту версию опираются установленные профили модлоадеров: %1. Без неё они не запустятся — их придётся переустановить.")
|
||||
.arg(removeConfirm.dependents.join(", "))
|
||||
}
|
||||
|
||||
Text {
|
||||
x: 20
|
||||
width: parent.width - 40
|
||||
visible: removeConfirm.builds.length > 0
|
||||
wrapMode: Text.Wrap
|
||||
color: "#cc6666"
|
||||
font.pixelSize: 12
|
||||
text: qsTr("Версию используют сборки: %1. Файлы понадобится скачать заново кнопкой «Установить».")
|
||||
.arg(removeConfirm.builds.join(", "))
|
||||
}
|
||||
|
||||
Text {
|
||||
x: 20
|
||||
width: parent.width - 40
|
||||
wrapMode: Text.Wrap
|
||||
color: "#888888"
|
||||
font.pixelSize: 12
|
||||
text: qsTr("Библиотеки и ресурсы в libraries/ и assets/ общие для всех версий и остаются на месте.")
|
||||
}
|
||||
}
|
||||
|
||||
footer: Item {
|
||||
implicitHeight: 60
|
||||
Rectangle {
|
||||
anchors.top: parent.top
|
||||
width: parent.width
|
||||
height: 1
|
||||
color: "#333333"
|
||||
}
|
||||
Row {
|
||||
anchors.centerIn: parent
|
||||
spacing: 12
|
||||
|
||||
Button {
|
||||
text: qsTr("Отмена")
|
||||
width: 130; height: 36
|
||||
contentItem: Text {
|
||||
text: parent.text
|
||||
color: "#ffffff"
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
background: Rectangle {
|
||||
color: parent.pressed ? "#6a8510" : "#91B315"
|
||||
radius: 6
|
||||
}
|
||||
onClicked: removeConfirm.close()
|
||||
}
|
||||
|
||||
Button {
|
||||
text: qsTr("Удалить")
|
||||
width: 130; height: 36
|
||||
contentItem: Text {
|
||||
text: parent.text
|
||||
color: "#ffffff"
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
background: Rectangle {
|
||||
color: parent.pressed ? "#8a3a3a" : "#663333"
|
||||
radius: 6
|
||||
border.color: "#cc6666"
|
||||
border.width: 1
|
||||
}
|
||||
onClicked: {
|
||||
const id = removeConfirm.versionId
|
||||
removeConfirm.close()
|
||||
picker.performRemove(id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onClosed: removeConfirm.versionId = ""
|
||||
}
|
||||
|
||||
header: Item {
|
||||
implicitHeight: 52
|
||||
Text {
|
||||
@@ -291,7 +468,7 @@ Dialog {
|
||||
Text {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: 10
|
||||
anchors.right: installedMark.left
|
||||
anchors.right: rightMarks.left
|
||||
anchors.rightMargin: 6
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: versionRow.modelData.label
|
||||
@@ -300,21 +477,47 @@ Dialog {
|
||||
font.pixelSize: 13
|
||||
}
|
||||
|
||||
Text {
|
||||
id: installedMark
|
||||
Row {
|
||||
id: rightMarks
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: 10
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: 8
|
||||
|
||||
Text {
|
||||
visible: versionRow.modelData.installed
|
||||
text: "✓"
|
||||
color: picker.selectedId === versionRow.modelData.id ? "#1e1e1e" : "#91B315"
|
||||
font.pixelSize: 13
|
||||
font.bold: true
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
// Версия весит от десятков мегабайт, а галочка рядом —
|
||||
// единственное место, где видно, что именно скачано.
|
||||
Image {
|
||||
visible: versionRow.modelData.installed
|
||||
source: "images/Trash.svg"
|
||||
width: 14
|
||||
height: 14
|
||||
fillMode: Image.PreserveAspectFit
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
opacity: trashArea.containsMouse ? 1.0 : 0.55
|
||||
|
||||
MouseArea {
|
||||
id: trashArea
|
||||
anchors.fill: parent
|
||||
anchors.margins: -5
|
||||
hoverEnabled: true
|
||||
onClicked: picker.askRemove(versionRow.modelData.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: versionArea
|
||||
anchors.fill: parent
|
||||
anchors.rightMargin: 40
|
||||
hoverEnabled: true
|
||||
onClicked: {
|
||||
picker.selectedId = versionRow.modelData.id
|
||||
|
||||
@@ -609,6 +609,63 @@ bool LauncherBackend::isVersionInstalled(const QString &versionId) const
|
||||
return VersionLoader::installedVersions(gameDir()).contains(versionId);
|
||||
}
|
||||
|
||||
QVariantMap LauncherBackend::versionRemovalInfo(const QString &versionId) const
|
||||
{
|
||||
const QString id = versionId.trimmed();
|
||||
if (id.isEmpty())
|
||||
return {};
|
||||
|
||||
const qint64 bytes = VersionLoader::installedSize(gameDir(), id);
|
||||
// Сборки не правим при удалении — ссылка на неустановленную версию это
|
||||
// рабочее состояние. Но перечислить, кого заденет, надо.
|
||||
QStringList builds;
|
||||
for (const CustomBuild &build : m_builds) {
|
||||
if (build.minecraftVersion == id || build.resolvedVersionId == id
|
||||
|| launchVersionIdOf(build) == id) {
|
||||
builds << build.name;
|
||||
}
|
||||
}
|
||||
|
||||
return {{QStringLiteral("id"), id},
|
||||
{QStringLiteral("installed"), isVersionInstalled(id)},
|
||||
{QStringLiteral("sizeMb"), static_cast<int>((bytes + 1024 * 1024 - 1) / (1024 * 1024))},
|
||||
{QStringLiteral("dependents"), VersionLoader::dependentsOf(gameDir(), id)},
|
||||
{QStringLiteral("builds"), builds}};
|
||||
}
|
||||
|
||||
void LauncherBackend::removeVersion(const QString &versionId)
|
||||
{
|
||||
const QString id = versionId.trimmed();
|
||||
if (id.isEmpty())
|
||||
return;
|
||||
|
||||
if (m_installer->versionId() == id || m_installer->isQueued(id)) {
|
||||
emit launchError(tr("Эта версия сейчас скачивается"));
|
||||
return;
|
||||
}
|
||||
if (m_switcher->isRunning() || m_busy || m_installer->isRunning()
|
||||
|| m_loaderInstaller->isRunning()) {
|
||||
emit launchError(tr("Дождитесь окончания текущей операции"));
|
||||
return;
|
||||
}
|
||||
// Какой версией сейчас играют, GameLauncher наружу не отдаёт, поэтому на
|
||||
// время игры удаление запрещено целиком.
|
||||
if (m_launcher->isRunning()) {
|
||||
emit launchError(tr("Сначала закройте игру"));
|
||||
return;
|
||||
}
|
||||
|
||||
QString error;
|
||||
if (!VersionLoader::remove(gameDir(), id, &error)) {
|
||||
emit launchError(tr("Не удалось удалить версию: %1").arg(error));
|
||||
return;
|
||||
}
|
||||
|
||||
invalidateCatalog();
|
||||
emit installedVersionsChanged();
|
||||
emit launchProgress(tr("Версия «%1» удалена").arg(id));
|
||||
}
|
||||
|
||||
QVariantList LauncherBackend::versionCatalog() const
|
||||
{
|
||||
if (m_catalogDirty) {
|
||||
|
||||
@@ -135,6 +135,9 @@ public:
|
||||
Q_INVOKABLE void refreshVersionCatalog(bool force = false);
|
||||
Q_INVOKABLE bool isVersionInstalled(const QString &versionId) const;
|
||||
Q_INVOKABLE void installVersion(const QString &versionId);
|
||||
// Что потеряется при удалении версии — для текста предупреждения.
|
||||
Q_INVOKABLE QVariantMap versionRemovalInfo(const QString &versionId) const;
|
||||
Q_INVOKABLE void removeVersion(const QString &versionId);
|
||||
Q_INVOKABLE void cancelDownload();
|
||||
|
||||
// Версии модлоадеров для выбранной версии игры. Возвращает список карт
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "minecraftversion.h"
|
||||
|
||||
#include <QDir>
|
||||
#include <QDirIterator>
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
#include <QHash>
|
||||
@@ -27,6 +28,27 @@ QString versionJarPath(const QString &gameDir, const QString &id)
|
||||
return gameDir + "/versions/" + id + "/" + id + ".jar";
|
||||
}
|
||||
|
||||
QString versionsRoot(const QString &gameDir)
|
||||
{
|
||||
return gameDir + QStringLiteral("/versions");
|
||||
}
|
||||
|
||||
// Идентификатор версии — это имя папки, поэтому «чинить» его как sanitizeId()
|
||||
// нельзя: подстановка символов увела бы нас не в ту версию. Только проверяем.
|
||||
bool isSafeVersionId(const QString &id)
|
||||
{
|
||||
return !id.isEmpty() && !id.startsWith(u'.') && !id.contains(u'/') && !id.contains(u'\\');
|
||||
}
|
||||
|
||||
// Верхний объект <id>.json без разворачивания цепочки: нужен только inheritsFrom.
|
||||
QJsonObject readVersionObject(const QString &gameDir, const QString &id)
|
||||
{
|
||||
QFile file(versionJsonPath(gameDir, id));
|
||||
if (!file.open(QIODevice::ReadOnly))
|
||||
return {};
|
||||
return QJsonDocument::fromJson(file.readAll()).object();
|
||||
}
|
||||
|
||||
// org.lwjgl:lwjgl:3.3.1:natives-windows -> org/lwjgl/lwjgl/3.3.1/lwjgl-3.3.1-natives-windows.jar
|
||||
QString mavenToPath(const QString &name)
|
||||
{
|
||||
@@ -311,6 +333,68 @@ QStringList VersionLoader::installedVersions(const QString &gameDir)
|
||||
return result;
|
||||
}
|
||||
|
||||
QStringList VersionLoader::dependentsOf(const QString &gameDir, const QString &versionId)
|
||||
{
|
||||
if (versionId.isEmpty())
|
||||
return {};
|
||||
|
||||
// Одного уровня хватает: профили модлоадеров наследуются от ванильных версий,
|
||||
// а не друг от друга.
|
||||
QStringList result;
|
||||
const QStringList installed = installedVersions(gameDir);
|
||||
for (const QString &id : installed) {
|
||||
if (id == versionId)
|
||||
continue;
|
||||
const QJsonObject object = readVersionObject(gameDir, id);
|
||||
if (object.value(QStringLiteral("inheritsFrom")).toString() == versionId)
|
||||
result << id;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
qint64 VersionLoader::installedSize(const QString &gameDir, const QString &versionId)
|
||||
{
|
||||
if (!isSafeVersionId(versionId))
|
||||
return 0;
|
||||
|
||||
qint64 total = 0;
|
||||
QDirIterator it(versionsRoot(gameDir) + u'/' + versionId,
|
||||
QDir::Files | QDir::Hidden | QDir::System | QDir::NoSymLinks,
|
||||
QDirIterator::Subdirectories);
|
||||
while (it.hasNext()) {
|
||||
it.next();
|
||||
total += it.fileInfo().size();
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
bool VersionLoader::remove(const QString &gameDir, const QString &versionId, QString *error)
|
||||
{
|
||||
const QString root = versionsRoot(gameDir);
|
||||
if (gameDir.isEmpty() || !isSafeVersionId(versionId)) {
|
||||
if (error)
|
||||
*error = QObject::tr("недопустимый идентификатор версии");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Удаляем только внутри <gameDir>/versions: проверка id уже не пропустит «..»,
|
||||
// но цена ошибки — чужая папка, а проверка стоит одной строки.
|
||||
const QString dir = QDir::cleanPath(root + u'/' + versionId);
|
||||
if (!dir.startsWith(root + u'/')) {
|
||||
if (error)
|
||||
*error = QObject::tr("недопустимый идентификатор версии");
|
||||
return false;
|
||||
}
|
||||
if (!QDir(dir).exists())
|
||||
return true;
|
||||
if (!QDir(dir).removeRecursively()) {
|
||||
if (error)
|
||||
*error = QObject::tr("не удалось удалить %1").arg(QDir::toNativeSeparators(dir));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
MinecraftVersion VersionLoader::load(const QString &gameDir,
|
||||
const QString &versionId,
|
||||
const QSet<QString> &features,
|
||||
|
||||
@@ -47,6 +47,16 @@ namespace VersionLoader {
|
||||
// Версии, реально установленные в <gameDir>/versions (есть папка и <id>.json).
|
||||
QStringList installedVersions(const QString &gameDir);
|
||||
|
||||
// Установленные профили, у которых inheritsFrom == versionId: без базовой версии
|
||||
// они не запустятся.
|
||||
QStringList dependentsOf(const QString &gameDir, const QString &versionId);
|
||||
|
||||
// Размер <gameDir>/versions/<id> в байтах; 0 — папки нет.
|
||||
qint64 installedSize(const QString &gameDir, const QString &versionId);
|
||||
|
||||
// Сносит <gameDir>/versions/<id>. Отсутствие папки — успех.
|
||||
bool remove(const QString &gameDir, const QString &versionId, QString *error);
|
||||
|
||||
// Читает и разворачивает версию. При ошибке возвращает невалидный объект и пишет error.
|
||||
MinecraftVersion load(const QString &gameDir,
|
||||
const QString &versionId,
|
||||
|
||||
Reference in New Issue
Block a user