diff --git a/CMakeLists.txt b/CMakeLists.txt index 763000fa2..61e526591 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -90,6 +90,7 @@ include(GetGitRevisionDescription) get_git_head_revision(GIT_REF_SPEC GIT_REV) git_describe(GIT_DESC --always --long --dirty) git_branch_name(GIT_BRANCH) +string(TIMESTAMP BUILD_DATE "%Y-%m-%d %H:%M:%S") configure_file("${CMAKE_CURRENT_SOURCE_DIR}/src/common/scm_rev.cpp.in" "${CMAKE_CURRENT_SOURCE_DIR}/src/common/scm_rev.cpp" @ONLY) diff --git a/src/common/config.cpp b/src/common/config.cpp index 893b1717e..6ff365596 100644 --- a/src/common/config.cpp +++ b/src/common/config.cpp @@ -17,6 +17,7 @@ static s32 gpuId = -1; // Vulkan physical device index. Set to negative for auto static std::string logFilter; static std::string logType = "async"; static std::string userName = "shadPS4"; +static std::string updateChannel = "unstable"; static bool useSpecialPad = false; static int specialPadClass = 1; static bool isDebugDump = false; @@ -87,6 +88,10 @@ std::string getUserName() { return userName; } +std::string getUpdateChannel() { + return updateChannel; +} + bool getUseSpecialPad() { return useSpecialPad; } @@ -235,6 +240,10 @@ void setUserName(const std::string& type) { userName = type; } +void setUpdateChannel(const std::string& type) { + updateChannel = type; +} + void setUseSpecialPad(bool use) { useSpecialPad = use; } @@ -373,6 +382,7 @@ void load(const std::filesystem::path& path) { logFilter = toml::find_or(general, "logFilter", ""); logType = toml::find_or(general, "logType", "sync"); userName = toml::find_or(general, "userName", "shadPS4"); + updateChannel = toml::find_or(general, "updateChannel", "unstable"); isShowSplash = toml::find_or(general, "showSplash", true); isAutoUpdate = toml::find_or(general, "autoUpdate", true); } @@ -466,6 +476,7 @@ void save(const std::filesystem::path& path) { data["General"]["logFilter"] = logFilter; data["General"]["logType"] = logType; data["General"]["userName"] = userName; + data["General"]["updateChannel"] = updateChannel; data["General"]["showSplash"] = isShowSplash; data["General"]["autoUpdate"] = isAutoUpdate; data["Input"]["useSpecialPad"] = useSpecialPad; @@ -518,6 +529,7 @@ void setDefaultValues() { logFilter = ""; logType = "async"; userName = "shadPS4"; + updateChannel = "unstable"; useSpecialPad = false; specialPadClass = 1; isDebugDump = false; diff --git a/src/common/config.h b/src/common/config.h index e7a0e075b..63dca08d7 100644 --- a/src/common/config.h +++ b/src/common/config.h @@ -16,6 +16,7 @@ bool isFullscreenMode(); std::string getLogFilter(); std::string getLogType(); std::string getUserName(); +std::string getUpdateChannel(); bool getUseSpecialPad(); int getSpecialPadClass(); @@ -49,6 +50,7 @@ void setFullscreenMode(bool enable); void setLanguage(u32 language); void setNeoMode(bool enable); void setUserName(const std::string& type); +void setUpdateChannel(const std::string& type); void setUseSpecialPad(bool use); void setSpecialPadClass(int type); diff --git a/src/common/scm_rev.cpp.in b/src/common/scm_rev.cpp.in index 7f6fba9ed..edcb3eedd 100644 --- a/src/common/scm_rev.cpp.in +++ b/src/common/scm_rev.cpp.in @@ -6,12 +6,14 @@ #define GIT_REV "@GIT_REV@" #define GIT_BRANCH "@GIT_BRANCH@" #define GIT_DESC "@GIT_DESC@" +#define BUILD_DATE "@BUILD_DATE@" namespace Common { const char g_scm_rev[] = GIT_REV; const char g_scm_branch[] = GIT_BRANCH; const char g_scm_desc[] = GIT_DESC; +const char g_scm_date[] = BUILD_DATE; } // namespace diff --git a/src/common/scm_rev.h b/src/common/scm_rev.h index 877a01272..005099d2d 100644 --- a/src/common/scm_rev.h +++ b/src/common/scm_rev.h @@ -8,5 +8,6 @@ namespace Common { extern const char g_scm_rev[]; extern const char g_scm_branch[]; extern const char g_scm_desc[]; +extern const char g_scm_date[]; } // namespace Common diff --git a/src/emulator.cpp b/src/emulator.cpp index 5112ffd6a..a80a441d2 100644 --- a/src/emulator.cpp +++ b/src/emulator.cpp @@ -78,8 +78,8 @@ Emulator::Emulator() { } Emulator::~Emulator() { - const auto config_dir = Common::FS::GetUserPath(Common::FS::PathType::UserDir); - Config::save(config_dir / "config.toml"); + //const auto config_dir = Common::FS::GetUserPath(Common::FS::PathType::UserDir); + //Config::save(config_dir / "config.toml"); } void Emulator::Run(const std::filesystem::path& file) { diff --git a/src/qt_gui/checkUpdate.cpp b/src/qt_gui/checkUpdate.cpp index a132e538c..c837802b6 100644 --- a/src/qt_gui/checkUpdate.cpp +++ b/src/qt_gui/checkUpdate.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include @@ -14,115 +15,245 @@ #include #include #include +#include +#include #include #include +#include +#include +#include #include #include #include #include +#include #include "checkUpdate.h" -// #include "externals/minizip-ng/mz.h" -// #include "externals/minizip-ng/mz_strm.h" -// #include "externals/minizip-ng/mz_strm_buf.h" -// #include "externals/minizip-ng/mz_strm_mem.h" -// #include "externals/minizip-ng/mz_strm_os.h" -// #include "externals/minizip-ng/mz_zip.h" using namespace Common::FS; namespace fs = std::filesystem; -CheckUpdate::CheckUpdate(QWidget* parent) : QDialog(parent) { - setWindowTitle("Update Check"); - CheckForUpdates(); +CheckUpdate::CheckUpdate(const bool showMessage, QWidget* parent) + : QDialog(parent), networkManager(new QNetworkAccessManager(this)) { + setWindowTitle(tr("Auto Updater")); + setFixedSize(420, 380); + CheckForUpdates(showMessage); } -void CheckUpdate::CheckForUpdates() { - QNetworkAccessManager* manager = new QNetworkAccessManager(this); - QNetworkRequest request( - QUrl("https://api.github.com/repos/DanielSvoboda/teste/releases/latest")); - QNetworkReply* reply = manager->get(request); +CheckUpdate::~CheckUpdate() {} - connect(reply, &QNetworkReply::finished, this, [this, reply]() { +void CheckUpdate::CheckForUpdates(const bool showMessage) { + QString updateChannel = QString::fromStdString(Config::getUpdateChannel()); + QUrl url; + + if (updateChannel == "unstable") { + url = QUrl("https://api.github.com/repos/DanielSvoboda/shadPS4/releases?per_page=1"); + } else if (updateChannel == "stable") { + url = QUrl("https://api.github.com/repos/shadps4-emu/shadPS4/releases/latest"); + } else { + QMessageBox::warning( + this, tr("Error"), + QString(tr("Invalid update channel:") + "\n%1\n" + + tr("In updateChannel in config.tml file must contain 'stable' or 'unstable'") + .arg(updateChannel))); + return; + } + + QNetworkRequest request(url); + QNetworkReply* reply = networkManager->get(request); + + connect(reply, &QNetworkReply::finished, this, [this, reply, showMessage, updateChannel]() { if (reply->error() != QNetworkReply::NoError) { QMessageBox::warning(this, tr("Error"), - QString(tr("Network error: ") + "\n%1").arg(reply->errorString())); + QString(tr("Network error:") + "\n%1").arg(reply->errorString())); reply->deleteLater(); return; } QByteArray response = reply->readAll(); QJsonDocument jsonDoc(QJsonDocument::fromJson(response)); - QJsonObject jsonObj = jsonDoc.object(); - QString downloadUrl = - jsonObj["assets"].toArray().first().toObject()["browser_download_url"].toString(); - QString latestVersion = QFileInfo(downloadUrl).baseName(); - QString latestRev = latestVersion.right(7); - - QString currentRev = QString::fromStdString(Common::g_scm_rev).left(7); - - if (latestRev == currentRev) { - setupUI_NoUpdate(); - } else { - updateDownloadUrl = downloadUrl; - setupUI_UpdateAvailable(downloadUrl); + if (jsonDoc.isNull()) { + QMessageBox::warning(this, tr("Error"), tr("Failed to parse update information.")); + reply->deleteLater(); + return; } + QString downloadUrl; + QString latestVersion; + QString latestRev; + QString latestDate; + + QJsonObject jsonObj; + if (jsonDoc.isArray()) { + QJsonArray jsonArray = jsonDoc.array(); + if (!jsonArray.isEmpty()) { + jsonObj = jsonArray.first().toObject(); + } else { + QMessageBox::warning(this, tr("Error"), tr("No releases found.")); + reply->deleteLater(); + return; + } + } else { + jsonObj = jsonDoc.object(); + } + + if (jsonObj.contains("tag_name")) { + latestVersion = jsonObj["tag_name"].toString(); + latestRev = latestVersion.right(7); + latestDate = jsonObj["published_at"].toString(); + } else { + QMessageBox::warning(this, tr("Error"), tr("Invalid release data.")); + reply->deleteLater(); + return; + } + + QString currentRev = QString::fromStdString(Common::g_scm_rev).left(7); + QString currentDate = Common::g_scm_date; + + QDateTime dateTime = QDateTime::fromString(latestDate, Qt::ISODate); + latestDate = dateTime.isValid() ? dateTime.toString("yyyy-MM-dd HH:mm:ss") : "Unknown date"; + + if (jsonObj.contains("assets")) { + QJsonArray assets = jsonObj["assets"].toArray(); + bool found = false; + for (const QJsonValue& assetValue : assets) { + QJsonObject assetObj = assetValue.toObject(); + + QString platformString; +#ifdef Q_OS_WIN + platformString = "win64-qt"; +#elif defined(Q_OS_LINUX) + + QString executablePath = QCoreApplication::applicationDirPath(); + QFileInfo fileInfo(executablePath); + + if (QProcessEnvironment::systemEnvironment().contains("APPIMAGE")) { + platformString = "linux-qt"; + } else { + platformString = "ubuntu64"; + } +#elif defined(Q_OS_MAC) + platformString = "macos-qt"; +#endif + if (assetObj["name"].toString().contains(platformString)) { + downloadUrl = assetObj["browser_download_url"].toString(); + found = true; + break; + } + } + if (!found) { + QMessageBox::warning(this, tr("Error"), + tr("No download URL found for the specified asset.")); + } + } else { + QMessageBox::warning(this, tr("Error"), tr("No assets found in the response.")); + } + + if (latestRev == currentRev && showMessage) { + QMessageBox::information(this, tr("Auto Updater"), + tr("Your version is already up to date!")); + } else { + QString compareUrlString = + QString("https://api.github.com/repos/DanielSvoboda/shadPS4/compare/%1...%2") + .arg(currentRev) + .arg(latestRev); + + QUrl compareUrl(compareUrlString); + QNetworkRequest compareRequest(compareUrl); + QNetworkReply* compareReply = networkManager->get(compareRequest); + + connect(compareReply, &QNetworkReply::finished, this, + [this, compareReply, downloadUrl, latestDate, latestRev, currentDate, + currentRev]() { + if (compareReply->error() != QNetworkReply::NoError) { + QMessageBox::warning(this, tr("Error"), + QString(tr("Network error:") + "\n%1") + .arg(compareReply->errorString())); + compareReply->deleteLater(); + return; + } + + QByteArray compareResponse = compareReply->readAll(); + QJsonDocument compareJsonDoc(QJsonDocument::fromJson(compareResponse)); + QJsonObject compareJsonObj = compareJsonDoc.object(); + QJsonArray commits = compareJsonObj["commits"].toArray(); + + QString changes; + for (const QJsonValue& commitValue : commits) { + QJsonObject commitObj = commitValue.toObject(); + QString message = commitObj["commit"].toObject()["message"].toString(); + + // Remove texts after the first line break, if any + int newlineIndex = message.indexOf('\n'); + if (newlineIndex != -1) { + message = message.left(newlineIndex); + } + if (!changes.isEmpty()) { + changes += "
"; + } + changes += "    • " + message; + } + setupUI_UpdateAvailable(downloadUrl, latestDate, latestRev, currentDate, + currentRev, + "

" + tr("Changes") + ":

" + changes); + compareReply->deleteLater(); + }); + } reply->deleteLater(); }); } -void CheckUpdate::setupUI_NoUpdate() { +void CheckUpdate::setupUI_UpdateAvailable(const QString& downloadUrl, const QString& latestDate, + const QString& latestRev, const QString& currentDate, + const QString& currentRev, const QString& textChangeLog) { QVBoxLayout* layout = new QVBoxLayout(this); + QHBoxLayout* titleLayout = new QHBoxLayout(); - QLabel* noUpdateLabel = new QLabel("Your version is already up to date!.", this); - layout->addWidget(noUpdateLabel); + QLabel* imageLabel = new QLabel(this); + QPixmap pixmap(":/images/shadps4.ico"); + imageLabel->setPixmap(pixmap); + imageLabel->setScaledContents(true); + imageLabel->setFixedSize(40, 40); - autoUpdateCheckBox = new QCheckBox("Auto Update", this); - layout->addWidget(autoUpdateCheckBox); + QLabel* titleLabel = new QLabel("

" + tr("Update Available") + "

", this); + titleLayout->addWidget(imageLabel); + titleLayout->addWidget(titleLabel); + layout->addLayout(titleLayout); - autoUpdateCheckBox->setChecked(Config::autoUpdate()); - - connect(autoUpdateCheckBox, &QCheckBox::stateChanged, this, [](int state) { - const auto user_dir = Common::FS::GetUserPath(Common::FS::PathType::UserDir); - Config::setAutoUpdate(state == Qt::Checked); - Config::save(user_dir / "config.toml"); - }); - - setLayout(layout); -} - -void CheckUpdate::setupUI_UpdateAvailable(const QString& downloadUrl) { - QVBoxLayout* layout = new QVBoxLayout(this); - - QLabel* updateLabel = - new QLabel("An update is available!\nDo you want to download and install it?", this); - updateLabel->setAlignment(Qt::AlignCenter); + QString updateText = QString("


Current Version:
%1 (%2)
" + "Latest Version: %3 (%4)

" + "

Do you want to update?

") + .arg(currentRev, currentDate, latestRev, latestDate); + QLabel* updateLabel = new QLabel(updateText, this); layout->addWidget(updateLabel); - // Criar um layout horizontal para os botões - QHBoxLayout* buttonLayout = new QHBoxLayout(); + QTextEdit* textField = new QTextEdit(this); + textField->setText(textChangeLog); + textField->setReadOnly(true); + textField->setFixedWidth(400); + textField->setFixedHeight(200); + layout->addWidget(textField); - yesButton = new QPushButton("Yes", this); + QHBoxLayout* bottomLayout = new QHBoxLayout(); + autoUpdateCheckBox = new QCheckBox(tr("Auto Update (Check at Startup)"), this); + yesButton = new QPushButton("Update", this); noButton = new QPushButton("No", this); + yesButton->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Preferred); + noButton->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Preferred); + bottomLayout->addWidget(autoUpdateCheckBox); - // Adicionar os botões ao layout horizontal - buttonLayout->addWidget(yesButton); - buttonLayout->addWidget(noButton); + QSpacerItem* spacer = new QSpacerItem(40, 20, QSizePolicy::Expanding, QSizePolicy::Minimum); + bottomLayout->addItem(spacer); - // Adicionar o layout horizontal ao layout principal - layout->addLayout(buttonLayout); + bottomLayout->addWidget(yesButton); + bottomLayout->addWidget(noButton); + layout->addLayout(bottomLayout); connect(yesButton, &QPushButton::clicked, this, - [this]() { DownloadAndInstallUpdate(updateDownloadUrl); }); - + [this, downloadUrl]() { DownloadAndInstallUpdate(downloadUrl); }); connect(noButton, &QPushButton::clicked, this, [this]() { close(); }); - autoUpdateCheckBox = new QCheckBox("Auto Update (Check at Startup)", this); - layout->addWidget(autoUpdateCheckBox); - autoUpdateCheckBox->setChecked(Config::autoUpdate()); - connect(autoUpdateCheckBox, &QCheckBox::stateChanged, this, [](int state) { const auto user_dir = Common::FS::GetUserPath(Common::FS::PathType::UserDir); Config::setAutoUpdate(state == Qt::Checked); @@ -133,14 +264,15 @@ void CheckUpdate::setupUI_UpdateAvailable(const QString& downloadUrl) { } void CheckUpdate::DownloadAndInstallUpdate(const QString& url) { - QNetworkAccessManager* manager = new QNetworkAccessManager(this); QNetworkRequest request(url); - QNetworkReply* reply = manager->get(request); + QNetworkReply* reply = networkManager->get(request); - connect(reply, &QNetworkReply::finished, this, [this, reply]() { + connect(reply, &QNetworkReply::finished, this, [this, reply, url]() { if (reply->error() != QNetworkReply::NoError) { QMessageBox::warning(this, tr("Error"), - QString(tr("Network error: ") + "\n%1").arg(reply->errorString())); + tr("Network error occurred while trying to access the URL:") + + "\n" + url + "\n" + tr("Error details:") + "\n" + + reply->errorString()); reply->deleteLater(); return; } @@ -159,12 +291,13 @@ void CheckUpdate::DownloadAndInstallUpdate(const QString& url) { file.write(reply->readAll()); file.close(); QMessageBox::information(this, tr("Download Complete"), - tr("The update has been downloaded. Starting installation.")); + tr("The update has been downloaded, press OK to install.")); Unzip(); + Install(); } else { QMessageBox::warning( this, tr("Error"), - QString(tr("Failed to save the update file at") + "\n%1").arg(downloadPath)); + QString(tr("Failed to save the update file at:") + "\n" + downloadPath)); } reply->deleteLater(); @@ -172,122 +305,281 @@ void CheckUpdate::DownloadAndInstallUpdate(const QString& url) { } void CheckUpdate::Unzip() { - // QString userPath = - // QString::fromStdString(Common::FS::GetUserPath(Common::FS::PathType::UserDir).string()); - // QString tempDirPath = userPath + "/temp_download_update"; - // QString zipFilePath = tempDirPath + "/temp_download_update.zip"; + QString userPath = + QString::fromStdString(Common::FS::GetUserPath(Common::FS::PathType::UserDir).string()); + QString tempDirPath = userPath + "/temp_download_update"; + QString zipFilePath = tempDirPath + "/temp_download_update.zip"; - // if (!fs::exists(zipFilePath.toStdString())) { - // QMessageBox::warning(this, tr("Error"), - // QString(tr("Arquivo zip não encontrado:") + - // "\n%1").arg(zipFilePath)); - // return; - // } + QFile zipFile(zipFilePath); + if (!zipFile.open(QIODevice::ReadOnly)) { + QMessageBox::warning(this, tr("Error"), + tr("Failed to open the ZIP file:") + "\n" + zipFilePath); + return; + } - // void* zip_reader = NULL; - // void* stream = NULL; + QByteArray zipData = zipFile.readAll(); + zipFile.close(); - // mz_stream_os_create(); - // if (mz_stream_os_open(stream, zipFilePath.toStdString().c_str(), MZ_OPEN_MODE_READ) != MZ_OK) - // { + const uint8_t* data = reinterpret_cast(zipData.constData()); + size_t size = zipData.size(); + size_t offset = 0; - // QMessageBox::warning(this, tr("Error"), - // QString(tr("Erro ao abrir o arquivo zip") + - // "\n%1").arg(zipFilePath)); - // mz_stream_os_delete(&stream); - // return; - //} +#pragma pack(push, 1) + struct ZipLocalFileHeader { + uint32_t signature; + uint16_t version; + uint16_t flags; + uint16_t method; + uint16_t time; + uint16_t date; + uint32_t crc32; + uint32_t compressedSize; + uint32_t uncompressedSize; + uint16_t filenameLength; + uint16_t extraFieldLength; + }; +#pragma pack(pop) - //// Criar o leitor do ZIP - // mz_zip_create(); - // if (mz_zip_open(zip_reader, stream, MZ_OPEN_MODE_READ) != MZ_OK) { + auto readLocalFileHeader = [&](const uint8_t* data, size_t offset, + ZipLocalFileHeader& header) -> bool { + memcpy(&header, data + offset, sizeof(header)); + return header.signature == 0x04034b50; + }; - // QMessageBox::warning( - // this, tr("Error"), - // QString(tr("Erro ao abrir o arquivo zip com mz_zip_open") + "\n%1").arg(zipFilePath)); + auto decompressData = [&](const std::vector& compressedData, + std::vector& decompressedData) -> bool { + zng_stream strm = {}; + strm.zalloc = Z_NULL; + strm.zfree = Z_NULL; + strm.opaque = Z_NULL; + strm.avail_in = compressedData.size(); + strm.next_in = reinterpret_cast(const_cast(compressedData.data())); - // mz_zip_delete(&zip_reader); - // mz_stream_os_delete(&stream); - // return; - //} + if (zng_inflateInit2(&strm, -MAX_WBITS) != Z_OK) { + return false; + } - //// Passa por (arquivo ou diretório) no arquivo zip - // while (mz_zip_goto_next_entry(zip_reader) == MZ_OK) { - // mz_zip_file* file_info = nullptr; - // if (mz_zip_entry_get_info(zip_reader, &file_info) != MZ_OK) { - // QMessageBox::warning( - // this, tr("Error"), - // QString(tr("Erro ao obter informações da entrada no zip.") + "\n%1") - // .arg(zipFilePath)); - // continue; - // } + strm.avail_out = decompressedData.size(); + strm.next_out = decompressedData.data(); - // QString caminho_arquivo = tempDirPath + "/" + file_info->filename; + int result = zng_inflate(&strm, Z_NO_FLUSH); + if (result != Z_STREAM_END) { + zng_inflateEnd(&strm); + return false; + } - // // Verifique se a entrada do zip é um diretório - // if (mz_zip_entry_is_dir(zip_reader) == MZ_OK) { - // fs::create_directories(caminho_arquivo.toStdString()); - // QMessageBox::warning(this, tr("Error"), - // QString(tr("Diretório criado:") + "\n%1").arg(caminho_arquivo)); - // } else { - // // Certifique-se de que o diretório pai do arquivo exista - // fs::create_directories(fs::path(caminho_arquivo.toStdString()).parent_path()); + zng_inflateEnd(&strm); + return true; + }; - // // Abra o arquivo de saída para gravação - // std::ofstream arquivo_saida(caminho_arquivo.toStdString(), std::ios::binary); - // if (!arquivo_saida) { + while (offset < size) { + ZipLocalFileHeader header; + if (readLocalFileHeader(data, offset, header)) { + uint16_t fileNameLength = header.filenameLength; + std::string fileName(reinterpret_cast(data + offset + sizeof(header)), + fileNameLength); - // QMessageBox::warning( - // this, tr("Error"), - // QString(tr("Erro ao abrir o arquivo de saída:") + - // "\n%1").arg(caminho_arquivo)); + if (fileName.empty()) { + QMessageBox::warning(this, tr("Error"), + tr("File name is empty. Possibly corrupted ZIP.")); + break; + } - // mz_zip_entry_close(zip_reader); - // continue; - // } + offset += sizeof(header) + fileNameLength + header.extraFieldLength; - // // Buffer temporário para leitura dos dados - // char buffer[4096]; - // int32_t bytes_lidos; + size_t compressedDataOffset = offset; + size_t compressedDataSize = header.compressedSize; + size_t uncompressedSize = header.uncompressedSize; - // // Leia os dados do arquivo zip e escreva no arquivo de saída - // while ((bytes_lidos = mz_zip_entry_read(zip_reader, buffer, sizeof(buffer))) > 0) { - // arquivo_saida.write(buffer, bytes_lidos); - // } - // arquivo_saida.close(); - // QMessageBox::warning(this, tr("Error"), - // QString(tr("Arquivo extraído:") + "\n%1").arg(caminho_arquivo)); - // } - // mz_zip_entry_close(zip_reader); - //} + if (header.method == 0) { + // 0 = No need to decompress, just copy the data + std::vector decompressedData( + data + compressedDataOffset, data + compressedDataOffset + compressedDataSize); - //// Fechar o arquivo zip - // mz_zip_close(zip_reader); - // mz_zip_delete(&zip_reader); - // mz_stream_os_close(stream); - // mz_stream_os_delete(&stream); + QString filePath = QString::fromUtf8(fileName.c_str()); + QString fullPath = tempDirPath + "/" + filePath; - // QMessageBox::warning(this, tr("Error"), - // QString(tr("Descompactação concluída em:") + "\n%1").arg(tempDirPath)); + QFileInfo fileInfo(fullPath); + QString dirPath = fileInfo.path(); + + QDir dir(dirPath); + if (!dir.exists()) { + if (!dir.mkpath(dirPath)) { + QMessageBox::warning(this, tr("Error"), + tr("Failed to create directory:") + "\n" + dirPath); + continue; + } + } + + QFile outFile(fullPath); + outFile.write(reinterpret_cast(decompressedData.data()), + decompressedData.size()); + outFile.close(); + + offset += compressedDataSize; + } else if (header.method == 8) { + // 8 = Decompression Deflate + std::vector compressedData( + data + compressedDataOffset, data + compressedDataOffset + compressedDataSize); + std::vector decompressedData(uncompressedSize); + + if (!decompressData(compressedData, decompressedData)) { + QMessageBox::warning(this, tr("Error"), + tr("Error decompressing file") + "\n" + + QString::fromStdString(fileName)); + continue; + } + + QString filePath = QString::fromUtf8(fileName.c_str()); + QString fullPath = tempDirPath + "/" + filePath; + + QFileInfo fileInfo(fullPath); + QString dirPath = fileInfo.path(); + + QDir dir(dirPath); + if (!dir.exists()) { + if (!dir.mkpath(dirPath)) { + QMessageBox::warning(this, tr("Error"), + tr("Failed to create directory:") + "\n" + dirPath); + continue; + } + } + + QFile outFile(fullPath); + if (!outFile.open(QIODevice::WriteOnly)) { // remove this? + QMessageBox::warning(this, tr("Error"), + tr("Failed to open output file:") + "\n" + fullPath); + continue; + } + outFile.write(reinterpret_cast(decompressedData.data()), + decompressedData.size()); + outFile.close(); + + offset += compressedDataSize; + } else { + QMessageBox::warning(this, tr("Error"), + tr("Unsupported compression method for file:") + + header.method + "\n" + QString::fromStdString(fileName)); + break; + } +#if defined(Q_OS_MAC) + if (filePath == "shadps4-macos-qt.tar.gz") { + // Unpack the tar.gz file + QString tarGzFilePath = tempDirPath + "/" + filePath; + QString tarExtractDirPath = tempDirPath + "/tar_extracted"; + QDir tarExtractDir(tarExtractDirPath); + if (!tarExtractDir.exists()) { + if (!tarExtractDir.mkpath(tarExtractDirPath)) { + QMessageBox::warning(this, tr("Error"), + tr("Failed to create TAR extraction directory:") + + "\n" + tarExtractDirPath); + return; + } + } + + QString tarCommand = + QString("tar -xzf %1 -C %2").arg(tarGzFilePath, tarExtractDirPath); + QProcess tarProcess; + tarProcess.start(tarCommand); + tarProcess.waitForFinished(); + + // Check if tar was successful + if (tarProcess.exitStatus() != QProcess::NormalExit || tarProcess.exitCode() != 0) { + QMessageBox::warning(this, tr("Error"), + tr("Failed to extract the TAR file:") + "\n" + + tarProcess.errorString()); + return; + } + // Remove .tar.gz file after extraction + QFile::remove(tarGzFilePath); + } +#endif + } else { + offset++; + } + } } -//// Criar e executar o arquivo batch para atualizar -// QFile batFile(tempDirPath + "/update.bat"); -// if (batFile.open(QIODevice::WriteOnly)) { -// QTextStream out(&batFile); -// out << "@echo off\n"; -// out << "taskkill /IM shadps4.exe /F\n"; -// out << "move /Y \"" << tempDirPath << "\\*\" \"" << userPath << "\"\n"; -// out << "start \"\" \"" << userPath << "\\shadps4.exe\"\n"; -// batFile.close(); +void CheckUpdate::Install() { + QString userPath = + QString::fromStdString(Common::FS::GetUserPath(Common::FS::PathType::UserDir).string()); + QString tempDirPath = userPath + "/temp_download_update"; + QString rootPath = QString::fromStdString(std::filesystem::current_path().string()); -// if (!QProcess::startDetached(batFile.fileName())) { -// QMessageBox::warning( -// this, tr("Error"), -// tr("Failed to start the update batch file:\n%1").arg(batFile.fileName())); -// } -//} else { -// QMessageBox::warning( -// this, tr("Error"), -// tr("Failed to create the update batch file:\n%1").arg(batFile.fileName())); -//} + QString startingUpdate = tr("Iniciando atualização..."); + + QString scriptContent; + QString scriptFileName; + QStringList arguments; + QString processCommand; + +#ifdef Q_OS_WIN + // Windows Batch Script + scriptFileName = tempDirPath + "/update.bat"; + scriptContent = QStringLiteral("@echo off\n" + "chcp 65001\n" + "echo %1\n" + "timeout /t 3 /nobreak\n" + "xcopy /E /I /Y \"%2\\*\" \"%3\\\"\n" + "timeout /t 3 /nobreak\n" + "del /Q \"%3\\update.bat\"\n" + "del /Q \"%3\\temp_download_update.zip\"\n" + "start \"\" \"%3\\shadps4.exe\"\n" + "rmdir /S /Q \"%2\"\n"); + arguments << "/C" << scriptFileName; + processCommand = "cmd.exe"; + +#elif defined(Q_OS_LINUX) + // Linux Shell Script + scriptFileName = tempDirPath + "/update.sh"; + scriptContent = QStringLiteral("#!/bin/bash\n" + "echo \"%1\"\n" + "sleep 3\n" + "cp -r \"%2/\"* \"%3/\"\n" + "sleep 3\n" + "rm \"%3/update.sh\"\n" + "rm \"%3/Shadps4-qt.AppImage\"\n" + "chmod +x \"%3/Shadps4-qt.AppImage\"\n" + "cd \"%3\" && ./Shadps4-qt.AppImage\n" + "rm -r \"%2\"\n"); + arguments << scriptFileName; + processCommand = "bash"; + +#elif defined(Q_OS_MAC) + // macOS Shell Script + scriptFileName = tempDirPath + "/update.sh"; + scriptContent = QStringLiteral("#!/bin/bash\n" + "echo \"%1\"\n" + "sleep 3\n" + "tar -xzf \"%2/temp_download_update.tar.gz\" -C \"%3\"\n" + "sleep 3\n" + "rm \"%3/update.sh\"\n" + "chmod +x \"%3/shadps4.app/Contents/MacOS/shadps4\"\n" + "open \"%3/shadps4.app\"\n" + "rm -r \"%2\"\n"); + arguments << scriptFileName; + processCommand = "bash"; +#else + QMessageBox::warning(this, tr("Error"), tr("Unsupported operating system.")); + return; +#endif + + QFile scriptFile(scriptFileName); + if (scriptFile.open(QIODevice::WriteOnly | QIODevice::Text)) { + QTextStream out(&scriptFile); + out << scriptContent.arg(startingUpdate).arg(tempDirPath).arg(rootPath); + scriptFile.close(); + +// Make the script executable on Unix-like systems +#if defined(Q_OS_LINUX) || defined(Q_OS_MAC) + std::filesystem::permissions(scriptFileName, std::filesystem::perms::owner_exec); +#endif + QProcess::startDetached(processCommand, arguments); + + exit(EXIT_SUCCESS); + } else { + QMessageBox::warning( + this, tr("Error"), + QString(tr("Failed to create the update script file:") + "\n" + scriptFileName)); + } +} \ No newline at end of file diff --git a/src/qt_gui/checkUpdate.h b/src/qt_gui/checkUpdate.h index 88ffb02a8..71f085157 100644 --- a/src/qt_gui/checkUpdate.h +++ b/src/qt_gui/checkUpdate.h @@ -6,26 +6,33 @@ #include #include +#include +#include class CheckUpdate : public QDialog { Q_OBJECT public: - explicit CheckUpdate(QWidget* parent = nullptr); + explicit CheckUpdate(const bool showMessage, QWidget* parent = nullptr); + ~CheckUpdate(); private slots: - void CheckForUpdates(); + void CheckForUpdates(const bool showMessage); void DownloadAndInstallUpdate(const QString& url); void Unzip(); + void Install(); private: - void setupUI_NoUpdate(); - void setupUI_UpdateAvailable(const QString& downloadUrl); + void setupUI_UpdateAvailable(const QString& downloadUrl, const QString& latestDate, + const QString& latestRev, const QString& currentDate, + const QString& currentRev, const QString& textChangeLog); QCheckBox* autoUpdateCheckBox; QPushButton* yesButton; QPushButton* noButton; QString updateDownloadUrl; + + QNetworkAccessManager* networkManager; }; #endif // CHECKUPDATE_H diff --git a/src/qt_gui/main_window.cpp b/src/qt_gui/main_window.cpp index 571c87150..3c83bf680 100644 --- a/src/qt_gui/main_window.cpp +++ b/src/qt_gui/main_window.cpp @@ -24,8 +24,8 @@ MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent), ui(new Ui::MainWi MainWindow::~MainWindow() { SaveWindowState(); - const auto config_dir = Common::FS::GetUserPath(Common::FS::PathType::UserDir); - Config::save(config_dir / "config.toml"); + //const auto config_dir = Common::FS::GetUserPath(Common::FS::PathType::UserDir); + //Config::save(config_dir / "config.toml"); } bool MainWindow::Init() { @@ -169,7 +169,7 @@ void MainWindow::CheckUpdateMain(bool check) { return; } } - auto checkUpdate = new CheckUpdate(this); + auto checkUpdate = new CheckUpdate(false, this); checkUpdate->show(); #endif } diff --git a/src/qt_gui/main_window_ui.h b/src/qt_gui/main_window_ui.h index 17c3c019d..bac46d3f3 100644 --- a/src/qt_gui/main_window_ui.h +++ b/src/qt_gui/main_window_ui.h @@ -304,7 +304,7 @@ public: bootInstallPkgAct->setText( QCoreApplication::translate("MainWindow", "Install Packages (PKG)", nullptr)); bootGameAct->setText(QCoreApplication::translate("MainWindow", "Boot Game", nullptr)); - updaterAct->setText(QCoreApplication::translate("MainWindow", "Check Update", nullptr)); + updaterAct->setText(QCoreApplication::translate("MainWindow", "Check for Updates", nullptr)); aboutAct->setText(QCoreApplication::translate("MainWindow", "About shadPS4", nullptr)); configureAct->setText(QCoreApplication::translate("MainWindow", "Configure...", nullptr)); #if QT_CONFIG(tooltip) diff --git a/src/qt_gui/settings_dialog.cpp b/src/qt_gui/settings_dialog.cpp index 4206c4463..6c43737fe 100644 --- a/src/qt_gui/settings_dialog.cpp +++ b/src/qt_gui/settings_dialog.cpp @@ -1,9 +1,16 @@ // SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later +#include +#include #include #include +#include +#include +#include +#include +#include "checkUpdate.h" #include "main_window.h" #include "settings_dialog.h" #include "ui_settings_dialog.h" @@ -115,6 +122,63 @@ SettingsDialog::SettingsDialog(std::span physical_devices, QWidge connect(ui->logFilterLineEdit, &QLineEdit::textChanged, this, [](const QString& text) { Config::setLogFilter(text.toStdString()); }); + + // Create the "Update" section and add it to the existing QGroupBox + QGroupBox* updateGroupBox = new QGroupBox("Update"); + updateGroupBox->setMaximumWidth(265); + + QVBoxLayout* updateLayout = new QVBoxLayout(); + updateGroupBox->setLayout(updateLayout); + + auto autoUpdateCheckBox = new QCheckBox("Auto Update (Check at Startup)"); + QComboBox* updateComboBox = new QComboBox(); + QPushButton* updateButton = new QPushButton("Check for Updates"); + + // Add items to the combobox + updateComboBox->addItem("stable"); + updateComboBox->addItem("unstable"); + + // Add widgets to the layout + updateLayout->addWidget(autoUpdateCheckBox); + updateLayout->addWidget(updateComboBox); + updateLayout->addWidget(updateButton); + + // Add the "Update" section to the general settings layout + ui->generalTab->layout()->addWidget(updateGroupBox); + + // Connect the button signal to a slot + connect(updateButton, &QPushButton::clicked, this, []() { + auto checkUpdate = new CheckUpdate(true); + checkUpdate->show(); + }); + + // Initialize checkbox state + autoUpdateCheckBox->setChecked(Config::autoUpdate()); + + // Connect checkbox state change to save configuration + connect(autoUpdateCheckBox, &QCheckBox::stateChanged, this, [](int state) { + const auto user_dir = Common::FS::GetUserPath(Common::FS::PathType::UserDir); + Config::setAutoUpdate(state == Qt::Checked); + }); + + // Initialize combobox state + updateComboBox->setCurrentText(QString::fromStdString(Config::getUpdateChannel())); + + // Connect combobox state change to save configuration + connect(updateComboBox, &QComboBox::currentTextChanged, this, [](const QString& channel) { + // Converter QString para std::string + std::string channelStr = channel.toStdString(); + Config::setUpdateChannel(channelStr); + }); + + // Initialize checkbox state + autoUpdateCheckBox->setChecked(Config::autoUpdate()); + + // Connect checkbox state change to save configuration + connect(autoUpdateCheckBox, &QCheckBox::stateChanged, this, [](int state) { + const auto user_dir = Common::FS::GetUserPath(Common::FS::PathType::UserDir); + Config::setAutoUpdate(state == Qt::Checked); + }); } // GPU TAB