2023-02-13 19:38:23 +00:00
|
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
//
|
|
|
|
// EmulationStation Desktop Edition
|
|
|
|
// GuiThemeDownloader.cpp
|
|
|
|
//
|
|
|
|
// Theme downloader.
|
|
|
|
//
|
|
|
|
|
|
|
|
#include "guis/GuiThemeDownloader.h"
|
|
|
|
|
2023-03-21 18:01:44 +00:00
|
|
|
#include "EmulationStation.h"
|
2023-03-22 19:56:48 +00:00
|
|
|
#include "components/MenuComponent.h"
|
2023-02-13 19:38:23 +00:00
|
|
|
#include "resources/ResourceManager.h"
|
|
|
|
|
|
|
|
#include "rapidjson/document.h"
|
|
|
|
#include "rapidjson/error/en.h"
|
|
|
|
|
2023-03-21 18:01:44 +00:00
|
|
|
#define DEBUG_CLONING false
|
|
|
|
|
2023-02-13 19:38:23 +00:00
|
|
|
GuiThemeDownloader::GuiThemeDownloader()
|
|
|
|
: mRenderer {Renderer::getInstance()}
|
|
|
|
, mBackground {":/graphics/frame.svg"}
|
2023-03-29 17:08:22 +00:00
|
|
|
, mGrid {glm::ivec2 {8, 8}}
|
2023-03-26 18:49:44 +00:00
|
|
|
, mRepositoryError {RepositoryError::NO_REPO_ERROR}
|
2023-03-21 18:01:44 +00:00
|
|
|
, mFetching {false}
|
|
|
|
, mLatestThemesList {false}
|
2023-03-30 17:19:36 +00:00
|
|
|
, mFullscreenViewing {false}
|
|
|
|
, mFullscreenViewerIndex {0}
|
2023-02-13 19:38:23 +00:00
|
|
|
{
|
|
|
|
addChild(&mBackground);
|
|
|
|
addChild(&mGrid);
|
|
|
|
|
2023-03-29 17:08:22 +00:00
|
|
|
const float fontSizeSmall {mRenderer->getIsVerticalOrientation() ? FONT_SIZE_MINI :
|
|
|
|
FONT_SIZE_SMALL};
|
|
|
|
|
2023-02-13 19:38:23 +00:00
|
|
|
// Set up grid.
|
2023-03-29 17:08:22 +00:00
|
|
|
mGrid.setEntry(std::make_shared<GuiComponent>(), glm::ivec2 {0, 2}, false, false,
|
|
|
|
glm::ivec2 {1, 5}, GridFlags::BORDER_TOP | GridFlags::BORDER_BOTTOM);
|
|
|
|
|
2023-02-13 19:38:23 +00:00
|
|
|
mTitle = std::make_shared<TextComponent>("THEME DOWNLOADER", Font::get(FONT_SIZE_LARGE),
|
|
|
|
0x555555FF, ALIGN_CENTER);
|
2023-03-29 17:08:22 +00:00
|
|
|
mGrid.setEntry(mTitle, glm::ivec2 {0, 0}, false, true, glm::ivec2 {8, 2});
|
|
|
|
|
|
|
|
mVariantsLabel =
|
|
|
|
std::make_shared<TextComponent>("", Font::get(fontSizeSmall), 0x555555FF, ALIGN_LEFT);
|
|
|
|
mGrid.setEntry(mVariantsLabel, glm::ivec2 {1, 2}, false, true, glm::ivec2 {1, 1},
|
|
|
|
GridFlags::BORDER_TOP);
|
|
|
|
|
|
|
|
mColorSchemesLabel =
|
|
|
|
std::make_shared<TextComponent>("", Font::get(fontSizeSmall), 0x555555FF, ALIGN_LEFT);
|
|
|
|
mGrid.setEntry(mColorSchemesLabel, glm::ivec2 {1, 3}, false, true, glm::ivec2 {1, 1});
|
|
|
|
|
|
|
|
mAspectRatiosLabel =
|
|
|
|
std::make_shared<TextComponent>("", Font::get(fontSizeSmall), 0x555555FF, ALIGN_LEFT);
|
|
|
|
mGrid.setEntry(mAspectRatiosLabel, glm::ivec2 {3, 2}, false, true, glm::ivec2 {1, 1},
|
|
|
|
GridFlags::BORDER_TOP);
|
|
|
|
|
|
|
|
mFutureUseLabel =
|
|
|
|
std::make_shared<TextComponent>("", Font::get(fontSizeSmall), 0x555555FF, ALIGN_LEFT);
|
|
|
|
mGrid.setEntry(mFutureUseLabel, glm::ivec2 {3, 3}, false, true, glm::ivec2 {1, 1});
|
|
|
|
|
|
|
|
mGrid.setEntry(std::make_shared<GuiComponent>(), glm::ivec2 {5, 2}, false, false,
|
|
|
|
glm::ivec2 {1, 5}, GridFlags::BORDER_TOP | GridFlags::BORDER_BOTTOM);
|
|
|
|
|
|
|
|
mVariantCount = std::make_shared<TextComponent>("", Font::get(fontSizeSmall, FONT_PATH_LIGHT),
|
|
|
|
0x555555FF, ALIGN_LEFT);
|
|
|
|
mGrid.setEntry(mVariantCount, glm::ivec2 {2, 2}, false, true, glm::ivec2 {1, 1},
|
|
|
|
GridFlags::BORDER_TOP);
|
|
|
|
|
|
|
|
mColorSchemesCount = std::make_shared<TextComponent>(
|
|
|
|
"", Font::get(fontSizeSmall, FONT_PATH_LIGHT), 0x555555FF, ALIGN_LEFT);
|
|
|
|
mGrid.setEntry(mColorSchemesCount, glm::ivec2 {2, 3}, false, true, glm::ivec2 {1, 1});
|
|
|
|
|
|
|
|
mAspectRatiosCount = std::make_shared<TextComponent>(
|
|
|
|
"", Font::get(fontSizeSmall, FONT_PATH_LIGHT), 0x555555FF, ALIGN_LEFT);
|
|
|
|
mGrid.setEntry(mAspectRatiosCount, glm::ivec2 {4, 2}, false, true, glm::ivec2 {1, 1},
|
|
|
|
GridFlags::BORDER_TOP);
|
|
|
|
|
|
|
|
mFutureUseCount = std::make_shared<TextComponent>("", Font::get(fontSizeSmall, FONT_PATH_LIGHT),
|
|
|
|
0x555555FF, ALIGN_LEFT);
|
|
|
|
mGrid.setEntry(mFutureUseCount, glm::ivec2 {4, 3}, false, true, glm::ivec2 {1, 1});
|
|
|
|
|
|
|
|
mDownloadStatus = std::make_shared<TextComponent>("", Font::get(fontSizeSmall, FONT_PATH_BOLD),
|
|
|
|
0x555555FF, ALIGN_LEFT);
|
|
|
|
mGrid.setEntry(mDownloadStatus, glm::ivec2 {1, 4}, false, true, glm::ivec2 {2, 1});
|
|
|
|
|
|
|
|
mLocalChanges = std::make_shared<TextComponent>("", Font::get(fontSizeSmall, FONT_PATH_BOLD),
|
|
|
|
0x555555FF, ALIGN_LEFT);
|
|
|
|
mGrid.setEntry(mLocalChanges, glm::ivec2 {3, 4}, false, true, glm::ivec2 {2, 1});
|
|
|
|
|
|
|
|
mScreenshot = std::make_shared<ImageComponent>();
|
2023-03-30 17:19:36 +00:00
|
|
|
mScreenshot->setLinearInterpolation(true);
|
2023-03-29 17:08:22 +00:00
|
|
|
mGrid.setEntry(mScreenshot, glm::ivec2 {1, 5}, false, true, glm::ivec2 {4, 1});
|
|
|
|
|
|
|
|
mAuthor = std::make_shared<TextComponent>("", Font::get(FONT_SIZE_MINI, FONT_PATH_LIGHT),
|
|
|
|
0x555555FF, ALIGN_LEFT);
|
|
|
|
mGrid.setEntry(mAuthor, glm::ivec2 {1, 6}, false, true, glm::ivec2 {4, 1});
|
2023-02-13 19:38:23 +00:00
|
|
|
|
2023-03-22 19:56:48 +00:00
|
|
|
mList = std::make_shared<ComponentList>();
|
2023-03-29 17:08:22 +00:00
|
|
|
mGrid.setEntry(mList, glm::ivec2 {6, 2}, true, true, glm::ivec2 {2, 5},
|
|
|
|
GridFlags::BORDER_TOP | GridFlags::BORDER_LEFT | GridFlags::BORDER_BOTTOM);
|
|
|
|
|
|
|
|
// Set up scroll indicators.
|
|
|
|
mScrollUp = std::make_shared<ImageComponent>();
|
|
|
|
mScrollDown = std::make_shared<ImageComponent>();
|
|
|
|
|
|
|
|
mScrollUp->setResize(0.0f, mTitle->getFont()->getLetterHeight() / 2.0f);
|
|
|
|
mScrollUp->setOrigin(0.0f, -0.35f);
|
|
|
|
|
|
|
|
mScrollDown->setResize(0.0f, mTitle->getFont()->getLetterHeight() / 2.0f);
|
|
|
|
mScrollDown->setOrigin(0.0f, 0.35f);
|
|
|
|
|
|
|
|
mScrollIndicator = std::make_shared<ScrollIndicatorComponent>(mList, mScrollUp, mScrollDown);
|
|
|
|
|
|
|
|
mGrid.setEntry(mScrollUp, glm::ivec2 {7, 0}, false, false, glm::ivec2 {1, 1});
|
|
|
|
mGrid.setEntry(mScrollDown, glm::ivec2 {7, 1}, false, false, glm::ivec2 {1, 1});
|
2023-03-22 19:56:48 +00:00
|
|
|
|
|
|
|
std::vector<std::shared_ptr<ButtonComponent>> buttons;
|
2023-03-29 21:34:32 +00:00
|
|
|
buttons.push_back(std::make_shared<ButtonComponent>("CLOSE", "CLOSE", [&] { delete this; }));
|
2023-03-22 19:56:48 +00:00
|
|
|
mButtons = makeButtonGrid(buttons);
|
2023-03-29 17:08:22 +00:00
|
|
|
mGrid.setEntry(mButtons, glm::ivec2 {0, 7}, true, false, glm::ivec2 {8, 1},
|
|
|
|
GridFlags::BORDER_TOP);
|
|
|
|
|
|
|
|
// Limit the width of the GUI on ultrawide monitors. The 1.778 aspect ratio value is
|
|
|
|
// the 16:9 reference.
|
|
|
|
const float aspectValue {1.778f / Renderer::getScreenAspectRatio()};
|
|
|
|
const float width {glm::clamp(0.95f * aspectValue, 0.70f, 0.98f) * mRenderer->getScreenWidth()};
|
|
|
|
setSize(width,
|
|
|
|
mTitle->getSize().y +
|
|
|
|
(FONT_SIZE_MEDIUM * 1.5f * (mRenderer->getIsVerticalOrientation() ? 10.0f : 9.0f)) +
|
|
|
|
mButtons->getSize().y);
|
2023-02-13 19:38:23 +00:00
|
|
|
|
|
|
|
setPosition((mRenderer->getScreenWidth() - mSize.x) / 2.0f,
|
|
|
|
(mRenderer->getScreenHeight() - mSize.y) / 2.0f);
|
|
|
|
|
2023-03-21 18:01:44 +00:00
|
|
|
mBusyAnim.setSize(mSize);
|
2023-03-29 20:21:55 +00:00
|
|
|
mBusyAnim.setText("DOWNLOADING THEMES LIST 100%");
|
2023-03-21 18:01:44 +00:00
|
|
|
mBusyAnim.onSizeChanged();
|
|
|
|
|
2023-03-29 17:08:22 +00:00
|
|
|
mList->setCursorChangedCallback([this](CursorState state) {
|
|
|
|
if (state == CursorState::CURSOR_SCROLLING || state == CursorState::CURSOR_STOPPED)
|
|
|
|
updateInfoPane();
|
|
|
|
});
|
|
|
|
|
2023-03-30 17:19:36 +00:00
|
|
|
mViewerIndicatorLeft = std::make_shared<TextComponent>(
|
|
|
|
"\uf104", Font::get(FONT_SIZE_LARGE * 1.2f, FONT_PATH_BOLD), 0xCCCCCCFF, ALIGN_CENTER);
|
|
|
|
|
|
|
|
mViewerIndicatorRight = std::make_shared<TextComponent>(
|
|
|
|
"\uf105", Font::get(FONT_SIZE_LARGE * 1.2f, FONT_PATH_BOLD), 0xCCCCCCFF, ALIGN_CENTER);
|
|
|
|
|
2023-03-21 18:01:44 +00:00
|
|
|
git_libgit2_init();
|
|
|
|
|
|
|
|
// The promise/future mechanism is used as signaling for the thread to indicate that
|
|
|
|
// repository fetching has been completed.
|
|
|
|
std::promise<bool>().swap(mPromise);
|
|
|
|
mFuture = mPromise.get_future();
|
|
|
|
|
2023-03-29 17:08:22 +00:00
|
|
|
mThemeDirectory = Utils::FileSystem::getHomePath() + "/.emulationstation/themes/";
|
2023-03-27 19:29:37 +00:00
|
|
|
fetchThemesList();
|
2023-03-21 18:01:44 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
GuiThemeDownloader::~GuiThemeDownloader()
|
|
|
|
{
|
|
|
|
if (mFetchThread.joinable())
|
|
|
|
mFetchThread.join();
|
|
|
|
|
|
|
|
git_libgit2_shutdown();
|
|
|
|
}
|
|
|
|
|
2023-03-27 19:29:37 +00:00
|
|
|
bool GuiThemeDownloader::fetchRepository(const std::string& repositoryName,
|
|
|
|
const std::string& url,
|
2023-03-21 18:01:44 +00:00
|
|
|
bool allowReset)
|
|
|
|
{
|
|
|
|
int errorCode {0};
|
2023-03-29 17:08:22 +00:00
|
|
|
const std::string path {mThemeDirectory + repositoryName};
|
2023-03-26 18:49:44 +00:00
|
|
|
mRepositoryError = RepositoryError::NO_REPO_ERROR;
|
2023-03-29 20:21:55 +00:00
|
|
|
mMessage = "";
|
2023-03-23 19:44:58 +00:00
|
|
|
|
2023-03-27 19:29:37 +00:00
|
|
|
const bool isThemesList {repositoryName == "themes-list"};
|
|
|
|
git_repository* repository {nullptr};
|
|
|
|
git_remote* gitRemote {nullptr};
|
2023-03-23 19:44:58 +00:00
|
|
|
|
2023-03-27 19:29:37 +00:00
|
|
|
try {
|
|
|
|
mFetching = true;
|
|
|
|
errorCode = git_repository_open(&repository, &path[0]);
|
2023-03-21 18:01:44 +00:00
|
|
|
|
2023-03-27 19:29:37 +00:00
|
|
|
if (errorCode != 0) {
|
|
|
|
mRepositoryError = RepositoryError::NOT_A_REPOSITORY;
|
|
|
|
throw std::runtime_error("Couldn't open local repository, ");
|
|
|
|
}
|
|
|
|
errorCode = git_remote_lookup(&gitRemote, repository, "origin");
|
|
|
|
if (errorCode != 0) {
|
|
|
|
mRepositoryError = RepositoryError::INVALID_ORIGIN;
|
|
|
|
throw std::runtime_error("Couldn't get information about origin, ");
|
|
|
|
}
|
2023-03-21 18:01:44 +00:00
|
|
|
|
2023-03-22 19:56:48 +00:00
|
|
|
#if LIBGIT2_VER_MAJOR >= 1
|
2023-03-27 19:29:37 +00:00
|
|
|
git_fetch_options fetchOptions;
|
|
|
|
git_fetch_options_init(&fetchOptions, GIT_FETCH_OPTIONS_VERSION);
|
2023-03-22 19:56:48 +00:00
|
|
|
#else
|
2023-03-27 19:29:37 +00:00
|
|
|
git_fetch_options fetchOptions = GIT_FETCH_OPTIONS_INIT;
|
2023-03-21 18:01:44 +00:00
|
|
|
#endif
|
2023-03-27 19:29:37 +00:00
|
|
|
errorCode = git_remote_fetch(gitRemote, nullptr, &fetchOptions, nullptr);
|
2023-03-21 18:01:44 +00:00
|
|
|
|
2023-03-27 19:29:37 +00:00
|
|
|
if (errorCode != 0)
|
|
|
|
throw std::runtime_error("Couldn't pull latest commits, ");
|
|
|
|
|
|
|
|
git_annotated_commit* annotated {nullptr};
|
|
|
|
git_object* object {nullptr};
|
2023-03-21 18:01:44 +00:00
|
|
|
|
2023-03-27 19:29:37 +00:00
|
|
|
if (git_repository_head_detached(repository)) {
|
|
|
|
LOG(LogWarning) << "Repository \"" << repositoryName
|
|
|
|
<< "\" has HEAD detached, resetting it";
|
|
|
|
git_buf buffer {};
|
|
|
|
errorCode = git_remote_default_branch(&buffer, gitRemote);
|
|
|
|
if (errorCode == 0) {
|
|
|
|
git_reference* oldTargetRef;
|
|
|
|
git_repository_head(&oldTargetRef, repository);
|
|
|
|
|
|
|
|
const std::string branchName {buffer.ptr, buffer.size};
|
|
|
|
errorCode = git_revparse_single(&object, repository, branchName.c_str());
|
2023-03-22 19:56:48 +00:00
|
|
|
#if LIBGIT2_VER_MAJOR >= 1
|
2023-03-27 19:29:37 +00:00
|
|
|
git_checkout_options checkoutOptions;
|
|
|
|
git_checkout_options_init(&checkoutOptions, GIT_CHECKOUT_OPTIONS_VERSION);
|
2023-03-22 19:56:48 +00:00
|
|
|
#else
|
2023-03-27 19:29:37 +00:00
|
|
|
git_checkout_options checkoutOptions = GIT_CHECKOUT_OPTIONS_INIT;
|
2023-03-22 19:56:48 +00:00
|
|
|
#endif
|
2023-03-27 19:29:37 +00:00
|
|
|
checkoutOptions.checkout_strategy = GIT_CHECKOUT_SAFE;
|
|
|
|
errorCode = git_checkout_tree(repository, object, &checkoutOptions);
|
|
|
|
errorCode = git_repository_set_head(repository, branchName.c_str());
|
2023-03-21 18:01:44 +00:00
|
|
|
|
2023-03-27 19:29:37 +00:00
|
|
|
git_reference_free(oldTargetRef);
|
|
|
|
}
|
|
|
|
git_buf_dispose(&buffer);
|
|
|
|
}
|
2023-03-21 18:01:44 +00:00
|
|
|
|
2023-03-27 19:29:37 +00:00
|
|
|
errorCode = git_revparse_single(&object, repository, "FETCH_HEAD");
|
|
|
|
errorCode = git_annotated_commit_lookup(&annotated, repository, git_object_id(object));
|
2023-03-21 18:01:44 +00:00
|
|
|
|
2023-03-27 19:29:37 +00:00
|
|
|
git_merge_analysis_t mergeAnalysis {};
|
|
|
|
git_merge_preference_t mergePreference {};
|
|
|
|
|
|
|
|
errorCode = git_merge_analysis(&mergeAnalysis, &mergePreference, repository,
|
|
|
|
(const git_annotated_commit**)(&annotated), 1);
|
2023-03-21 18:01:44 +00:00
|
|
|
|
|
|
|
if (errorCode != 0) {
|
2023-03-27 19:29:37 +00:00
|
|
|
git_object_free(object);
|
|
|
|
git_annotated_commit_free(annotated);
|
|
|
|
throw std::runtime_error("Couldn't run Git merge analysis, ");
|
2023-03-21 18:01:44 +00:00
|
|
|
}
|
|
|
|
|
2023-03-27 19:29:37 +00:00
|
|
|
if (!(mergeAnalysis & GIT_MERGE_ANALYSIS_UP_TO_DATE) &&
|
|
|
|
!(mergeAnalysis & GIT_MERGE_ANALYSIS_FASTFORWARD)) {
|
|
|
|
if (allowReset) {
|
|
|
|
LOG(LogWarning) << "Repository \"" << repositoryName
|
|
|
|
<< "\" has diverged from origin, performing hard reset";
|
|
|
|
git_object* objectHead {nullptr};
|
|
|
|
errorCode = git_revparse_single(&objectHead, repository, "HEAD");
|
|
|
|
errorCode = git_reset(repository, objectHead, GIT_RESET_HARD, nullptr);
|
|
|
|
git_object_free(objectHead);
|
2023-03-23 19:44:58 +00:00
|
|
|
}
|
2023-03-27 19:29:37 +00:00
|
|
|
else {
|
|
|
|
LOG(LogWarning) << "Repository \"" << repositoryName
|
|
|
|
<< "\" has diverged from origin, can't fast-forward";
|
|
|
|
git_annotated_commit_free(annotated);
|
|
|
|
git_object_free(object);
|
|
|
|
mPromise.set_value(true);
|
|
|
|
mRepositoryError = RepositoryError::HAS_DIVERGED;
|
|
|
|
return true;
|
2023-03-23 19:44:58 +00:00
|
|
|
}
|
2023-03-27 19:29:37 +00:00
|
|
|
}
|
2023-03-21 18:01:44 +00:00
|
|
|
|
2023-03-29 20:36:15 +00:00
|
|
|
if (allowReset && checkLocalChanges(repository)) {
|
|
|
|
LOG(LogInfo) << "Repository \"" << repositoryName
|
|
|
|
<< "\" contains local changes, performing hard reset";
|
|
|
|
resetRepository(repository);
|
|
|
|
}
|
|
|
|
|
2023-03-27 19:29:37 +00:00
|
|
|
if (mergeAnalysis & GIT_MERGE_ANALYSIS_UP_TO_DATE) {
|
|
|
|
LOG(LogInfo) << "Repository \"" << repositoryName << "\" already up to date";
|
2023-03-29 20:21:55 +00:00
|
|
|
if (repositoryName != "themes-list")
|
|
|
|
mMessage = "THEME ALREADY UP TO DATE";
|
2023-03-27 19:29:37 +00:00
|
|
|
git_annotated_commit_free(annotated);
|
|
|
|
git_object_free(object);
|
|
|
|
mPromise.set_value(true);
|
|
|
|
if (isThemesList)
|
|
|
|
mLatestThemesList = true;
|
|
|
|
return false;
|
|
|
|
}
|
2023-03-21 18:01:44 +00:00
|
|
|
|
2023-03-27 19:29:37 +00:00
|
|
|
LOG(LogInfo) << "Performing Git fast-forward of repository \"" << repositoryName << "\"";
|
2023-03-21 18:01:44 +00:00
|
|
|
|
2023-03-27 19:29:37 +00:00
|
|
|
git_reference* oldTargetRef {nullptr};
|
|
|
|
git_repository_head(&oldTargetRef, repository);
|
2023-03-21 18:01:44 +00:00
|
|
|
|
2023-03-27 19:29:37 +00:00
|
|
|
const git_oid* objectID {nullptr};
|
|
|
|
objectID = git_annotated_commit_id(annotated);
|
2023-03-21 18:01:44 +00:00
|
|
|
|
2023-03-27 19:29:37 +00:00
|
|
|
git_object_lookup(&object, repository, objectID, GIT_OBJECT_COMMIT);
|
|
|
|
git_reference* newTargetRef {nullptr};
|
2023-03-21 18:01:44 +00:00
|
|
|
|
2023-03-22 19:56:48 +00:00
|
|
|
#if LIBGIT2_VER_MAJOR >= 1
|
2023-03-27 19:29:37 +00:00
|
|
|
git_checkout_options checkoutOptions;
|
|
|
|
git_checkout_options_init(&checkoutOptions, GIT_CHECKOUT_OPTIONS_VERSION);
|
2023-03-22 19:56:48 +00:00
|
|
|
#else
|
2023-03-27 19:29:37 +00:00
|
|
|
git_checkout_options checkoutOptions = GIT_CHECKOUT_OPTIONS_INIT;
|
2023-03-22 19:56:48 +00:00
|
|
|
#endif
|
2023-03-27 19:29:37 +00:00
|
|
|
checkoutOptions.checkout_strategy = GIT_CHECKOUT_SAFE;
|
2023-03-21 18:01:44 +00:00
|
|
|
|
2023-03-27 19:29:37 +00:00
|
|
|
git_checkout_tree(repository, object, &checkoutOptions);
|
|
|
|
errorCode = git_reference_set_target(&newTargetRef, oldTargetRef, objectID, nullptr);
|
2023-03-21 18:01:44 +00:00
|
|
|
|
2023-03-27 19:29:37 +00:00
|
|
|
git_reference_free(oldTargetRef);
|
|
|
|
git_reference_free(newTargetRef);
|
|
|
|
git_annotated_commit_free(annotated);
|
|
|
|
git_object_free(object);
|
2023-03-21 18:01:44 +00:00
|
|
|
|
2023-03-27 19:29:37 +00:00
|
|
|
if (errorCode != 0)
|
|
|
|
throw std::runtime_error("Couldn't fast-forward repository, ");
|
2023-03-21 18:01:44 +00:00
|
|
|
|
2023-03-27 19:29:37 +00:00
|
|
|
if (isThemesList)
|
|
|
|
mLatestThemesList = true;
|
2023-03-21 18:01:44 +00:00
|
|
|
|
2023-03-27 19:29:37 +00:00
|
|
|
if (gitRemote != nullptr)
|
|
|
|
git_remote_disconnect(gitRemote);
|
|
|
|
}
|
|
|
|
catch (std::runtime_error& runtimeError) {
|
|
|
|
const git_error* gitError {git_error_last()};
|
|
|
|
LOG(LogError) << "GuiThemeDownloader: " << runtimeError.what() << gitError->message;
|
2023-03-29 20:21:55 +00:00
|
|
|
mMessage = gitError->message;
|
2023-03-27 19:29:37 +00:00
|
|
|
git_error_clear();
|
|
|
|
if (gitRemote != nullptr)
|
|
|
|
git_remote_disconnect(gitRemote);
|
|
|
|
mPromise.set_value(true);
|
|
|
|
return true;
|
|
|
|
}
|
2023-03-21 18:01:44 +00:00
|
|
|
|
2023-03-29 20:21:55 +00:00
|
|
|
if (repositoryName != "themes-list")
|
|
|
|
mMessage = "THEME HAS BEEN UPDATED";
|
|
|
|
|
2023-03-27 19:29:37 +00:00
|
|
|
mPromise.set_value(true);
|
|
|
|
return false;
|
|
|
|
}
|
2023-03-21 18:01:44 +00:00
|
|
|
|
2023-03-27 19:29:37 +00:00
|
|
|
bool GuiThemeDownloader::checkLocalChanges(git_repository* repository, bool hasFetched)
|
|
|
|
{
|
|
|
|
git_status_list* status {nullptr};
|
|
|
|
size_t statusEntryCount {0};
|
|
|
|
int errorCode {0};
|
2023-03-21 18:01:44 +00:00
|
|
|
|
2023-03-22 19:56:48 +00:00
|
|
|
#if LIBGIT2_VER_MAJOR >= 1
|
2023-03-27 19:29:37 +00:00
|
|
|
git_status_options statusOptions;
|
|
|
|
git_status_options_init(&statusOptions, GIT_STATUS_OPTIONS_VERSION);
|
2023-03-22 19:56:48 +00:00
|
|
|
#else
|
2023-03-27 19:29:37 +00:00
|
|
|
git_status_options statusOptions = GIT_STATUS_OPTIONS_INIT;
|
2023-03-22 19:56:48 +00:00
|
|
|
#endif
|
2023-03-27 19:29:37 +00:00
|
|
|
// We don't include untracked files (GIT_STATUS_OPT_INCLUDE_UNTRACKED) as this makes
|
|
|
|
// it possible to add custom files to the repository without overwriting these when
|
|
|
|
// pulling theme updates.
|
|
|
|
statusOptions.show = GIT_STATUS_SHOW_INDEX_AND_WORKDIR;
|
|
|
|
statusOptions.flags =
|
|
|
|
GIT_STATUS_OPT_RENAMES_HEAD_TO_INDEX | GIT_STATUS_OPT_SORT_CASE_SENSITIVELY;
|
2023-03-21 18:01:44 +00:00
|
|
|
|
2023-03-27 19:29:37 +00:00
|
|
|
errorCode = git_status_list_new(&status, repository, &statusOptions);
|
|
|
|
if (errorCode == 0)
|
|
|
|
statusEntryCount = git_status_list_entrycount(status);
|
2023-03-21 18:01:44 +00:00
|
|
|
|
2023-03-27 19:29:37 +00:00
|
|
|
git_status_list_free(status);
|
|
|
|
// TODO: Also check if there are any local commits not on origin.
|
2023-03-21 18:01:44 +00:00
|
|
|
|
2023-03-27 19:29:37 +00:00
|
|
|
return (statusEntryCount != 0);
|
|
|
|
}
|
2023-03-21 18:01:44 +00:00
|
|
|
|
2023-03-27 19:29:37 +00:00
|
|
|
void GuiThemeDownloader::resetRepository(git_repository* repository)
|
|
|
|
{
|
|
|
|
git_object* objectHead {nullptr};
|
|
|
|
if (git_revparse_single(&objectHead, repository, "HEAD") == 0)
|
|
|
|
git_reset(repository, objectHead, GIT_RESET_HARD, nullptr);
|
|
|
|
git_object_free(objectHead);
|
|
|
|
}
|
2023-03-21 18:01:44 +00:00
|
|
|
|
2023-03-27 19:29:37 +00:00
|
|
|
void GuiThemeDownloader::makeInventory()
|
|
|
|
{
|
|
|
|
for (auto& theme : mThemeSets) {
|
2023-03-29 17:08:22 +00:00
|
|
|
const std::string path {mThemeDirectory + theme.reponame};
|
2023-03-27 19:29:37 +00:00
|
|
|
theme.invalidRepository = false;
|
|
|
|
theme.manuallyDownloaded = false;
|
|
|
|
theme.hasLocalChanges = false;
|
|
|
|
theme.isCloned = false;
|
|
|
|
|
|
|
|
if (Utils::FileSystem::exists(path + "-main")) {
|
|
|
|
theme.manuallyDownloaded = true;
|
|
|
|
theme.manualExtension = "-main";
|
|
|
|
}
|
|
|
|
else if (Utils::FileSystem::exists(path + "-master")) {
|
|
|
|
theme.manuallyDownloaded = true;
|
|
|
|
theme.manualExtension = "-master";
|
|
|
|
}
|
2023-03-21 18:01:44 +00:00
|
|
|
|
2023-03-27 19:29:37 +00:00
|
|
|
if (Utils::FileSystem::exists(path)) {
|
|
|
|
git_repository* repository {nullptr};
|
|
|
|
int errorCode {0};
|
2023-03-21 18:01:44 +00:00
|
|
|
|
2023-03-27 19:29:37 +00:00
|
|
|
errorCode = git_repository_open(&repository, &path[0]);
|
|
|
|
if (errorCode != 0) {
|
|
|
|
theme.invalidRepository = true;
|
|
|
|
git_repository_free(repository);
|
|
|
|
continue;
|
|
|
|
}
|
2023-03-21 18:01:44 +00:00
|
|
|
|
2023-03-27 19:29:37 +00:00
|
|
|
theme.isCloned = true;
|
2023-03-21 18:01:44 +00:00
|
|
|
|
2023-03-27 19:29:37 +00:00
|
|
|
if (checkLocalChanges(repository))
|
|
|
|
theme.hasLocalChanges = true;
|
2023-03-21 18:01:44 +00:00
|
|
|
|
2023-03-27 19:29:37 +00:00
|
|
|
git_repository_free(repository);
|
2023-03-21 18:01:44 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
bool GuiThemeDownloader::renameDirectory(const std::string& path)
|
|
|
|
{
|
|
|
|
LOG(LogInfo) << "Renaming directory " << path;
|
|
|
|
int index {1};
|
|
|
|
|
|
|
|
if (!Utils::FileSystem::exists(path + "_DISABLED"))
|
|
|
|
return Utils::FileSystem::renameFile(path, path + "_DISABLED", false);
|
|
|
|
|
|
|
|
// This will hopefully never be needed as it should only occur if a theme has been downloaded
|
|
|
|
// manually multiple times and the theme downloader has been ran multiple times as well.
|
|
|
|
for (; index < 10; ++index) {
|
|
|
|
if (!Utils::FileSystem::exists(path + "_" + std::to_string(index) + "_DISABLED"))
|
|
|
|
return Utils::FileSystem::renameFile(
|
|
|
|
path, path + "_" + std::to_string(index) + "_DISABLED", false);
|
|
|
|
}
|
|
|
|
|
|
|
|
return true;
|
2023-02-13 19:38:23 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
void GuiThemeDownloader::parseThemesList()
|
|
|
|
{
|
2023-03-21 18:01:44 +00:00
|
|
|
// Temporary location for testing purposes.
|
|
|
|
// const std::string themesFile {Utils::FileSystem::getHomePath() +
|
|
|
|
// "/.emulationstation/themes.json"};
|
|
|
|
|
2023-03-29 17:08:22 +00:00
|
|
|
const std::string themesFile {mThemeDirectory + "themes-list/themes.json"};
|
2023-02-13 19:38:23 +00:00
|
|
|
|
|
|
|
if (!Utils::FileSystem::exists(themesFile)) {
|
|
|
|
LOG(LogInfo) << "GuiThemeDownloader: No themes.json file found";
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
const ResourceData& themesFileData {ResourceManager::getInstance().getFileData(themesFile)};
|
|
|
|
rapidjson::Document doc;
|
|
|
|
doc.Parse(reinterpret_cast<const char*>(themesFileData.ptr.get()), themesFileData.length);
|
|
|
|
|
|
|
|
if (doc.HasParseError()) {
|
|
|
|
LOG(LogWarning) << "GuiThemeDownloader: Couldn't parse the themes.json file";
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2023-03-21 18:01:44 +00:00
|
|
|
if (doc.HasMember("latestStableRelease") && doc["latestStableRelease"].IsString()) {
|
|
|
|
const int latestStableRelease {std::stoi(doc["latestStableRelease"].GetString())};
|
|
|
|
if (latestStableRelease > PROGRAM_RELEASE_NUMBER) {
|
|
|
|
LOG(LogWarning) << "Not running the most current application release, theme "
|
|
|
|
"downloading is not recommended";
|
|
|
|
mWindow->pushGui(new GuiMsgBox(
|
|
|
|
getHelpStyle(),
|
|
|
|
"IT SEEMS AS IF YOU'RE NOT RUNNING THE LATEST ES-DE RELEASE, PLEASE UPGRADE BEFORE "
|
|
|
|
"PROCEEDING AS THESE THEMES MAY NOT BE COMPATIBLE WITH YOUR VERSION",
|
|
|
|
"OK", [] { return; }, "", nullptr, "", nullptr, true));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-02-27 17:06:35 +00:00
|
|
|
if (doc.HasMember("themeSets") && doc["themeSets"].IsArray()) {
|
|
|
|
const rapidjson::Value& themeSets {doc["themeSets"]};
|
|
|
|
for (int i {0}; i < static_cast<int>(themeSets.Size()); ++i) {
|
|
|
|
ThemeEntry themeEntry;
|
|
|
|
const rapidjson::Value& theme {themeSets[i]};
|
2023-02-13 19:38:23 +00:00
|
|
|
|
2023-02-27 17:06:35 +00:00
|
|
|
if (theme.HasMember("name") && theme["name"].IsString())
|
|
|
|
themeEntry.name = theme["name"].GetString();
|
2023-02-13 19:38:23 +00:00
|
|
|
|
2023-02-27 17:06:35 +00:00
|
|
|
if (theme.HasMember("reponame") && theme["reponame"].IsString())
|
|
|
|
themeEntry.reponame = theme["reponame"].GetString();
|
2023-02-13 19:38:23 +00:00
|
|
|
|
2023-02-27 17:06:35 +00:00
|
|
|
if (theme.HasMember("url") && theme["url"].IsString())
|
|
|
|
themeEntry.url = theme["url"].GetString();
|
2023-02-13 19:38:23 +00:00
|
|
|
|
2023-03-29 17:08:22 +00:00
|
|
|
if (theme.HasMember("author") && theme["author"].IsString())
|
|
|
|
themeEntry.author = theme["author"].GetString();
|
|
|
|
|
|
|
|
if (theme.HasMember("newEntry") && theme["newEntry"].IsBool())
|
|
|
|
themeEntry.newEntry = theme["newEntry"].GetBool();
|
|
|
|
|
2023-02-27 17:06:35 +00:00
|
|
|
if (theme.HasMember("variants") && theme["variants"].IsArray()) {
|
|
|
|
const rapidjson::Value& variants {theme["variants"]};
|
|
|
|
for (int i {0}; i < static_cast<int>(variants.Size()); ++i)
|
|
|
|
themeEntry.variants.emplace_back(variants[i].GetString());
|
|
|
|
}
|
2023-02-13 19:38:23 +00:00
|
|
|
|
2023-02-27 17:06:35 +00:00
|
|
|
if (theme.HasMember("colorSchemes") && theme["colorSchemes"].IsArray()) {
|
|
|
|
const rapidjson::Value& colorSchemes {theme["colorSchemes"]};
|
|
|
|
for (int i {0}; i < static_cast<int>(colorSchemes.Size()); ++i)
|
|
|
|
themeEntry.colorSchemes.emplace_back(colorSchemes[i].GetString());
|
|
|
|
}
|
2023-02-13 19:38:23 +00:00
|
|
|
|
2023-02-27 17:06:35 +00:00
|
|
|
if (theme.HasMember("aspectRatios") && theme["aspectRatios"].IsArray()) {
|
|
|
|
const rapidjson::Value& aspectRatios {theme["aspectRatios"]};
|
|
|
|
for (int i {0}; i < static_cast<int>(aspectRatios.Size()); ++i)
|
|
|
|
themeEntry.aspectRatios.emplace_back(aspectRatios[i].GetString());
|
|
|
|
}
|
2023-02-13 19:38:23 +00:00
|
|
|
|
2023-02-27 17:06:35 +00:00
|
|
|
if (theme.HasMember("transitions") && theme["transitions"].IsArray()) {
|
|
|
|
const rapidjson::Value& transitions {theme["transitions"]};
|
|
|
|
for (int i {0}; i < static_cast<int>(transitions.Size()); ++i)
|
|
|
|
themeEntry.transitions.emplace_back(transitions[i].GetString());
|
|
|
|
}
|
2023-02-13 19:38:23 +00:00
|
|
|
|
2023-02-27 17:06:35 +00:00
|
|
|
if (theme.HasMember("screenshots") && theme["screenshots"].IsArray()) {
|
|
|
|
const rapidjson::Value& screenshots {theme["screenshots"]};
|
|
|
|
for (int i {0}; i < static_cast<int>(screenshots.Size()); ++i) {
|
|
|
|
Screenshot screenshotEntry;
|
|
|
|
if (screenshots[i].HasMember("image") && screenshots[i]["image"].IsString())
|
|
|
|
screenshotEntry.image = screenshots[i]["image"].GetString();
|
2023-02-13 19:38:23 +00:00
|
|
|
|
2023-02-27 17:06:35 +00:00
|
|
|
if (screenshots[i].HasMember("caption") && screenshots[i]["caption"].IsString())
|
|
|
|
screenshotEntry.caption = screenshots[i]["caption"].GetString();
|
2023-02-13 19:38:23 +00:00
|
|
|
|
2023-02-27 17:06:35 +00:00
|
|
|
if (screenshotEntry.image != "" && screenshotEntry.caption != "")
|
|
|
|
themeEntry.screenshots.emplace_back(screenshotEntry);
|
2023-02-13 19:38:23 +00:00
|
|
|
}
|
|
|
|
}
|
2023-02-27 17:06:35 +00:00
|
|
|
|
|
|
|
mThemeSets.emplace_back(themeEntry);
|
2023-02-13 19:38:23 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-03-29 21:34:32 +00:00
|
|
|
LOG(LogDebug) << "GuiThemeDownloader::parseThemesList(): Parsed " << mThemeSets.size()
|
|
|
|
<< " theme sets";
|
2023-03-22 19:56:48 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
void GuiThemeDownloader::populateGUI()
|
|
|
|
{
|
2023-03-29 17:08:22 +00:00
|
|
|
if (mThemeSets.empty())
|
|
|
|
return;
|
|
|
|
|
2023-03-22 19:56:48 +00:00
|
|
|
for (auto& theme : mThemeSets) {
|
2023-03-27 19:29:37 +00:00
|
|
|
std::string themeName {Utils::String::toUpper(theme.name)};
|
2023-03-29 21:34:32 +00:00
|
|
|
if (theme.newEntry && !theme.isCloned)
|
|
|
|
themeName.append(" ").append(ViewController::BRANCH_CHAR);
|
2023-03-29 17:08:22 +00:00
|
|
|
if (theme.isCloned)
|
|
|
|
themeName.append(" ").append(ViewController::TICKMARK_CHAR);
|
2023-03-27 19:29:37 +00:00
|
|
|
if (theme.manuallyDownloaded || theme.invalidRepository)
|
2023-03-29 17:08:22 +00:00
|
|
|
themeName.append(" ").append(ViewController::CROSSEDCIRCLE_CHAR);
|
|
|
|
if (theme.hasLocalChanges)
|
|
|
|
themeName.append(" ").append(ViewController::EXCLAMATION_CHAR);
|
|
|
|
|
2023-03-22 19:56:48 +00:00
|
|
|
ComponentListRow row;
|
2023-03-27 19:29:37 +00:00
|
|
|
std::shared_ptr<TextComponent> themeNameElement {
|
|
|
|
std::make_shared<TextComponent>(themeName, Font::get(FONT_SIZE_MEDIUM), 0x777777FF)};
|
2023-03-22 19:56:48 +00:00
|
|
|
|
2023-03-27 19:29:37 +00:00
|
|
|
ThemeGUIEntry guiEntry;
|
|
|
|
guiEntry.themeName = themeNameElement;
|
|
|
|
mThemeGUIEntries.emplace_back(guiEntry);
|
|
|
|
row.addElement(themeNameElement, false);
|
2023-03-22 19:56:48 +00:00
|
|
|
|
2023-03-27 19:29:37 +00:00
|
|
|
row.makeAcceptInputHandler([this, &theme] {
|
2023-03-22 19:56:48 +00:00
|
|
|
std::promise<bool>().swap(mPromise);
|
2023-03-27 19:29:37 +00:00
|
|
|
if (theme.manuallyDownloaded || theme.invalidRepository) {
|
|
|
|
mWindow->pushGui(new GuiMsgBox(
|
|
|
|
getHelpStyle(),
|
|
|
|
"IT SEEMS AS IF THIS THEME HAS BEEN MANUALLY DOWNLOADED INSTEAD OF VIA "
|
|
|
|
"THIS THEME DOWNLOADER. A FRESH DOWNLOAD IS REQUIRED AND THE OLD THEME "
|
|
|
|
"DIRECTORY \"" +
|
|
|
|
theme.reponame + theme.manualExtension + "\" WILL BE RENAMED TO \"" +
|
|
|
|
theme.reponame + theme.manualExtension + "_DISABLED\"",
|
|
|
|
"PROCEED",
|
|
|
|
[this, theme] {
|
2023-03-29 17:08:22 +00:00
|
|
|
renameDirectory(mThemeDirectory + theme.reponame + theme.manualExtension);
|
2023-03-27 19:29:37 +00:00
|
|
|
std::promise<bool>().swap(mPromise);
|
|
|
|
mFuture = mPromise.get_future();
|
|
|
|
mFetchThread = std::thread(&GuiThemeDownloader::cloneRepository, this,
|
|
|
|
theme.reponame, theme.url);
|
2023-03-29 20:21:55 +00:00
|
|
|
mStatusType = StatusType::STATUS_DOWNLOADING;
|
|
|
|
mStatusText = "DOWNLOADING THEME";
|
2023-03-27 19:29:37 +00:00
|
|
|
},
|
|
|
|
"ABORT", [] { return; }, "", nullptr, true, true,
|
|
|
|
(mRenderer->getIsVerticalOrientation() ?
|
|
|
|
0.75f :
|
|
|
|
0.45f * (1.778f / mRenderer->getScreenAspectRatio()))));
|
|
|
|
}
|
|
|
|
else if (theme.hasLocalChanges) {
|
|
|
|
mWindow->pushGui(new GuiMsgBox(
|
|
|
|
getHelpStyle(),
|
|
|
|
"THEME REPOSITORY \"" + theme.reponame +
|
|
|
|
"\" CONTAINS LOCAL CHANGES. PROCEED TO OVERWRITE YOUR CHANGES "
|
|
|
|
"OR ABORT TO SKIP ALL UPDATES FOR THIS THEME",
|
|
|
|
"PROCEED",
|
|
|
|
[this, theme] {
|
|
|
|
std::promise<bool>().swap(mPromise);
|
|
|
|
mFuture = mPromise.get_future();
|
|
|
|
mFetchThread = std::thread(&GuiThemeDownloader::fetchRepository, this,
|
|
|
|
theme.reponame, theme.url, true);
|
2023-03-29 20:21:55 +00:00
|
|
|
mStatusType = StatusType::STATUS_UPDATING;
|
|
|
|
mStatusText = "UPDATING THEME";
|
2023-03-27 19:29:37 +00:00
|
|
|
},
|
|
|
|
"ABORT", [] { return; }, "", nullptr, true, true,
|
|
|
|
(mRenderer->getIsVerticalOrientation() ?
|
|
|
|
0.75f :
|
|
|
|
0.45f * (1.778f / mRenderer->getScreenAspectRatio()))));
|
|
|
|
}
|
|
|
|
else if (theme.isCloned) {
|
|
|
|
mFuture = mPromise.get_future();
|
|
|
|
mFetchThread = std::thread(&GuiThemeDownloader::fetchRepository, this,
|
|
|
|
theme.reponame, theme.url, false);
|
2023-03-29 20:21:55 +00:00
|
|
|
mStatusType = StatusType::STATUS_UPDATING;
|
|
|
|
mStatusText = "UPDATING THEME";
|
2023-03-27 19:29:37 +00:00
|
|
|
}
|
|
|
|
else {
|
|
|
|
mFuture = mPromise.get_future();
|
|
|
|
mFetchThread = std::thread(&GuiThemeDownloader::cloneRepository, this,
|
|
|
|
theme.reponame, theme.url);
|
2023-03-29 20:21:55 +00:00
|
|
|
mStatusType = StatusType::STATUS_DOWNLOADING;
|
|
|
|
mStatusText = "DOWNLOADING THEME";
|
2023-03-27 19:29:37 +00:00
|
|
|
}
|
2023-03-22 19:56:48 +00:00
|
|
|
});
|
|
|
|
mList->addRow(row);
|
|
|
|
}
|
2023-03-29 17:08:22 +00:00
|
|
|
|
|
|
|
mVariantsLabel->setText("VARIANTS:");
|
|
|
|
mColorSchemesLabel->setText(mRenderer->getIsVerticalOrientation() ? "COL. SCHEMES:" :
|
|
|
|
"COLOR SCHEMES:");
|
|
|
|
mAspectRatiosLabel->setText("ASPECT RATIOS:");
|
|
|
|
|
|
|
|
updateInfoPane();
|
2023-03-22 19:56:48 +00:00
|
|
|
updateHelpPrompts();
|
2023-02-13 19:38:23 +00:00
|
|
|
}
|
|
|
|
|
2023-03-27 19:29:37 +00:00
|
|
|
void GuiThemeDownloader::updateGUI()
|
|
|
|
{
|
2023-03-29 17:08:22 +00:00
|
|
|
updateInfoPane();
|
|
|
|
|
2023-03-27 19:29:37 +00:00
|
|
|
for (size_t i {0}; i < mThemeSets.size(); ++i) {
|
|
|
|
std::string themeName {Utils::String::toUpper(mThemeSets[i].name)};
|
2023-03-29 21:34:32 +00:00
|
|
|
if (mThemeSets[i].newEntry && !mThemeSets[i].isCloned)
|
|
|
|
themeName.append(" ").append(ViewController::BRANCH_CHAR);
|
2023-03-29 17:08:22 +00:00
|
|
|
if (mThemeSets[i].isCloned)
|
|
|
|
themeName.append(" ").append(ViewController::TICKMARK_CHAR);
|
2023-03-27 19:29:37 +00:00
|
|
|
if (mThemeSets[i].manuallyDownloaded || mThemeSets[i].invalidRepository)
|
2023-03-29 17:08:22 +00:00
|
|
|
themeName.append(" ").append(ViewController::CROSSEDCIRCLE_CHAR);
|
|
|
|
if (mThemeSets[i].hasLocalChanges)
|
|
|
|
themeName.append(" ").append(ViewController::EXCLAMATION_CHAR);
|
2023-03-27 19:29:37 +00:00
|
|
|
|
|
|
|
mThemeGUIEntries[i].themeName->setText(themeName);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-03-29 17:08:22 +00:00
|
|
|
void GuiThemeDownloader::updateInfoPane()
|
|
|
|
{
|
|
|
|
assert(static_cast<size_t>(mList->size()) == mThemeSets.size());
|
|
|
|
if (!mThemeSets[mList->getCursorId()].screenshots.empty())
|
|
|
|
mScreenshot->setImage(mThemeDirectory + "themes-list/" +
|
|
|
|
mThemeSets[mList->getCursorId()].screenshots.front().image);
|
|
|
|
|
|
|
|
if (mThemeSets[mList->getCursorId()].isCloned) {
|
|
|
|
mDownloadStatus->setText(ViewController::TICKMARK_CHAR + " INSTALLED");
|
|
|
|
mDownloadStatus->setColor(0x449944FF);
|
|
|
|
}
|
|
|
|
else if (mThemeSets[mList->getCursorId()].invalidRepository ||
|
|
|
|
mThemeSets[mList->getCursorId()].manuallyDownloaded) {
|
|
|
|
mDownloadStatus->setText(ViewController::CROSSEDCIRCLE_CHAR + " MANUAL DOWNLOAD");
|
|
|
|
mDownloadStatus->setColor(0x992222FF);
|
|
|
|
}
|
|
|
|
else {
|
2023-03-29 21:34:32 +00:00
|
|
|
if (mThemeSets[mList->getCursorId()].newEntry)
|
|
|
|
mDownloadStatus->setText("NOT INSTALLED (NEW)");
|
|
|
|
else
|
|
|
|
mDownloadStatus->setText("NOT INSTALLED");
|
2023-03-29 17:08:22 +00:00
|
|
|
mDownloadStatus->setColor(0x999999FF);
|
|
|
|
}
|
|
|
|
if (mThemeSets[mList->getCursorId()].hasLocalChanges) {
|
|
|
|
mLocalChanges->setText(ViewController::EXCLAMATION_CHAR + " LOCAL CHANGES");
|
|
|
|
mLocalChanges->setColor(0x992222FF);
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
mLocalChanges->setText("");
|
|
|
|
}
|
|
|
|
|
|
|
|
mVariantCount->setText(std::to_string(mThemeSets[mList->getCursorId()].variants.size()));
|
|
|
|
mColorSchemesCount->setText(
|
|
|
|
std::to_string(mThemeSets[mList->getCursorId()].colorSchemes.size()));
|
|
|
|
mAspectRatiosCount->setText(
|
|
|
|
std::to_string(mThemeSets[mList->getCursorId()].aspectRatios.size()));
|
|
|
|
mAuthor->setText("CREATED BY " +
|
|
|
|
Utils::String::toUpper(mThemeSets[mList->getCursorId()].author));
|
|
|
|
}
|
|
|
|
|
2023-03-30 17:19:36 +00:00
|
|
|
void GuiThemeDownloader::setupFullscreenViewer()
|
|
|
|
{
|
|
|
|
if (mThemeSets.empty())
|
|
|
|
return;
|
|
|
|
|
|
|
|
mViewerScreenshots.clear();
|
|
|
|
mViewerCaptions.clear();
|
|
|
|
mFullscreenViewerIndex = 0;
|
|
|
|
mFullscreenViewing = true;
|
|
|
|
|
|
|
|
for (auto& screenshot : mThemeSets[mList->getCursorId()].screenshots) {
|
|
|
|
auto image = std::make_shared<ImageComponent>(false, false);
|
|
|
|
image->setLinearInterpolation(true);
|
|
|
|
image->setMaxSize(mRenderer->getScreenWidth() * 0.86f,
|
|
|
|
mRenderer->getScreenHeight() * 0.86f);
|
|
|
|
image->setImage(mThemeDirectory + "themes-list/" + screenshot.image);
|
|
|
|
// Center image on screen.
|
|
|
|
glm::vec3 imagePos {image->getPosition()};
|
|
|
|
imagePos.x = (mRenderer->getScreenWidth() - image->getSize().x) / 2.0f;
|
|
|
|
imagePos.y = (mRenderer->getScreenHeight() - image->getSize().y) / 2.0f;
|
|
|
|
image->setPosition(imagePos);
|
|
|
|
mViewerScreenshots.emplace_back(image);
|
|
|
|
auto caption = std::make_shared<TextComponent>(screenshot.caption,
|
|
|
|
Font::get(FONT_SIZE_MINI, FONT_PATH_REGULAR),
|
|
|
|
0xCCCCCCFF, ALIGN_LEFT);
|
|
|
|
glm::vec3 textPos {image->getPosition()};
|
|
|
|
textPos.y += image->getSize().y;
|
|
|
|
caption->setPosition(textPos);
|
|
|
|
mViewerCaptions.emplace_back(caption);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (mViewerScreenshots.size() > 0) {
|
|
|
|
// Navigation indicators to the left and right of the screenshot.
|
|
|
|
glm::vec3 indicatorPos {mViewerScreenshots.front()->getPosition()};
|
|
|
|
indicatorPos.x -= mViewerIndicatorLeft->getSize().x * 2.0f;
|
|
|
|
indicatorPos.y += (mViewerScreenshots.front()->getSize().y / 2.0f) -
|
|
|
|
(mViewerIndicatorLeft->getSize().y / 2.0f);
|
|
|
|
mViewerIndicatorLeft->setPosition(indicatorPos);
|
|
|
|
indicatorPos.x +=
|
|
|
|
mViewerScreenshots.front()->getSize().x + (mViewerIndicatorRight->getSize().x * 3.0f);
|
|
|
|
mViewerIndicatorRight->setPosition(indicatorPos);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-03-21 18:01:44 +00:00
|
|
|
void GuiThemeDownloader::update(int deltaTime)
|
|
|
|
{
|
|
|
|
if (mFuture.valid()) {
|
|
|
|
// Only wait one millisecond as this update() function runs very frequently.
|
|
|
|
if (mFuture.wait_for(std::chrono::milliseconds(1)) == std::future_status::ready) {
|
|
|
|
if (mFetchThread.joinable()) {
|
|
|
|
mFetchThread.join();
|
|
|
|
mFetching = false;
|
2023-03-26 18:49:44 +00:00
|
|
|
if (mRepositoryError != RepositoryError::NO_REPO_ERROR) {
|
2023-03-27 19:29:37 +00:00
|
|
|
std::string errorMessage {"ERROR: "};
|
2023-03-29 20:21:55 +00:00
|
|
|
errorMessage.append(Utils::String::toUpper(mMessage));
|
2023-03-27 19:29:37 +00:00
|
|
|
mWindow->queueInfoPopup(errorMessage, 6000);
|
2023-03-29 20:21:55 +00:00
|
|
|
LOG(LogError) << "Error: " << mMessage;
|
|
|
|
mMessage = "";
|
2023-03-21 18:01:44 +00:00
|
|
|
}
|
2023-03-27 19:29:37 +00:00
|
|
|
if (mThemeSets.empty() && mLatestThemesList) {
|
2023-03-21 18:01:44 +00:00
|
|
|
parseThemesList();
|
2023-03-27 19:29:37 +00:00
|
|
|
makeInventory();
|
|
|
|
populateGUI();
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
makeInventory();
|
|
|
|
updateGUI();
|
|
|
|
}
|
2023-03-21 18:01:44 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (mFetching) {
|
|
|
|
int progress {mReceivedObjectsProgress != 1.0f ? 0 : 100};
|
2023-03-29 20:21:55 +00:00
|
|
|
if (mStatusType != StatusType::STATUS_NO_CHANGE) {
|
|
|
|
if (mStatusType == StatusType::STATUS_DOWNLOADING)
|
|
|
|
mBusyAnim.setText(mStatusText + " 100%");
|
|
|
|
else if (mStatusType == StatusType::STATUS_UPDATING)
|
|
|
|
mBusyAnim.setText(mStatusText);
|
|
|
|
mBusyAnim.onSizeChanged();
|
|
|
|
mStatusType = StatusType::STATUS_NO_CHANGE;
|
|
|
|
}
|
2023-03-21 18:01:44 +00:00
|
|
|
if (mReceivedObjectsProgress != 1.0f) {
|
2023-03-22 21:40:14 +00:00
|
|
|
progress = static_cast<int>(
|
|
|
|
std::round(glm::mix(0.0f, 100.0f, static_cast<float>(mReceivedObjectsProgress))));
|
2023-03-29 20:21:55 +00:00
|
|
|
if (mStatusText.substr(0, 11) == "DOWNLOADING")
|
|
|
|
mBusyAnim.setText(mStatusText + " " + std::to_string(progress) + "%");
|
|
|
|
else
|
|
|
|
mBusyAnim.setText(mStatusText);
|
2023-03-21 18:01:44 +00:00
|
|
|
}
|
|
|
|
else if (mReceivedObjectsProgress != 0.0f) {
|
2023-03-22 21:40:14 +00:00
|
|
|
progress = static_cast<int>(
|
|
|
|
std::round(glm::mix(0.0f, 100.0f, static_cast<float>(mResolveDeltaProgress))));
|
2023-03-29 20:21:55 +00:00
|
|
|
if (mStatusText.substr(0, 11) == "DOWNLOADING")
|
|
|
|
mBusyAnim.setText(mStatusText + " " + std::to_string(progress) + "%");
|
|
|
|
else
|
|
|
|
mBusyAnim.setText(mStatusText);
|
2023-03-21 18:01:44 +00:00
|
|
|
}
|
|
|
|
mBusyAnim.update(deltaTime);
|
|
|
|
}
|
2023-03-29 17:08:22 +00:00
|
|
|
|
2023-03-29 20:21:55 +00:00
|
|
|
if (mMessage != "") {
|
|
|
|
mWindow->queueInfoPopup(mMessage, 6000);
|
|
|
|
mMessage = "";
|
|
|
|
}
|
|
|
|
|
2023-03-29 17:08:22 +00:00
|
|
|
GuiComponent::update(deltaTime);
|
2023-03-21 18:01:44 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
void GuiThemeDownloader::render(const glm::mat4& parentTrans)
|
|
|
|
{
|
|
|
|
glm::mat4 trans {parentTrans * getTransform()};
|
|
|
|
renderChildren(trans);
|
2023-03-29 17:08:22 +00:00
|
|
|
|
|
|
|
if (mGrayRectangleCoords.size() == 4) {
|
|
|
|
mRenderer->setMatrix(parentTrans * getTransform());
|
|
|
|
mRenderer->drawRect(mGrayRectangleCoords[0], mGrayRectangleCoords[1],
|
|
|
|
mGrayRectangleCoords[2], mGrayRectangleCoords[3], 0x00000009,
|
|
|
|
0x00000009);
|
|
|
|
}
|
|
|
|
|
2023-03-21 18:01:44 +00:00
|
|
|
if (mFetching)
|
|
|
|
mBusyAnim.render(trans);
|
2023-03-30 17:19:36 +00:00
|
|
|
|
|
|
|
if (mFullscreenViewing && mViewerScreenshots.size() > 0) {
|
|
|
|
mRenderer->setMatrix(parentTrans);
|
|
|
|
mRenderer->drawRect(0.0f, 0.0f, mRenderer->getScreenWidth(), mRenderer->getScreenHeight(),
|
|
|
|
0x222222FF, 0x222222FF);
|
|
|
|
mViewerScreenshots[mFullscreenViewerIndex]->render(parentTrans);
|
|
|
|
mViewerCaptions[mFullscreenViewerIndex]->render(parentTrans);
|
|
|
|
if (mFullscreenViewerIndex != 0)
|
|
|
|
mViewerIndicatorLeft->render(parentTrans);
|
|
|
|
if (mFullscreenViewerIndex != mViewerCaptions.size() - 1)
|
|
|
|
mViewerIndicatorRight->render(parentTrans);
|
|
|
|
}
|
2023-03-21 18:01:44 +00:00
|
|
|
}
|
|
|
|
|
2023-02-13 19:38:23 +00:00
|
|
|
void GuiThemeDownloader::onSizeChanged()
|
|
|
|
{
|
|
|
|
const float screenSize {mRenderer->getIsVerticalOrientation() ? mRenderer->getScreenWidth() :
|
|
|
|
mRenderer->getScreenHeight()};
|
|
|
|
mGrid.setRowHeightPerc(0, (mTitle->getFont()->getLetterHeight() + screenSize * 0.2f) / mSize.y /
|
2023-03-29 17:08:22 +00:00
|
|
|
4.0f);
|
|
|
|
mGrid.setRowHeightPerc(1, (mTitle->getFont()->getLetterHeight() + screenSize * 0.2f) / mSize.y /
|
|
|
|
4.0f);
|
|
|
|
mGrid.setRowHeightPerc(2, (mVariantsLabel->getFont()->getLetterHeight() + screenSize * 0.08f) /
|
|
|
|
mSize.y / 2.0f);
|
|
|
|
mGrid.setRowHeightPerc(3,
|
|
|
|
(mColorSchemesLabel->getFont()->getLetterHeight() + screenSize * 0.06f) /
|
|
|
|
mSize.y / 2.0f);
|
|
|
|
mGrid.setRowHeightPerc(4, (mDownloadStatus->getFont()->getLetterHeight() + screenSize * 0.08f) /
|
|
|
|
mSize.y / 2.0f);
|
|
|
|
mGrid.setRowHeightPerc(5, 0.5f);
|
|
|
|
mGrid.setRowHeightPerc(6, (mAuthor->getFont()->getLetterHeight() + screenSize * 0.06f) /
|
|
|
|
mSize.y / 2.0f);
|
|
|
|
|
|
|
|
mGrid.setColWidthPerc(0, 0.01f);
|
|
|
|
mGrid.setColWidthPerc(1, 0.18f);
|
|
|
|
mGrid.setColWidthPerc(2, 0.05f);
|
|
|
|
mGrid.setColWidthPerc(3, 0.18f);
|
|
|
|
mGrid.setColWidthPerc(4, 0.04f);
|
|
|
|
mGrid.setColWidthPerc(5, 0.005f);
|
|
|
|
mGrid.setColWidthPerc(7, 0.04f);
|
2023-02-13 19:38:23 +00:00
|
|
|
|
|
|
|
mGrid.setSize(mSize);
|
|
|
|
mBackground.fitTo(mSize, glm::vec3 {0.0f, 0.0f, 0.0f}, glm::vec2 {-32.0f, -32.0f});
|
2023-03-29 17:08:22 +00:00
|
|
|
mScreenshot->setMaxSize(mGrid.getColWidth(1) + mGrid.getColWidth(2) + mGrid.getColWidth(3) +
|
|
|
|
mGrid.getColWidth(4),
|
|
|
|
mGrid.getRowHeight(5));
|
|
|
|
|
|
|
|
mGrayRectangleCoords.clear();
|
|
|
|
mGrayRectangleCoords.emplace_back(0.0f);
|
|
|
|
mGrayRectangleCoords.emplace_back(mList->getPosition().y);
|
|
|
|
mGrayRectangleCoords.emplace_back(mSize.x);
|
|
|
|
mGrayRectangleCoords.emplace_back(mGrid.getRowHeight(2) + mGrid.getRowHeight(3) +
|
|
|
|
mGrid.getRowHeight(4) + mGrid.getRowHeight(5) +
|
|
|
|
mGrid.getRowHeight(6));
|
2023-02-13 19:38:23 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
bool GuiThemeDownloader::input(InputConfig* config, Input input)
|
|
|
|
{
|
2023-03-30 17:19:36 +00:00
|
|
|
if (mFullscreenViewing && input.value) {
|
|
|
|
if (config->isMappedLike("left", input)) {
|
|
|
|
if (mFullscreenViewerIndex > 0)
|
|
|
|
--mFullscreenViewerIndex;
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
else if (config->isMappedLike("right", input)) {
|
|
|
|
if (mViewerScreenshots.size() > mFullscreenViewerIndex + 1)
|
|
|
|
++mFullscreenViewerIndex;
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
mViewerScreenshots.clear();
|
|
|
|
mViewerCaptions.clear();
|
|
|
|
mFullscreenViewing = false;
|
|
|
|
mFullscreenViewerIndex = 0;
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-02-13 19:38:23 +00:00
|
|
|
if (config->isMappedTo("b", input) && input.value) {
|
|
|
|
delete this;
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2023-03-30 17:19:36 +00:00
|
|
|
if (config->isMappedTo("x", input) && input.value && mGrid.getSelectedComponent() == mList) {
|
|
|
|
setupFullscreenViewer();
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2023-03-29 20:21:55 +00:00
|
|
|
return GuiComponent::input(config, input);
|
2023-02-13 19:38:23 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
std::vector<HelpPrompt> GuiThemeDownloader::getHelpPrompts()
|
|
|
|
{
|
2023-03-22 19:56:48 +00:00
|
|
|
std::vector<HelpPrompt> prompts {mGrid.getHelpPrompts()};
|
2023-03-30 17:19:36 +00:00
|
|
|
prompts.push_back(HelpPrompt("b", "close"));
|
|
|
|
|
|
|
|
if (mList->size() > 0) {
|
|
|
|
if (mGrid.getSelectedComponent() == mList)
|
|
|
|
prompts.push_back(HelpPrompt("x", "view screenshots"));
|
|
|
|
|
|
|
|
if (mThemeSets[mList->getCursorId()].isCloned)
|
|
|
|
prompts.push_back(HelpPrompt("a", "fetch updates"));
|
|
|
|
else
|
|
|
|
prompts.push_back(HelpPrompt("a", "download"));
|
|
|
|
}
|
2023-02-13 19:38:23 +00:00
|
|
|
|
|
|
|
return prompts;
|
|
|
|
}
|
2023-03-27 19:29:37 +00:00
|
|
|
|
|
|
|
bool GuiThemeDownloader::fetchThemesList()
|
|
|
|
{
|
|
|
|
const std::string repositoryName {"themes-list"};
|
|
|
|
const std::string url {"https://gitlab.com/es-de/themes/themes-list.git"};
|
2023-03-29 17:08:22 +00:00
|
|
|
const std::string path {mThemeDirectory + "themes-list"};
|
2023-03-27 19:29:37 +00:00
|
|
|
|
|
|
|
if (Utils::FileSystem::exists(path)) {
|
|
|
|
git_repository* repository {nullptr};
|
|
|
|
int errorCode {git_repository_open(&repository, &path[0])};
|
|
|
|
if (errorCode != 0) {
|
|
|
|
if (renameDirectory(path)) {
|
|
|
|
git_repository_free(repository);
|
|
|
|
LOG(LogError) << "Couldn't rename \"" << path << "\", permission problems?";
|
|
|
|
mWindow->pushGui(new GuiMsgBox(
|
|
|
|
getHelpStyle(), "COULDN'T RENAME DIRECTORY\n" + path + "\nPERMISSION PROBLEMS?",
|
|
|
|
"OK", [] { return; }, "", nullptr, "", nullptr, true));
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
checkLocalChanges(repository);
|
|
|
|
|
|
|
|
// We always hard reset the themes list as it should never contain any local changes.
|
|
|
|
resetRepository(repository);
|
|
|
|
|
|
|
|
git_repository_free(repository);
|
|
|
|
|
|
|
|
mFetchThread =
|
|
|
|
std::thread(&GuiThemeDownloader::fetchRepository, this, repositoryName, url, false);
|
2023-03-29 20:21:55 +00:00
|
|
|
mStatusType = StatusType::STATUS_UPDATING;
|
|
|
|
mStatusText = "UPDATING THEMES LIST";
|
2023-03-27 19:29:37 +00:00
|
|
|
}
|
|
|
|
else {
|
|
|
|
LOG(LogInfo) << "GuiThemeDownloader: Creating initial themes list repository clone";
|
|
|
|
mFetchThread = std::thread(&GuiThemeDownloader::cloneRepository, this, repositoryName, url);
|
2023-03-29 20:21:55 +00:00
|
|
|
mStatusType = StatusType::STATUS_DOWNLOADING;
|
|
|
|
mStatusText = "DOWNLOADING THEMES LIST";
|
2023-03-27 19:29:37 +00:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
bool GuiThemeDownloader::cloneRepository(const std::string& repositoryName, const std::string& url)
|
|
|
|
{
|
|
|
|
int errorCode {0};
|
|
|
|
git_repository* repository {nullptr};
|
2023-03-29 17:08:22 +00:00
|
|
|
const std::string path {mThemeDirectory + repositoryName};
|
2023-03-27 19:29:37 +00:00
|
|
|
|
|
|
|
#if LIBGIT2_VER_MAJOR >= 1
|
|
|
|
auto fetchProgressFunc = [](const git_indexer_progress* stats, void* payload) -> int {
|
|
|
|
#else
|
|
|
|
auto fetchProgressFunc = [](const git_transfer_progress* stats, void* payload) -> int {
|
|
|
|
#endif
|
|
|
|
(void)payload;
|
|
|
|
if (stats->received_objects == stats->total_objects) {
|
|
|
|
#if (DEBUG_CLONING)
|
|
|
|
LOG(LogDebug) << "Indexed deltas: " << stats->indexed_deltas
|
|
|
|
<< " Total deltas: " << stats->total_deltas;
|
|
|
|
#endif
|
|
|
|
mReceivedObjectsProgress = 1.0f;
|
|
|
|
if (stats->total_deltas > 0) {
|
|
|
|
mResolveDeltaProgress = static_cast<float>(stats->indexed_deltas) /
|
|
|
|
static_cast<float>(stats->total_deltas);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
else if (stats->total_objects > 0) {
|
|
|
|
#if (DEBUG_CLONING)
|
|
|
|
LOG(LogDebug) << "Received objects: " << stats->received_objects
|
|
|
|
<< " Total objects: " << stats->total_objects
|
|
|
|
<< " Indexed objects: " << stats->indexed_objects
|
|
|
|
<< " Received bytes: " << stats->received_bytes;
|
|
|
|
#endif
|
|
|
|
if (stats->total_objects > 0) {
|
|
|
|
mReceivedObjectsProgress = static_cast<float>(stats->received_objects) /
|
|
|
|
static_cast<float>(stats->total_objects);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return 0;
|
|
|
|
};
|
|
|
|
|
|
|
|
#if LIBGIT2_VER_MAJOR >= 1
|
|
|
|
git_clone_options cloneOptions;
|
|
|
|
git_clone_options_init(&cloneOptions, GIT_CLONE_OPTIONS_VERSION);
|
|
|
|
#else
|
|
|
|
git_clone_options cloneOptions = GIT_CLONE_OPTIONS_INIT;
|
|
|
|
#endif
|
|
|
|
|
|
|
|
cloneOptions.checkout_opts.checkout_strategy = GIT_CHECKOUT_SAFE;
|
|
|
|
cloneOptions.fetch_opts.callbacks.transfer_progress = fetchProgressFunc;
|
|
|
|
|
|
|
|
mReceivedObjectsProgress = 0.0f;
|
|
|
|
mResolveDeltaProgress = 0.0f;
|
|
|
|
|
|
|
|
mFetching = true;
|
|
|
|
errorCode = git_clone(&repository, &url[0], &path[0], &cloneOptions);
|
|
|
|
git_repository_free(repository);
|
|
|
|
|
|
|
|
if (errorCode != 0) {
|
|
|
|
const git_error* gitError {git_error_last()};
|
|
|
|
LOG(LogWarning) << "GuiThemeDownloader: Git returned error code " << errorCode
|
|
|
|
<< ", error message: \"" << gitError->message << "\"";
|
2023-03-29 20:21:55 +00:00
|
|
|
mMessage = gitError->message;
|
2023-03-27 19:29:37 +00:00
|
|
|
git_error_clear();
|
|
|
|
mPromise.set_value(true);
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
mLatestThemesList = true;
|
|
|
|
mPromise.set_value(true);
|
|
|
|
return false;
|
|
|
|
}
|