Skip to content

Commit

Permalink
feat: display progress bar when extracting multiple files from a vpk,…
Browse files Browse the repository at this point in the history
… add better way to toggle user action triggerability
  • Loading branch information
craftablescience committed Sep 21, 2023
1 parent 25cd382 commit 8692e48
Show file tree
Hide file tree
Showing 3 changed files with 125 additions and 37 deletions.
2 changes: 1 addition & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
cmake_minimum_required(VERSION 3.25 FATAL_ERROR)
project(vpkedit
DESCRIPTION "A tool to read, preview, and write to VPK files."
VERSION 3.3.1
VERSION 3.3.2
HOMEPAGE_URL "https://github.com/craftablescience/VPKEdit")
set(PROJECT_NAME_PRETTY "VPKEdit" CACHE STRING "" FORCE)
set(PROJECT_HOMEPAGE_URL_API "https://api.github.com/repos/craftablescience/VPKEdit" CACHE STRING "" FORCE)
Expand Down
134 changes: 98 additions & 36 deletions src/gui/Window.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
#include <QStatusBar>
#include <QStyle>
#include <QStyleFactory>
#include <QThread>

#include <sapp/FilesystemSearchProvider.h>

Expand All @@ -42,24 +43,25 @@ constexpr auto VPK_SAVE_FILTER = "Valve PacK (*.vpk);;All files (*.*)";

Window::Window(QSettings& options, QWidget* parent)
: QMainWindow(parent)
, extractWorkerThread(nullptr)
, modified(false) {
this->setWindowIcon(QIcon(":/icon.png"));
this->setMinimumSize(900, 500);

// File menu
auto* fileMenu = this->menuBar()->addMenu(tr("&File"));
fileMenu->addAction(this->style()->standardIcon(QStyle::SP_FileIcon), tr("&Create Empty..."), Qt::CTRL | Qt::Key_N, [=] {
this->createEmptyVPKAction = fileMenu->addAction(this->style()->standardIcon(QStyle::SP_FileIcon), tr("&Create Empty..."), Qt::CTRL | Qt::Key_N, [=] {
this->newVPK(false);
});
fileMenu->addAction(this->style()->standardIcon(QStyle::SP_FileIcon), tr("Create From &Folder..."), Qt::CTRL | Qt::Key_N, [=] {
this->createVPKFromDirAction = fileMenu->addAction(this->style()->standardIcon(QStyle::SP_FileIcon), tr("Create From &Folder..."), Qt::CTRL | Qt::Key_N, [=] {
this->newVPK(true);
});
fileMenu->addAction(this->style()->standardIcon(QStyle::SP_DirIcon), tr("&Open..."), Qt::CTRL | Qt::Key_O, [=] {
this->openVPKAction = fileMenu->addAction(this->style()->standardIcon(QStyle::SP_DirIcon), tr("&Open..."), Qt::CTRL | Qt::Key_O, [=] {
this->openVPK();
});

if (CFileSystemSearchProvider provider; provider.Available()) {
auto* openRelativeToMenu = fileMenu->addMenu(this->style()->standardIcon(QStyle::SP_DirLinkIcon), tr("Open &In..."));
this->openVPKRelativeToMenu = fileMenu->addMenu(this->style()->standardIcon(QStyle::SP_DirLinkIcon), tr("Open &In..."));

QList<std::tuple<QString, QString, QDir>> sourceGames;
auto installedSteamAppCount = provider.GetNumInstalledApps();
Expand All @@ -83,10 +85,12 @@ Window::Window(QSettings& options, QWidget* parent)

for (const auto& [gameName, iconPath, relativeDirectoryPath] : sourceGames) {
const auto relativeDirectory = relativeDirectoryPath.path();
openRelativeToMenu->addAction(QIcon(iconPath), gameName, [=] {
this->openVPKRelativeToMenu->addAction(QIcon(iconPath), gameName, [=] {
this->openVPK(relativeDirectory);
});
}
} else {
this->openVPKRelativeToMenu = nullptr;
}

this->saveVPKAction = fileMenu->addAction(this->style()->standardIcon(QStyle::SP_DialogSaveButton), tr("&Save"), Qt::CTRL | Qt::Key_S, [=] {
Expand Down Expand Up @@ -410,19 +414,19 @@ void Window::aboutQt() {
}

std::optional<std::vector<std::byte>> Window::readBinaryEntry(const QString& path) {
auto entry = (*this->vpk).findEntry(path.toStdString());
auto entry = this->vpk->findEntry(path.toStdString());
if (!entry) {
return std::nullopt;
}
return (*this->vpk).readBinaryEntry(*entry);
return this->vpk->readBinaryEntry(*entry);
}

std::optional<QString> Window::readTextEntry(const QString& path) {
auto entry = (*this->vpk).findEntry(path.toStdString());
auto entry = this->vpk->findEntry(path.toStdString());
if (!entry) {
return std::nullopt;
}
auto textData = (*this->vpk).readTextEntry(*entry);
auto textData = this->vpk->readTextEntry(*entry);
if (!textData) {
return std::nullopt;
}
Expand All @@ -442,7 +446,7 @@ void Window::selectSubItemInDir(const QString& name) {
}

void Window::extractFile(const QString& path, QString savePath) {
auto entry = (*this->vpk).findEntry(path.toStdString());
auto entry = this->vpk->findEntry(path.toStdString());
if (!entry) {
QMessageBox::critical(this, tr("Error"), "Failed to find file in VPK.");
return;
Expand All @@ -466,23 +470,48 @@ void Window::extractFile(const QString& path, QString savePath) {
}

void Window::extractFilesIf(const QString& saveDir, const std::function<bool(const QString&)>& predicate) {
for (const auto& [directory, entries] : (*this->vpk).getEntries()) {
QString dir(directory.c_str());
if (!predicate(dir)) {
// Set up progress bar
this->statusText->hide();
this->statusProgressBar->show();

// Get progress bar maximum
int progressBarMax = 0;
for (const auto& [directory, entries] : this->vpk->getEntries()) {
if (!predicate(QString(directory.c_str()))) {
continue;
}
progressBarMax += static_cast<int>(entries.size());
}

QDir qDir;
if (!qDir.mkpath(saveDir + '/' + dir)) {
QMessageBox::critical(this, tr("Error"), "Failed to create directory.");
return;
}
this->statusProgressBar->setMinimum(0);
this->statusProgressBar->setMaximum(progressBarMax);
this->statusProgressBar->setValue(0);

for (const auto& entry : entries) {
auto filePath = saveDir + '/' + dir + '/' + entry.filename.c_str();
this->writeEntryToFile(filePath, entry);
}
}
this->freezeActions(true);

// Set up thread
this->extractWorkerThread = new QThread(this);
auto* worker = new ExtractVPKWorker();
worker->moveToThread(this->extractWorkerThread);
QObject::connect(this->extractWorkerThread, &QThread::started, worker, [=] {
worker->run(this, saveDir, predicate);
});
QObject::connect(worker, &ExtractVPKWorker::progressUpdated, this, [=] {
this->statusProgressBar->setValue(this->statusProgressBar->value() + 1);
});
QObject::connect(worker, &ExtractVPKWorker::taskFinished, this, [=] {
// Kill thread
this->extractWorkerThread->quit();
this->extractWorkerThread->wait();
delete this->extractWorkerThread;
this->extractWorkerThread = nullptr;

this->freezeActions(false);

this->statusText->show();
this->statusProgressBar->hide();
});
this->extractWorkerThread->start();
}

void Window::extractDir(const QString& path, QString saveDir) {
Expand All @@ -503,7 +532,7 @@ void Window::extractAll(QString saveDir) {
return;
}
saveDir += '/';
saveDir += (*this->vpk).getPrettyFileName();
saveDir += this->vpk->getRealFileName();

this->extractFilesIf(saveDir, [](const QString&) { return true; });
}
Expand Down Expand Up @@ -547,15 +576,12 @@ void Window::clearContents() {
this->searchBar->setDisabled(true);

this->entryTree->clearContents();
this->entryTree->setDisabled(true);

this->fileViewer->clearContents();

this->saveAsVPKAction->setDisabled(true);
this->closeFileAction->setDisabled(true);
this->addFileAction->setDisabled(true);
this->extractAllAction->setDisabled(true);

this->markModified(false);
this->freezeActions(true, false); // Leave create/open unfrozen
}

void Window::closeEvent(QCloseEvent* event) {
Expand All @@ -566,11 +592,29 @@ void Window::closeEvent(QCloseEvent* event) {
event->accept();
}

void Window::freezeActions(bool freeze, bool freezeCreationActions) {
this->createEmptyVPKAction->setDisabled(freeze && freezeCreationActions);
this->createVPKFromDirAction->setDisabled(freeze && freezeCreationActions);
this->openVPKAction->setDisabled(freeze && freezeCreationActions);
if (this->openVPKRelativeToMenu) this->openVPKRelativeToMenu->setDisabled(freeze && freezeCreationActions);
this->saveVPKAction->setDisabled(freeze || !this->modified);
this->saveAsVPKAction->setDisabled(freeze);
this->closeFileAction->setDisabled(freeze);
this->addFileAction->setDisabled(freeze);
this->extractAllAction->setDisabled(freeze);

this->searchBar->setDisabled(freeze);
this->entryTree->setDisabled(freeze);
this->fileViewer->setDisabled(freeze);
}

bool Window::loadVPK(const QString& path) {
QString fixedPath(path);
fixedPath.replace('\\', '/');

this->clearContents();
this->freezeActions(true);

this->vpk = VPK::open(fixedPath.toStdString());
if (!this->vpk) {
QMessageBox::critical(this, tr("Error"), "Unable to load given VPK. Please ensure you are loading a "
Expand All @@ -584,13 +628,8 @@ bool Window::loadVPK(const QString& path) {
this->statusText->hide();
this->statusProgressBar->show();

this->searchBar->setDisabled(false);

this->entryTree->loadVPK(this->vpk.value(), this->statusProgressBar, [=] {
this->saveAsVPKAction->setDisabled(false);
this->closeFileAction->setDisabled(false);
this->addFileAction->setDisabled(false);
this->extractAllAction->setDisabled(false);
this->freezeActions(false);

this->statusText->setText(' ' + QString("Loaded \"") + path + '\"');
this->statusText->show();
Expand All @@ -601,7 +640,7 @@ bool Window::loadVPK(const QString& path) {
}

void Window::writeEntryToFile(const QString& path, const VPKEntry& entry) {
auto data = (*this->vpk).readBinaryEntry(entry);
auto data = this->vpk->readBinaryEntry(entry);
if (!data) {
QMessageBox::critical(this, tr("Error"), QString("Failed to read data from the VPK for \"") + entry.filename.c_str() + "\". Please ensure that a game or another application is not using the VPK.");
return;
Expand All @@ -617,3 +656,26 @@ void Window::writeEntryToFile(const QString& path, const VPKEntry& entry) {
}
file.close();
}

void ExtractVPKWorker::run(Window* window, const QString& saveDir, const std::function<bool(const QString&)>& predicate) {
int currentEntry = 0;
for (const auto& [directory, entries] : window->vpk->getEntries()) {
QString dir(directory.c_str());
if (!predicate(dir)) {
continue;
}

QDir qDir;
if (!qDir.mkpath(saveDir + '/' + dir)) {
QMessageBox::critical(window, tr("Error"), "Failed to create directory.");
return;
}

for (const auto& entry : entries) {
auto filePath = saveDir + '/' + dir + '/' + entry.filename.c_str();
window->writeEntryToFile(filePath, entry);
emit progressUpdated(++currentEntry);
}
}
emit taskFinished();
}
26 changes: 26 additions & 0 deletions src/gui/Window.h
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,24 @@
#include <QMainWindow>
#include <vpkedit/VPK.h>

class QAction;
class QLabel;
class QLineEdit;
class QMenu;
class QNetworkAccessManager;
class QNetworkReply;
class QProgressBar;
class QSettings;
class QThread;

class EntryTree;
class FileViewer;

class Window : public QMainWindow {
Q_OBJECT;

friend class ExtractVPKWorker;

public:
explicit Window(QSettings& options, QWidget* parent = nullptr);

Expand Down Expand Up @@ -76,6 +81,10 @@ class Window : public QMainWindow {
EntryTree* entryTree;
FileViewer* fileViewer;

QAction* createEmptyVPKAction;
QAction* createVPKFromDirAction;
QAction* openVPKAction;
QMenu* openVPKRelativeToMenu;
QAction* saveVPKAction;
QAction* saveAsVPKAction;
QAction* closeFileAction;
Expand All @@ -84,12 +93,29 @@ class Window : public QMainWindow {

QNetworkAccessManager* checkForUpdatesNetworkManager;

QThread* extractWorkerThread;

std::optional<vpkedit::VPK> vpk;
bool modified;

void freezeActions(bool freeze, bool freezeCreationActions = true);

bool loadVPK(const QString& path);

void checkForUpdatesReply(QNetworkReply* reply);

void writeEntryToFile(const QString& path, const vpkedit::VPKEntry& entry);
};

class ExtractVPKWorker : public QObject {
Q_OBJECT;

public:
ExtractVPKWorker() = default;

void run(Window* window, const QString& saveDir, const std::function<bool(const QString&)>& predicate);

signals:
void progressUpdated(int value);
void taskFinished();
};

0 comments on commit 8692e48

Please sign in to comment.