diff --git a/CMakeLists.txt b/CMakeLists.txt index 09fe248a6..d24e30115 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -135,6 +135,7 @@ endif() #------------------------------------------------------------------------------- #define basic sources and headers set(ES_HEADERS + ${CMAKE_CURRENT_SOURCE_DIR}/src/AsyncHandle.h ${CMAKE_CURRENT_SOURCE_DIR}/src/AudioManager.h ${CMAKE_CURRENT_SOURCE_DIR}/src/EmulationStation.h ${CMAKE_CURRENT_SOURCE_DIR}/src/FileData.h diff --git a/src/AsyncHandle.h b/src/AsyncHandle.h new file mode 100644 index 000000000..4d07de5be --- /dev/null +++ b/src/AsyncHandle.h @@ -0,0 +1,43 @@ +#pragma once + +enum AsyncHandleStatus +{ + ASYNC_IN_PROGRESS, + ASYNC_ERROR, + ASYNC_DONE +}; + +// Handle for some asynchronous operation. +class AsyncHandle +{ +public: + AsyncHandle() : mStatus(ASYNC_IN_PROGRESS) {}; + + virtual void update() = 0; + + // Update and return the latest status. + inline AsyncHandleStatus status() { update(); return mStatus; } + + // User-friendly string of our current status. Will return error message if status() == SEARCH_ERROR. + inline std::string getStatusString() + { + switch(mStatus) + { + case ASYNC_IN_PROGRESS: + return "in progress"; + case ASYNC_ERROR: + return mError; + case ASYNC_DONE: + return "done"; + default: + return "something impossible has occured; row, row, fight the power"; + } + } + +protected: + inline void setStatus(AsyncHandleStatus status) { mStatus = status; } + inline void setError(const std::string& error) { setStatus(ASYNC_ERROR); mError = error; } + + std::string mError; + AsyncHandleStatus mStatus; +}; diff --git a/src/HttpReq.cpp b/src/HttpReq.cpp index 867cec035..05423dfa0 100644 --- a/src/HttpReq.cpp +++ b/src/HttpReq.cpp @@ -144,14 +144,9 @@ HttpReq::Status HttpReq::status() return mStatus; } -std::string HttpReq::getContent() +std::string HttpReq::getContent() const { - if(mStatus != REQ_SUCCESS) - { - LOG(LogError) << "Called getContent() on an incomplete HttpReq!"; - return ""; - } - + assert(mStatus == REQ_SUCCESS); return mContent.str(); } diff --git a/src/HttpReq.h b/src/HttpReq.h index 44d214e27..9c4f22034 100644 --- a/src/HttpReq.h +++ b/src/HttpReq.h @@ -42,7 +42,7 @@ public: std::string getErrorMsg(); - std::string getContent(); + std::string getContent() const; // mStatus must be REQ_SUCCESS static std::string urlEncode(const std::string &s); static bool isUrl(const std::string& s); diff --git a/src/components/ComponentList.cpp b/src/components/ComponentList.cpp index 2b03190e2..96f3cebe7 100644 --- a/src/components/ComponentList.cpp +++ b/src/components/ComponentList.cpp @@ -116,6 +116,8 @@ void ComponentList::onCursorChanged(const CursorState& state) mCameraOffset = 0; else if(mCameraOffset + mSize.y() > totalHeight) mCameraOffset = totalHeight - mSize.y(); + }else{ + mCameraOffset = 0; } updateHelpPrompts(); diff --git a/src/components/OptionListComponent.h b/src/components/OptionListComponent.h index c60211996..ee95eaf60 100644 --- a/src/components/OptionListComponent.h +++ b/src/components/OptionListComponent.h @@ -259,7 +259,7 @@ private: ss << getSelectedObjects().size() << " SELECTED"; mText.setText(ss.str()); mText.setSize(0, mText.getSize().y()); - setSize(mText.getSize().x() + mRightArrow.getSize().x() + 16, mText.getSize().y()); + setSize(mText.getSize().x() + mRightArrow.getSize().x() + 24, mText.getSize().y()); if(mParent) // hack since theres no "on child size changed" callback atm... mParent->onSizeChanged(); }else{ @@ -270,7 +270,7 @@ private: { mText.setText(strToUpper(it->name)); mText.setSize(0, mText.getSize().y()); - setSize(mText.getSize().x() + mLeftArrow.getSize().x() + mRightArrow.getSize().x() + 16, mText.getSize().y()); + setSize(mText.getSize().x() + mLeftArrow.getSize().x() + mRightArrow.getSize().x() + 24, mText.getSize().y()); if(mParent) // hack since theres no "on child size changed" callback atm... mParent->onSizeChanged(); break; diff --git a/src/components/ScraperSearchComponent.cpp b/src/components/ScraperSearchComponent.cpp index d87414bfb..82c16c989 100644 --- a/src/components/ScraperSearchComponent.cpp +++ b/src/components/ScraperSearchComponent.cpp @@ -15,6 +15,8 @@ ScraperSearchComponent::ScraperSearchComponent(Window* window, SearchType type) { addChild(&mGrid); + mBlockAccept = false; + using namespace Eigen; // left spacer (empty component, needed for borders) @@ -87,6 +89,8 @@ void ScraperSearchComponent::search(const ScraperSearchParams& params) { mResultList->clear(); mScraperResults.clear(); + mThumbnailReq.reset(); + mMDResolveHandle.reset(); updateInfoPane(); mLastSearch = params; @@ -120,6 +124,7 @@ void ScraperSearchComponent::onSearchDone(const std::vector mGrid.resetCursor(); } + mBlockAccept = false; updateInfoPane(); if(mSearchType == ALWAYS_ACCEPT_FIRST_RESULT) @@ -177,6 +182,9 @@ bool ScraperSearchComponent::input(InputConfig* config, Input input) { if(config->isMappedTo("a", input) && input.value != 0) { + if(mBlockAccept) + return true; + //if you're on a result if(getSelectedIndex() != -1) { @@ -195,24 +203,28 @@ bool ScraperSearchComponent::input(InputConfig* config, Input input) return ret; } +void ScraperSearchComponent::render(const Eigen::Affine3f& parentTrans) +{ + Eigen::Affine3f trans = parentTrans * getTransform(); + + if(mBlockAccept) + { + Renderer::setMatrix(trans); + Renderer::drawRect((int)mResultList->getPosition().x(), (int)mResultList->getPosition().y(), + (int)mResultList->getSize().x(), (int)mResultList->getSize().y(), 0x00000011); + } + + renderChildren(trans); +} + void ScraperSearchComponent::returnResult(ScraperSearchResult result) { + mBlockAccept = true; + // resolve metadata image before returning if(!result.imageUrl.empty()) { - downloadImageAsync(mWindow, result.imageUrl, getSaveAsPath(mLastSearch, "image", result.imageUrl), - [this, result] (std::string filePath) mutable -> void - { - if(filePath.empty()) - { - onSearchError("Error downloading boxart."); - return; - } - - result.mdl.set("image", filePath); - result.imageUrl = ""; - this->returnResult(result); // re-enter this function - }); + mMDResolveHandle = resolveMetaDataAssets(result, mLastSearch); return; } @@ -226,19 +238,30 @@ void ScraperSearchComponent::update(int deltaTime) updateThumbnail(); } - if(mSearchHandle && mSearchHandle->status() != SEARCH_IN_PROGRESS) + if(mSearchHandle && mSearchHandle->status() != ASYNC_IN_PROGRESS) { - if(mSearchHandle->status() == SEARCH_DONE) + if(mSearchHandle->status() == ASYNC_DONE) { onSearchDone(mSearchHandle->getResults()); - }else if(mSearchHandle->status() == SEARCH_ERROR) + }else if(mSearchHandle->status() == ASYNC_ERROR) { onSearchError(mSearchHandle->getStatusString()); } - mSearchHandle.reset(); } + if(mMDResolveHandle && mMDResolveHandle->status() != ASYNC_IN_PROGRESS) + { + if(mMDResolveHandle->status() == ASYNC_DONE) + { + returnResult(mMDResolveHandle->getResult()); + }else if(mMDResolveHandle->status() == ASYNC_ERROR) + { + onSearchError(mMDResolveHandle->getStatusString()); + } + mMDResolveHandle.reset(); + } + GuiComponent::update(deltaTime); } diff --git a/src/components/ScraperSearchComponent.h b/src/components/ScraperSearchComponent.h index 1e6b134a1..e3adee360 100644 --- a/src/components/ScraperSearchComponent.h +++ b/src/components/ScraperSearchComponent.h @@ -34,6 +34,7 @@ public: bool input(InputConfig* config, Input input) override; void update(int deltaTime) override; + void render(const Eigen::Affine3f& parentTrans) override; std::vector getHelpPrompts() override; void onSizeChanged() override; void onFocusGained() override; @@ -65,8 +66,10 @@ private: std::function mAcceptCallback; std::function mSkipCallback; std::function mCancelCallback; + bool mBlockAccept; std::unique_ptr mSearchHandle; + std::unique_ptr mMDResolveHandle; std::vector mScraperResults; std::unique_ptr mThumbnailReq; }; diff --git a/src/scrapers/GamesDBScraper.cpp b/src/scrapers/GamesDBScraper.cpp index ab1e3a83d..83a04d3f2 100644 --- a/src/scrapers/GamesDBScraper.cpp +++ b/src/scrapers/GamesDBScraper.cpp @@ -79,11 +79,14 @@ std::unique_ptr GamesDBScraper::getResultsAsync(const Scrap GamesDBHandle::GamesDBHandle(const ScraperSearchParams& params, const std::string& url) : mReq(std::unique_ptr(new HttpReq(url))) { - setStatus(SEARCH_IN_PROGRESS); + setStatus(ASYNC_IN_PROGRESS); } void GamesDBHandle::update() { + if(mStatus == ASYNC_DONE) + return; + if(mReq->status() == HttpReq::REQ_IN_PROGRESS) return; @@ -156,7 +159,7 @@ void GamesDBHandle::update() game = game.next_sibling("Game"); } - setStatus(SEARCH_DONE); + setStatus(ASYNC_DONE); setResults(results); return; } diff --git a/src/scrapers/Scraper.cpp b/src/scrapers/Scraper.cpp index e730b9b15..3a450852f 100644 --- a/src/scrapers/Scraper.cpp +++ b/src/scrapers/Scraper.cpp @@ -9,48 +9,116 @@ #include "GamesDBScraper.h" #include "TheArchiveScraper.h" -std::string ScraperSearchHandle::getStatusString() +std::shared_ptr createScraperByName(const std::string& name) { - switch(mStatus) + if(name == "TheGamesDB") + return std::shared_ptr(new GamesDBScraper()); + else if(name == "TheArchive") + return std::shared_ptr(new TheArchiveScraper()); + + return nullptr; +} + +std::unique_ptr resolveMetaDataAssets(const ScraperSearchResult& result, const ScraperSearchParams& search) +{ + return std::unique_ptr(new MDResolveHandle(result, search)); +} + +MDResolveHandle::MDResolveHandle(const ScraperSearchResult& result, const ScraperSearchParams& search) : mResult(result) +{ + if(!result.imageUrl.empty()) { - case SEARCH_IN_PROGRESS: - return "search in progress"; - case SEARCH_ERROR: - return mError; - case SEARCH_DONE: - return "search done"; - default: - return "something impossible has occured"; + std::string imgPath = getSaveAsPath(search, "image", result.imageUrl); + mFuncs.push_back(ResolvePair(downloadImageAsync(result.imageUrl, imgPath), [this, imgPath] + { + mResult.mdl.set("image", imgPath); + mResult.imageUrl = ""; + })); } } -std::string processFileDownload(std::shared_ptr r, std::string saveAs) +void MDResolveHandle::update() { - if(r->status() != HttpReq::REQ_SUCCESS) + if(mStatus == ASYNC_DONE || mStatus == ASYNC_ERROR) + return; + + auto it = mFuncs.begin(); + while(it != mFuncs.end()) { - LOG(LogError) << "Failed to download file - HttpReq error: " << r->getErrorMsg(); - return ""; + if(it->first->status() == ASYNC_ERROR) + { + setError(it->first->getStatusString()); + return; + }else if(it->first->status() == ASYNC_DONE) + { + it->second(); + it = mFuncs.erase(it); + continue; + } + it++; } - std::ofstream stream(saveAs, std::ios_base::out | std::ios_base::binary); - if(stream.fail()) + if(mFuncs.empty()) + setStatus(ASYNC_DONE); +} + +std::unique_ptr downloadImageAsync(const std::string& url, const std::string& saveAs) +{ + return std::unique_ptr(new ImageDownloadHandle(url, saveAs, + Settings::getInstance()->getInt("ScraperResizeWidth"), Settings::getInstance()->getInt("ScraperResizeHeight"))); +} + +ImageDownloadHandle::ImageDownloadHandle(const std::string& url, const std::string& path, int maxWidth, int maxHeight) : + mSavePath(path), mMaxWidth(maxWidth), mMaxHeight(maxHeight), mReq(new HttpReq(url)) +{ +} + +void ImageDownloadHandle::update() +{ + if(mReq->status() == HttpReq::REQ_IN_PROGRESS) + return; + + if(mReq->status() != HttpReq::REQ_SUCCESS) { - LOG(LogError) << "Failed to open \"" << saveAs << "\" for writing downloaded file."; - return ""; + std::stringstream ss; + ss << "Network error: " << mReq->getErrorMsg(); + setError(ss.str()); + return; } - std::string content = r->getContent(); + // download is done, save it to disk + std::ofstream stream(mSavePath, std::ios_base::out | std::ios_base::binary); + if(stream.bad()) + { + setError("Failed to open image path to write. Permission error? Disk full?"); + return; + } + + const std::string& content = mReq->getContent(); stream.write(content.data(), content.length()); stream.close(); + if(stream.bad()) + { + setError("Failed to save image. Disk full?"); + return; + } - return saveAs; + // resize it + if(!resizeImage(mSavePath, mMaxWidth, mMaxHeight)) + { + setError("Error saving resized image. Out of memory? Disk full?"); + return; + } + + setStatus(ASYNC_DONE); } //you can pass 0 for width or height to keep aspect ratio -void resizeImage(const std::string& path, int maxWidth, int maxHeight) +bool resizeImage(const std::string& path, int maxWidth, int maxHeight) { + // nothing to do if(maxWidth == 0 && maxHeight == 0) - return; + return true; FREE_IMAGE_FORMAT format = FIF_UNKNOWN; FIBITMAP* image = NULL; @@ -62,7 +130,7 @@ void resizeImage(const std::string& path, int maxWidth, int maxHeight) if(format == FIF_UNKNOWN) { LOG(LogError) << "Error - could not detect filetype for image \"" << path << "\"!"; - return; + return false; } //make sure we can read this filetype first, then load it @@ -71,7 +139,7 @@ void resizeImage(const std::string& path, int maxWidth, int maxHeight) image = FreeImage_Load(format, path.c_str()); }else{ LOG(LogError) << "Error - file format reading not supported for image \"" << path << "\"!"; - return; + return false; } float width = (float)FreeImage_GetWidth(image); @@ -91,43 +159,16 @@ void resizeImage(const std::string& path, int maxWidth, int maxHeight) if(imageRescaled == NULL) { LOG(LogError) << "Could not resize image! (not enough memory? invalid bitdepth?)"; - return; - } - - if(!FreeImage_Save(format, imageRescaled, path.c_str())) - { - LOG(LogError) << "Failed to save resized image!"; + return false; } + bool saved = FreeImage_Save(format, imageRescaled, path.c_str()); FreeImage_Unload(imageRescaled); -} -void downloadImageAsync(Window* window, const std::string& url, const std::string& saveAs, std::function returnFunc) -{ - std::shared_ptr httpreq = std::make_shared(url); - AsyncReqComponent* req = new AsyncReqComponent(window, httpreq, - [returnFunc, saveAs] (std::shared_ptr r) - { - std::string file = processFileDownload(r, saveAs); - if(!file.empty()) - resizeImage(file, Settings::getInstance()->getInt("ScraperResizeWidth"), Settings::getInstance()->getInt("ScraperResizeHeight")); - returnFunc(file); - }, NULL); + if(!saved) + LOG(LogError) << "Failed to save resized image!"; - window->pushGui(req); -} - -std::string downloadImage(const std::string& url, const std::string& saveAs) -{ - std::shared_ptr httpreq = std::make_shared(url); - while(httpreq->status() == HttpReq::REQ_IN_PROGRESS); - - std::string file = processFileDownload(httpreq, saveAs); - - if(!file.empty()) - resizeImage(file, Settings::getInstance()->getInt("ScraperResizeWidth"), Settings::getInstance()->getInt("ScraperResizeHeight")); - - return file; + return saved; } std::string getSaveAsPath(const ScraperSearchParams& params, const std::string& suffix, const std::string& url) @@ -153,42 +194,3 @@ std::string getSaveAsPath(const ScraperSearchParams& params, const std::string& path += name + ext; return path; } - - -std::shared_ptr createScraperByName(const std::string& name) -{ - if(name == "TheGamesDB") - return std::shared_ptr(new GamesDBScraper()); - else if(name == "TheArchive") - return std::shared_ptr(new TheArchiveScraper()); - - return nullptr; -} - -void resolveMetaDataAssetsAsync(Window* window, const ScraperSearchParams& params, MetaDataList mdl, std::function returnFunc) -{ - const std::vector& mdd = params.game->metadata.getMDD(); - for(auto it = mdd.begin(); it != mdd.end(); it++) - { - std::string key = it->key; - std::string val = mdl.get(key); - if(it->type == MD_IMAGE_PATH && HttpReq::isUrl(val)) - { - downloadImageAsync(window, val, getSaveAsPath(params, key, val), [window, params, mdl, key, returnFunc] (std::string savedAs) mutable -> - void - { - if(savedAs.empty()) - { - //error - LOG(LogError) << "Could not resolve image for [" << getCleanFileName(params.game->getPath()) << "]! Skipping."; - } - - mdl.set(key, savedAs); - resolveMetaDataAssetsAsync(window, params, mdl, returnFunc); - }); - return; - } - } - - returnFunc(mdl); -} diff --git a/src/scrapers/Scraper.h b/src/scrapers/Scraper.h index ef0ce2207..21c7198f1 100644 --- a/src/scrapers/Scraper.h +++ b/src/scrapers/Scraper.h @@ -3,11 +3,10 @@ #include "../MetaData.h" #include "../SystemData.h" #include "../HttpReq.h" +#include "../AsyncHandle.h" #include #include -class Window; - struct ScraperSearchParams { SystemData* system; @@ -25,34 +24,16 @@ struct ScraperSearchResult std::string thumbnailUrl; }; -enum ScraperSearchStatus -{ - SEARCH_IN_PROGRESS, - SEARCH_ERROR, - SEARCH_DONE -}; - -class ScraperSearchHandle +class ScraperSearchHandle : public AsyncHandle { public: virtual void update() = 0; - - // Update and return the latest status. - inline ScraperSearchStatus status() { update(); return mStatus; } - - // User-friendly string of our current status. Will return error message if status() == SEARCH_ERROR. - std::string getStatusString(); - - inline const std::vector& getResults() const { assert(mStatus != SEARCH_IN_PROGRESS); return mResults; } + inline const std::vector& getResults() const { assert(mStatus != ASYNC_IN_PROGRESS); return mResults; } protected: - inline void setError(const std::string& error) { setStatus(SEARCH_ERROR); mError = error; } - inline void setStatus(ScraperSearchStatus status) { mStatus = status; } inline void setResults(const std::vector& results) { mResults = results; } private: - std::string mError; - ScraperSearchStatus mStatus; std::vector mResults; }; @@ -67,23 +48,48 @@ public: std::shared_ptr createScraperByName(const std::string& name); + +// Meta data asset downloading stuff. +class MDResolveHandle : public AsyncHandle +{ +public: + MDResolveHandle(const ScraperSearchResult& result, const ScraperSearchParams& search); + + void update() override; + inline const ScraperSearchResult& getResult() const { assert(mStatus == ASYNC_DONE); return mResult; } + +private: + ScraperSearchResult mResult; + + typedef std::pair< std::unique_ptr, std::function > ResolvePair; + std::vector mFuncs; +}; + +class ImageDownloadHandle : public AsyncHandle +{ +public: + ImageDownloadHandle(const std::string& url, const std::string& path, int maxWidth, int maxHeight); + + void update() override; + +private: + std::unique_ptr mReq; + std::string mSavePath; + int mMaxWidth; + int mMaxHeight; +}; + //About the same as "~/.emulationstation/downloaded_images/[system_name]/[game_name].[url's extension]". //Will create the "downloaded_images" and "subdirectory" directories if they do not exist. std::string getSaveAsPath(const ScraperSearchParams& params, const std::string& suffix, const std::string& url); -//Returns the path to the downloaded file (saveAs) on completion. -//Returns empty string if an error occured. //Will resize according to Settings::getInt("ScraperResizeWidth") and Settings::getInt("ScraperResizeHeight"). -std::string downloadImage(const std::string& url, const std::string& saveAs); +std::unique_ptr downloadImageAsync(const std::string& url, const std::string& saveAs); -//Returns (via returnFunc) the path to the downloaded file (saveAs) on completion. -//Returns empty string if an error occured. -//Will resize according to Settings::getInt("ScraperResizeWidth") and Settings::getInt("ScraperResizeHeight"). -//Same as downloadImage, just async. -void downloadImageAsync(Window* window, const std::string& url, const std::string& saveAs, std::function returnFunc); - -void resolveMetaDataAssetsAsync(Window* window, const ScraperSearchParams& params, MetaDataList mdl, std::function returnFunc); +// Resolves all metadata assets that need to be downloaded. +std::unique_ptr resolveMetaDataAssets(const ScraperSearchResult& result, const ScraperSearchParams& search); //You can pass 0 for maxWidth or maxHeight to automatically keep the aspect ratio. //Will overwrite the image at [path] with the new resized one. -void resizeImage(const std::string& path, int maxWidth, int maxHeight); +//Returns true if successful, false otherwise. +bool resizeImage(const std::string& path, int maxWidth, int maxHeight); diff --git a/src/scrapers/TheArchiveScraper.cpp b/src/scrapers/TheArchiveScraper.cpp index fa008bd21..0cfbe4891 100644 --- a/src/scrapers/TheArchiveScraper.cpp +++ b/src/scrapers/TheArchiveScraper.cpp @@ -25,12 +25,12 @@ std::unique_ptr TheArchiveScraper::getResultsAsync(const Sc TheArchiveHandle::TheArchiveHandle(const ScraperSearchParams& params, const std::string& url) : mReq(std::unique_ptr(new HttpReq(url))) { - setStatus(SEARCH_IN_PROGRESS); + setStatus(ASYNC_IN_PROGRESS); } void TheArchiveHandle::update() { - if(status() == SEARCH_DONE) + if(mStatus == ASYNC_DONE) return; if(mReq->status() == HttpReq::REQ_IN_PROGRESS) @@ -91,6 +91,6 @@ void TheArchiveHandle::update() game = game.next_sibling("game"); } - setStatus(SEARCH_DONE); + setStatus(ASYNC_DONE); setResults(results); }