// SPDX-License-Identifier: MIT // // EmulationStation Desktop Edition // GuiThemeDownloader.cpp // // Theme downloader. // #include "guis/GuiThemeDownloader.h" #include "EmulationStation.h" #include "components/MenuComponent.h" #include "resources/ResourceManager.h" #include "rapidjson/document.h" #include "rapidjson/error/en.h" #define DEBUG_CLONING false GuiThemeDownloader::GuiThemeDownloader() : mRenderer {Renderer::getInstance()} , mBackground {":/graphics/frame.svg"} , mGrid {glm::ivec2 {8, 8}} , mRepositoryError {RepositoryError::NO_REPO_ERROR} , mFetching {false} , mLatestThemesList {false} , mFullscreenViewing {false} , mFullscreenViewerIndex {0} { addChild(&mBackground); addChild(&mGrid); const float fontSizeSmall {mRenderer->getIsVerticalOrientation() ? FONT_SIZE_MINI : FONT_SIZE_SMALL}; // Set up grid. mGrid.setEntry(std::make_shared(), glm::ivec2 {0, 2}, false, false, glm::ivec2 {1, 5}, GridFlags::BORDER_TOP | GridFlags::BORDER_BOTTOM); mTitle = std::make_shared("THEME DOWNLOADER", Font::get(FONT_SIZE_LARGE), 0x555555FF, ALIGN_CENTER); mGrid.setEntry(mTitle, glm::ivec2 {0, 0}, false, true, glm::ivec2 {8, 2}); mVariantsLabel = std::make_shared("", 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("", Font::get(fontSizeSmall), 0x555555FF, ALIGN_LEFT); mGrid.setEntry(mColorSchemesLabel, glm::ivec2 {1, 3}, false, true, glm::ivec2 {1, 1}); mAspectRatiosLabel = std::make_shared("", 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("", Font::get(fontSizeSmall), 0x555555FF, ALIGN_LEFT); mGrid.setEntry(mFutureUseLabel, glm::ivec2 {3, 3}, false, true, glm::ivec2 {1, 1}); mGrid.setEntry(std::make_shared(), glm::ivec2 {5, 2}, false, false, glm::ivec2 {1, 5}, GridFlags::BORDER_TOP | GridFlags::BORDER_BOTTOM); mVariantCount = std::make_shared("", 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( "", 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( "", 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("", 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("", 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("", 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(); mScreenshot->setLinearInterpolation(true); mGrid.setEntry(mScreenshot, glm::ivec2 {1, 5}, false, true, glm::ivec2 {4, 1}); mAuthor = std::make_shared("", Font::get(FONT_SIZE_MINI, FONT_PATH_LIGHT), 0x555555FF, ALIGN_LEFT); mGrid.setEntry(mAuthor, glm::ivec2 {1, 6}, false, true, glm::ivec2 {4, 1}); mList = std::make_shared(); 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(); mScrollDown = std::make_shared(); 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(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}); std::vector> buttons; buttons.push_back(std::make_shared("CLOSE", "CLOSE", [&] { delete this; })); mButtons = makeButtonGrid(buttons); 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); setPosition((mRenderer->getScreenWidth() - mSize.x) / 2.0f, (mRenderer->getScreenHeight() - mSize.y) / 2.0f); mBusyAnim.setSize(mSize); mBusyAnim.setText("DOWNLOADING THEMES LIST 100%"); mBusyAnim.onSizeChanged(); mList->setCursorChangedCallback([this](CursorState state) { if (state == CursorState::CURSOR_SCROLLING || state == CursorState::CURSOR_STOPPED) updateInfoPane(); }); mViewerIndicatorLeft = std::make_shared( "\uf104", Font::get(FONT_SIZE_LARGE * 1.2f, FONT_PATH_BOLD), 0xCCCCCCFF, ALIGN_CENTER); mViewerIndicatorRight = std::make_shared( "\uf105", Font::get(FONT_SIZE_LARGE * 1.2f, FONT_PATH_BOLD), 0xCCCCCCFF, ALIGN_CENTER); git_libgit2_init(); // The promise/future mechanism is used as signaling for the thread to indicate that // repository fetching has been completed. std::promise().swap(mPromise); mFuture = mPromise.get_future(); mThemeDirectory = Utils::FileSystem::getHomePath() + "/.emulationstation/themes/"; fetchThemesList(); } GuiThemeDownloader::~GuiThemeDownloader() { if (mFetchThread.joinable()) mFetchThread.join(); git_libgit2_shutdown(); } bool GuiThemeDownloader::fetchRepository(const std::string& repositoryName, const std::string& url, bool allowReset) { int errorCode {0}; const std::string path {mThemeDirectory + repositoryName}; mRepositoryError = RepositoryError::NO_REPO_ERROR; mMessage = ""; const bool isThemesList {repositoryName == "themes-list"}; git_repository* repository {nullptr}; git_remote* gitRemote {nullptr}; try { mFetching = true; errorCode = git_repository_open(&repository, &path[0]); 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, "); } #if LIBGIT2_VER_MAJOR >= 1 git_fetch_options fetchOptions; git_fetch_options_init(&fetchOptions, GIT_FETCH_OPTIONS_VERSION); #else git_fetch_options fetchOptions = GIT_FETCH_OPTIONS_INIT; #endif errorCode = git_remote_fetch(gitRemote, nullptr, &fetchOptions, nullptr); if (errorCode != 0) throw std::runtime_error("Couldn't pull latest commits, "); git_annotated_commit* annotated {nullptr}; git_object* object {nullptr}; 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()); #if LIBGIT2_VER_MAJOR >= 1 git_checkout_options checkoutOptions; git_checkout_options_init(&checkoutOptions, GIT_CHECKOUT_OPTIONS_VERSION); #else git_checkout_options checkoutOptions = GIT_CHECKOUT_OPTIONS_INIT; #endif checkoutOptions.checkout_strategy = GIT_CHECKOUT_SAFE; errorCode = git_checkout_tree(repository, object, &checkoutOptions); errorCode = git_repository_set_head(repository, branchName.c_str()); git_reference_free(oldTargetRef); } git_buf_dispose(&buffer); } errorCode = git_revparse_single(&object, repository, "FETCH_HEAD"); errorCode = git_annotated_commit_lookup(&annotated, repository, git_object_id(object)); git_merge_analysis_t mergeAnalysis {}; git_merge_preference_t mergePreference {}; errorCode = git_merge_analysis(&mergeAnalysis, &mergePreference, repository, (const git_annotated_commit**)(&annotated), 1); if (errorCode != 0) { git_object_free(object); git_annotated_commit_free(annotated); throw std::runtime_error("Couldn't run Git merge analysis, "); } 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); } 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; } } if (allowReset && checkLocalChanges(repository)) { LOG(LogInfo) << "Repository \"" << repositoryName << "\" contains local changes, performing hard reset"; resetRepository(repository); } if (mergeAnalysis & GIT_MERGE_ANALYSIS_UP_TO_DATE) { LOG(LogInfo) << "Repository \"" << repositoryName << "\" already up to date"; if (repositoryName != "themes-list") mMessage = "THEME ALREADY UP TO DATE"; git_annotated_commit_free(annotated); git_object_free(object); mPromise.set_value(true); if (isThemesList) mLatestThemesList = true; return false; } LOG(LogInfo) << "Performing Git fast-forward of repository \"" << repositoryName << "\""; git_reference* oldTargetRef {nullptr}; git_repository_head(&oldTargetRef, repository); const git_oid* objectID {nullptr}; objectID = git_annotated_commit_id(annotated); git_object_lookup(&object, repository, objectID, GIT_OBJECT_COMMIT); git_reference* newTargetRef {nullptr}; #if LIBGIT2_VER_MAJOR >= 1 git_checkout_options checkoutOptions; git_checkout_options_init(&checkoutOptions, GIT_CHECKOUT_OPTIONS_VERSION); #else git_checkout_options checkoutOptions = GIT_CHECKOUT_OPTIONS_INIT; #endif checkoutOptions.checkout_strategy = GIT_CHECKOUT_SAFE; git_checkout_tree(repository, object, &checkoutOptions); errorCode = git_reference_set_target(&newTargetRef, oldTargetRef, objectID, nullptr); git_reference_free(oldTargetRef); git_reference_free(newTargetRef); git_annotated_commit_free(annotated); git_object_free(object); if (errorCode != 0) throw std::runtime_error("Couldn't fast-forward repository, "); if (isThemesList) mLatestThemesList = true; 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; mMessage = gitError->message; git_error_clear(); if (gitRemote != nullptr) git_remote_disconnect(gitRemote); mPromise.set_value(true); return true; } if (repositoryName != "themes-list") mMessage = "THEME HAS BEEN UPDATED"; mPromise.set_value(true); return false; } bool GuiThemeDownloader::checkLocalChanges(git_repository* repository, bool hasFetched) { git_status_list* status {nullptr}; size_t statusEntryCount {0}; int errorCode {0}; #if LIBGIT2_VER_MAJOR >= 1 git_status_options statusOptions; git_status_options_init(&statusOptions, GIT_STATUS_OPTIONS_VERSION); #else git_status_options statusOptions = GIT_STATUS_OPTIONS_INIT; #endif // 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; errorCode = git_status_list_new(&status, repository, &statusOptions); if (errorCode == 0) statusEntryCount = git_status_list_entrycount(status); git_status_list_free(status); // TODO: Also check if there are any local commits not on origin. return (statusEntryCount != 0); } 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); } void GuiThemeDownloader::makeInventory() { for (auto& theme : mThemeSets) { const std::string path {mThemeDirectory + theme.reponame}; 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"; } if (Utils::FileSystem::exists(path)) { git_repository* repository {nullptr}; int errorCode {0}; errorCode = git_repository_open(&repository, &path[0]); if (errorCode != 0) { theme.invalidRepository = true; git_repository_free(repository); continue; } theme.isCloned = true; if (checkLocalChanges(repository)) theme.hasLocalChanges = true; git_repository_free(repository); } } } 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; } void GuiThemeDownloader::parseThemesList() { // Temporary location for testing purposes. // const std::string themesFile {Utils::FileSystem::getHomePath() + // "/.emulationstation/themes.json"}; const std::string themesFile {mThemeDirectory + "themes-list/themes.json"}; 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(themesFileData.ptr.get()), themesFileData.length); if (doc.HasParseError()) { LOG(LogWarning) << "GuiThemeDownloader: Couldn't parse the themes.json file"; return; } 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)); } } if (doc.HasMember("themeSets") && doc["themeSets"].IsArray()) { const rapidjson::Value& themeSets {doc["themeSets"]}; for (int i {0}; i < static_cast(themeSets.Size()); ++i) { ThemeEntry themeEntry; const rapidjson::Value& theme {themeSets[i]}; if (theme.HasMember("name") && theme["name"].IsString()) themeEntry.name = theme["name"].GetString(); if (theme.HasMember("reponame") && theme["reponame"].IsString()) themeEntry.reponame = theme["reponame"].GetString(); if (theme.HasMember("url") && theme["url"].IsString()) themeEntry.url = theme["url"].GetString(); if (theme.HasMember("author") && theme["author"].IsString()) themeEntry.author = theme["author"].GetString(); if (theme.HasMember("newEntry") && theme["newEntry"].IsBool()) themeEntry.newEntry = theme["newEntry"].GetBool(); if (theme.HasMember("variants") && theme["variants"].IsArray()) { const rapidjson::Value& variants {theme["variants"]}; for (int i {0}; i < static_cast(variants.Size()); ++i) themeEntry.variants.emplace_back(variants[i].GetString()); } if (theme.HasMember("colorSchemes") && theme["colorSchemes"].IsArray()) { const rapidjson::Value& colorSchemes {theme["colorSchemes"]}; for (int i {0}; i < static_cast(colorSchemes.Size()); ++i) themeEntry.colorSchemes.emplace_back(colorSchemes[i].GetString()); } if (theme.HasMember("aspectRatios") && theme["aspectRatios"].IsArray()) { const rapidjson::Value& aspectRatios {theme["aspectRatios"]}; for (int i {0}; i < static_cast(aspectRatios.Size()); ++i) themeEntry.aspectRatios.emplace_back(aspectRatios[i].GetString()); } if (theme.HasMember("transitions") && theme["transitions"].IsArray()) { const rapidjson::Value& transitions {theme["transitions"]}; for (int i {0}; i < static_cast(transitions.Size()); ++i) themeEntry.transitions.emplace_back(transitions[i].GetString()); } if (theme.HasMember("screenshots") && theme["screenshots"].IsArray()) { const rapidjson::Value& screenshots {theme["screenshots"]}; for (int i {0}; i < static_cast(screenshots.Size()); ++i) { Screenshot screenshotEntry; if (screenshots[i].HasMember("image") && screenshots[i]["image"].IsString()) screenshotEntry.image = screenshots[i]["image"].GetString(); if (screenshots[i].HasMember("caption") && screenshots[i]["caption"].IsString()) screenshotEntry.caption = screenshots[i]["caption"].GetString(); if (screenshotEntry.image != "" && screenshotEntry.caption != "") themeEntry.screenshots.emplace_back(screenshotEntry); } } mThemeSets.emplace_back(themeEntry); } } LOG(LogDebug) << "GuiThemeDownloader::parseThemesList(): Parsed " << mThemeSets.size() << " theme sets"; } void GuiThemeDownloader::populateGUI() { if (mThemeSets.empty()) return; for (auto& theme : mThemeSets) { std::string themeName {Utils::String::toUpper(theme.name)}; if (theme.newEntry && !theme.isCloned) themeName.append(" ").append(ViewController::BRANCH_CHAR); if (theme.isCloned) themeName.append(" ").append(ViewController::TICKMARK_CHAR); if (theme.manuallyDownloaded || theme.invalidRepository) themeName.append(" ").append(ViewController::CROSSEDCIRCLE_CHAR); if (theme.hasLocalChanges) themeName.append(" ").append(ViewController::EXCLAMATION_CHAR); ComponentListRow row; std::shared_ptr themeNameElement { std::make_shared(themeName, Font::get(FONT_SIZE_MEDIUM), 0x777777FF)}; ThemeGUIEntry guiEntry; guiEntry.themeName = themeNameElement; mThemeGUIEntries.emplace_back(guiEntry); row.addElement(themeNameElement, false); row.makeAcceptInputHandler([this, &theme] { std::promise().swap(mPromise); 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] { renameDirectory(mThemeDirectory + theme.reponame + theme.manualExtension); std::promise().swap(mPromise); mFuture = mPromise.get_future(); mFetchThread = std::thread(&GuiThemeDownloader::cloneRepository, this, theme.reponame, theme.url); mStatusType = StatusType::STATUS_DOWNLOADING; mStatusText = "DOWNLOADING THEME"; }, "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().swap(mPromise); mFuture = mPromise.get_future(); mFetchThread = std::thread(&GuiThemeDownloader::fetchRepository, this, theme.reponame, theme.url, true); mStatusType = StatusType::STATUS_UPDATING; mStatusText = "UPDATING THEME"; }, "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); mStatusType = StatusType::STATUS_UPDATING; mStatusText = "UPDATING THEME"; } else { mFuture = mPromise.get_future(); mFetchThread = std::thread(&GuiThemeDownloader::cloneRepository, this, theme.reponame, theme.url); mStatusType = StatusType::STATUS_DOWNLOADING; mStatusText = "DOWNLOADING THEME"; } }); mList->addRow(row); } mVariantsLabel->setText("VARIANTS:"); mColorSchemesLabel->setText(mRenderer->getIsVerticalOrientation() ? "COL. SCHEMES:" : "COLOR SCHEMES:"); mAspectRatiosLabel->setText("ASPECT RATIOS:"); updateInfoPane(); updateHelpPrompts(); } void GuiThemeDownloader::updateGUI() { updateInfoPane(); updateHelpPrompts(); for (size_t i {0}; i < mThemeSets.size(); ++i) { std::string themeName {Utils::String::toUpper(mThemeSets[i].name)}; if (mThemeSets[i].newEntry && !mThemeSets[i].isCloned) themeName.append(" ").append(ViewController::BRANCH_CHAR); if (mThemeSets[i].isCloned) themeName.append(" ").append(ViewController::TICKMARK_CHAR); if (mThemeSets[i].manuallyDownloaded || mThemeSets[i].invalidRepository) themeName.append(" ").append(ViewController::CROSSEDCIRCLE_CHAR); if (mThemeSets[i].hasLocalChanges) themeName.append(" ").append(ViewController::EXCLAMATION_CHAR); mThemeGUIEntries[i].themeName->setText(themeName); } } void GuiThemeDownloader::updateInfoPane() { assert(static_cast(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 { if (mThemeSets[mList->getCursorId()].newEntry) mDownloadStatus->setText("NOT INSTALLED (NEW)"); else mDownloadStatus->setText("NOT INSTALLED"); 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)); } 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(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(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); } } 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; if (mRepositoryError != RepositoryError::NO_REPO_ERROR) { std::string errorMessage {"ERROR: "}; errorMessage.append(Utils::String::toUpper(mMessage)); mWindow->queueInfoPopup(errorMessage, 6000); LOG(LogError) << "Error: " << mMessage; mMessage = ""; } if (mThemeSets.empty() && mLatestThemesList) { parseThemesList(); makeInventory(); populateGUI(); } else { makeInventory(); updateGUI(); } } } } if (mFetching) { int progress {mReceivedObjectsProgress != 1.0f ? 0 : 100}; 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; } if (mReceivedObjectsProgress != 1.0f) { progress = static_cast( std::round(glm::mix(0.0f, 100.0f, static_cast(mReceivedObjectsProgress)))); if (mStatusText.substr(0, 11) == "DOWNLOADING") mBusyAnim.setText(mStatusText + " " + std::to_string(progress) + "%"); else mBusyAnim.setText(mStatusText); } else if (mReceivedObjectsProgress != 0.0f) { progress = static_cast( std::round(glm::mix(0.0f, 100.0f, static_cast(mResolveDeltaProgress)))); if (mStatusText.substr(0, 11) == "DOWNLOADING") mBusyAnim.setText(mStatusText + " " + std::to_string(progress) + "%"); else mBusyAnim.setText(mStatusText); } mBusyAnim.update(deltaTime); } if (mMessage != "") { mWindow->queueInfoPopup(mMessage, 6000); mMessage = ""; } GuiComponent::update(deltaTime); } void GuiThemeDownloader::render(const glm::mat4& parentTrans) { glm::mat4 trans {parentTrans * getTransform()}; renderChildren(trans); if (mGrayRectangleCoords.size() == 4) { mRenderer->setMatrix(parentTrans * getTransform()); mRenderer->drawRect(mGrayRectangleCoords[0], mGrayRectangleCoords[1], mGrayRectangleCoords[2], mGrayRectangleCoords[3], 0x00000009, 0x00000009); } if (mFetching) mBusyAnim.render(trans); 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); } } void GuiThemeDownloader::onSizeChanged() { const float screenSize {mRenderer->getIsVerticalOrientation() ? mRenderer->getScreenWidth() : mRenderer->getScreenHeight()}; mGrid.setRowHeightPerc(0, (mTitle->getFont()->getLetterHeight() + screenSize * 0.2f) / mSize.y / 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); mGrid.setSize(mSize); mBackground.fitTo(mSize, glm::vec3 {0.0f, 0.0f, 0.0f}, glm::vec2 {-32.0f, -32.0f}); 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)); } bool GuiThemeDownloader::input(InputConfig* config, Input input) { 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; } } if (config->isMappedTo("b", input) && input.value) { delete this; return true; } if (config->isMappedTo("x", input) && input.value && mGrid.getSelectedComponent() == mList) { setupFullscreenViewer(); return true; } return GuiComponent::input(config, input); } std::vector GuiThemeDownloader::getHelpPrompts() { std::vector prompts {mGrid.getHelpPrompts()}; 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")); } return prompts; } bool GuiThemeDownloader::fetchThemesList() { const std::string repositoryName {"themes-list"}; const std::string url {"https://gitlab.com/es-de/themes/themes-list.git"}; const std::string path {mThemeDirectory + "themes-list"}; 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); mStatusType = StatusType::STATUS_UPDATING; mStatusText = "UPDATING THEMES LIST"; } else { LOG(LogInfo) << "GuiThemeDownloader: Creating initial themes list repository clone"; mFetchThread = std::thread(&GuiThemeDownloader::cloneRepository, this, repositoryName, url); mStatusType = StatusType::STATUS_DOWNLOADING; mStatusText = "DOWNLOADING THEMES LIST"; return false; } return false; } bool GuiThemeDownloader::cloneRepository(const std::string& repositoryName, const std::string& url) { int errorCode {0}; git_repository* repository {nullptr}; const std::string path {mThemeDirectory + repositoryName}; #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(stats->indexed_deltas) / static_cast(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(stats->received_objects) / static_cast(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 << "\""; mMessage = gitError->message; git_error_clear(); mPromise.set_value(true); return true; } mLatestThemesList = true; mPromise.set_value(true); return false; }