From cc8123f5a6092b95745474a50c98eca4ae18c8da Mon Sep 17 00:00:00 2001
From: Leon Styhre <leon@leonstyhre.com>
Date: Sun, 13 Feb 2022 20:03:34 +0100
Subject: [PATCH] Added a GameSelectorComponent for displaying game media in
 SystemView.

---
 es-app/src/CollectionSystemsManager.cpp       |   4 +
 es-app/src/FileData.cpp                       |  38 +++++-
 es-app/src/FileData.h                         |  14 +-
 es-core/CMakeLists.txt                        |   1 +
 es-core/src/ThemeData.cpp                     |   3 +
 .../src/components/GameSelectorComponent.h    | 124 ++++++++++++++++++
 6 files changed, 180 insertions(+), 4 deletions(-)
 create mode 100644 es-core/src/components/GameSelectorComponent.h

diff --git a/es-app/src/CollectionSystemsManager.cpp b/es-app/src/CollectionSystemsManager.cpp
index a5f1bb3ba..16fd70a29 100644
--- a/es-app/src/CollectionSystemsManager.cpp
+++ b/es-app/src/CollectionSystemsManager.cpp
@@ -1443,6 +1443,10 @@ void CollectionSystemsManager::trimCollectionCount(FileData* rootFolder, int lim
             (CollectionFileData*)rootFolder->getChildrenListToDisplay().back();
         ViewController::getInstance()->getGamelistView(curSys).get()->remove(gameToRemove, false);
     }
+    // Also update the lists of last played and most played games as these could otherwise
+    // contain dangling pointers.
+    rootFolder->updateLastPlayedList();
+    rootFolder->updateMostPlayedList();
 }
 
 const bool CollectionSystemsManager::themeFolderExists(const std::string& folder)
diff --git a/es-app/src/FileData.cpp b/es-app/src/FileData.cpp
index b68258d94..f811de823 100644
--- a/es-app/src/FileData.cpp
+++ b/es-app/src/FileData.cpp
@@ -39,6 +39,8 @@ FileData::FileData(FileType type,
     , mEnvData {envData}
     , mSystem {system}
     , mOnlyFolders {false}
+    , mUpdateChildrenLastPlayed {false}
+    , mUpdateChildrenMostPlayed {false}
     , mDeletionFlag {false}
 {
     // Metadata needs at least a name field (since that's what getName() will return).
@@ -736,6 +738,9 @@ void FileData::sort(const SortType& type, bool mFavoritesOnTop)
         sortFavoritesOnTop(*type.comparisonFunction, mGameCount);
     else
         sort(*type.comparisonFunction, mGameCount);
+
+    updateLastPlayedList();
+    updateMostPlayedList();
 }
 
 void FileData::countGames(std::pair<unsigned int, unsigned int>& gameCount)
@@ -743,9 +748,6 @@ void FileData::countGames(std::pair<unsigned int, unsigned int>& gameCount)
     bool isKidMode = (Settings::getInstance()->getString("UIMode") == "kid" ||
                       Settings::getInstance()->getBool("ForceKid"));
 
-    (Settings::getInstance()->getString("UIMode") == "kid" ||
-     Settings::getInstance()->getBool("ForceKid"));
-
     for (unsigned int i = 0; i < mChildren.size(); ++i) {
         if (mChildren[i]->getType() == GAME && mChildren[i]->getCountAsGame()) {
             if (!isKidMode || (isKidMode && mChildren[i]->getKidgame())) {
@@ -761,6 +763,36 @@ void FileData::countGames(std::pair<unsigned int, unsigned int>& gameCount)
     mGameCount = gameCount;
 }
 
+void FileData::updateLastPlayedList()
+{
+    if (!mUpdateChildrenLastPlayed)
+        return;
+
+    mChildrenLastPlayed.clear();
+    mChildrenLastPlayed = getChildrenRecursive();
+
+    std::stable_sort(mChildrenLastPlayed.begin(), mChildrenLastPlayed.end());
+    std::sort(std::begin(mChildrenLastPlayed), std::end(mChildrenLastPlayed),
+              [](FileData* a, FileData* b) {
+                  return a->metadata.get("lastplayed") > b->metadata.get("lastplayed");
+              });
+}
+
+void FileData::updateMostPlayedList()
+{
+    if (!mUpdateChildrenMostPlayed)
+        return;
+
+    mChildrenMostPlayed.clear();
+    mChildrenMostPlayed = getChildrenRecursive();
+
+    std::stable_sort(mChildrenMostPlayed.begin(), mChildrenMostPlayed.end());
+    std::sort(std::begin(mChildrenMostPlayed), std::end(mChildrenMostPlayed),
+              [](FileData* a, FileData* b) {
+                  return a->metadata.getInt("playcount") > b->metadata.getInt("playcount");
+              });
+}
+
 const FileData::SortType& FileData::getSortTypeFromString(const std::string& desc) const
 {
     std::vector<FileData::SortType> SortTypes = FileSorts::SortTypes;
diff --git a/es-app/src/FileData.h b/es-app/src/FileData.h
index 73332be6f..b40659000 100644
--- a/es-app/src/FileData.h
+++ b/es-app/src/FileData.h
@@ -56,6 +56,13 @@ public:
     const std::vector<FileData*>& getChildren() const { return mChildren; }
     SystemData* getSystem() const { return mSystem; }
     SystemEnvironmentData* getSystemEnvData() const { return mEnvData; }
+
+    // These functions are used by GameSelectorComponent.
+    const std::vector<FileData*>& getChildrenLastPlayed() const { return mChildrenLastPlayed; }
+    const std::vector<FileData*>& getChildrenMostPlayed() const { return mChildrenMostPlayed; }
+    void setUpdateChildrenLastPlayed(bool state) { mUpdateChildrenLastPlayed = state; }
+    void setUpdateChildrenMostPlayed(bool state) { mUpdateChildrenMostPlayed = state; }
+
     const bool getOnlyFoldersFlag() const { return mOnlyFolders; }
     const bool getHasFoldersFlag() const { return mHasFolders; }
     static const std::string getROMDirectory();
@@ -127,7 +134,8 @@ public:
     MetaDataList metadata;
     // Only count the games, a cheaper alternative to a full sort when that is not required.
     void countGames(std::pair<unsigned int, unsigned int>& gameCount);
-
+    void updateLastPlayedList();
+    void updateMostPlayedList();
     void setSortTypeString(std::string typestring) { mSortTypeString = typestring; }
     const std::string& getSortTypeString() const { return mSortTypeString; }
     const FileData::SortType& getSortTypeFromString(const std::string& desc) const;
@@ -146,10 +154,14 @@ private:
     std::unordered_map<std::string, FileData*> mChildrenByFilename;
     std::vector<FileData*> mChildren;
     std::vector<FileData*> mFilteredChildren;
+    std::vector<FileData*> mChildrenLastPlayed;
+    std::vector<FileData*> mChildrenMostPlayed;
     // The pair includes all games, and favorite games.
     std::pair<unsigned int, unsigned int> mGameCount;
     bool mOnlyFolders;
     bool mHasFolders;
+    bool mUpdateChildrenLastPlayed;
+    bool mUpdateChildrenMostPlayed;
     // Used for flagging a game for deletion from its gamelist.xml file.
     bool mDeletionFlag;
 };
diff --git a/es-core/CMakeLists.txt b/es-core/CMakeLists.txt
index ddbde8dc3..779cd75f2 100644
--- a/es-core/CMakeLists.txt
+++ b/es-core/CMakeLists.txt
@@ -42,6 +42,7 @@ set(CORE_HEADERS
     ${CMAKE_CURRENT_SOURCE_DIR}/src/components/DateTimeComponent.h
     ${CMAKE_CURRENT_SOURCE_DIR}/src/components/DateTimeEditComponent.h
     ${CMAKE_CURRENT_SOURCE_DIR}/src/components/FlexboxComponent.h
+    ${CMAKE_CURRENT_SOURCE_DIR}/src/components/GameSelectorComponent.h
     ${CMAKE_CURRENT_SOURCE_DIR}/src/components/GridTileComponent.h
     ${CMAKE_CURRENT_SOURCE_DIR}/src/components/HelpComponent.h
     ${CMAKE_CURRENT_SOURCE_DIR}/src/components/IList.h
diff --git a/es-core/src/ThemeData.cpp b/es-core/src/ThemeData.cpp
index 2ead841c9..d383deb92 100644
--- a/es-core/src/ThemeData.cpp
+++ b/es-core/src/ThemeData.cpp
@@ -279,6 +279,9 @@ std::map<std::string, std::map<std::string, ThemeData::ElementPropertyType>>
        {"forceUppercase", BOOLEAN}, // For backward compatibility with legacy themes.
        {"lineSpacing", FLOAT},
        {"zIndex", FLOAT}}},
+     {"gameselector",
+      {{"selection", STRING},
+       {"count", UNSIGNED_INTEGER}}},
      {"helpsystem",
       {{"pos", NORMALIZED_PAIR},
        {"origin", NORMALIZED_PAIR},
diff --git a/es-core/src/components/GameSelectorComponent.h b/es-core/src/components/GameSelectorComponent.h
new file mode 100644
index 000000000..26f72041a
--- /dev/null
+++ b/es-core/src/components/GameSelectorComponent.h
@@ -0,0 +1,124 @@
+//  SPDX-License-Identifier: MIT
+//
+//  EmulationStation Desktop Edition
+//  GameSelectorComponent.h
+//
+//  Makes a selection of games based on theme-controlled criteria.
+//
+
+#ifndef ES_CORE_COMPONENTS_GAME_SELECTOR_COMPONENT_H
+#define ES_CORE_COMPONENTS_GAME_SELECTOR_COMPONENT_H
+
+#include "GuiComponent.h"
+#include "Log.h"
+#include "ThemeData.h"
+
+class GameSelectorComponent : public GuiComponent
+{
+public:
+    GameSelectorComponent(SystemData* system)
+        : mSystem {system}
+        , mGameSelection {GameSelection::RANDOM}
+        , mGameCount {1}
+    {
+    }
+
+    const std::vector<FileData*>& getGames() const { return mGames; }
+
+    void refreshGames()
+    {
+        mGames.clear();
+
+        bool isKidMode {(Settings::getInstance()->getString("UIMode") == "kid" ||
+                         Settings::getInstance()->getBool("ForceKid"))};
+
+        if (mGameSelection == GameSelection::RANDOM) {
+            for (int i = 0; i < mGameCount; ++i) {
+                FileData* randomGame {mSystem->getRandomGame()};
+                if (randomGame != nullptr)
+                    mGames.emplace_back(randomGame);
+            }
+        }
+        else if (mGameSelection == GameSelection::LAST_PLAYED) {
+            for (auto& child : mSystem->getRootFolder()->getChildrenLastPlayed()) {
+                if (child->getType() != GAME)
+                    continue;
+                if (!child->getCountAsGame())
+                    continue;
+                if (isKidMode && !child->getKidgame())
+                    continue;
+                if (child->metadata.get("lastplayed") == "0")
+                    continue;
+                mGames.emplace_back(child);
+                if (static_cast<int>(mGames.size()) == mGameCount)
+                    break;
+            }
+        }
+        else if (mGameSelection == GameSelection::MOST_PLAYED) {
+            for (auto& child : mSystem->getRootFolder()->getChildrenMostPlayed()) {
+                if (child->getType() != GAME)
+                    continue;
+                if (!child->getCountAsGame())
+                    continue;
+                if (isKidMode && !child->getKidgame())
+                    continue;
+                if (child->metadata.get("playcount") == "0")
+                    continue;
+                mGames.emplace_back(child);
+                if (static_cast<int>(mGames.size()) == mGameCount)
+                    break;
+            }
+        }
+    }
+
+    void applyTheme(const std::shared_ptr<ThemeData>& theme,
+                    const std::string& view,
+                    const std::string& element,
+                    unsigned int properties)
+    {
+        const ThemeData::ThemeElement* elem {theme->getElement(view, element, "gameselector")};
+        if (!elem)
+            return;
+
+        if (elem->has("selection")) {
+            const std::string selection {elem->get<std::string>("selection")};
+            if (selection == "random") {
+                mGameSelection = GameSelection::RANDOM;
+            }
+            else if (selection == "lastplayed") {
+                mGameSelection = GameSelection::LAST_PLAYED;
+                mSystem->getRootFolder()->setUpdateChildrenLastPlayed(true);
+                mSystem->getRootFolder()->updateLastPlayedList();
+            }
+            else if (selection == "mostplayed") {
+                mGameSelection = GameSelection::MOST_PLAYED;
+                mSystem->getRootFolder()->setUpdateChildrenMostPlayed(true);
+                mSystem->getRootFolder()->updateMostPlayedList();
+            }
+            else {
+                mGameSelection = GameSelection::RANDOM;
+                LOG(LogWarning) << "GameSelectorComponent: Invalid theme configuration, property "
+                                   "<selection> set to \""
+                                << selection << "\"";
+            }
+        }
+
+        if (elem->has("count"))
+            mGameCount = glm::clamp(static_cast<int>(elem->get<unsigned int>("count")), 1, 30);
+    }
+
+private:
+    enum class GameSelection {
+        RANDOM, // Replace with AllowShortEnumsOnASingleLine: false (clang-format >=11.0).
+        LAST_PLAYED,
+        MOST_PLAYED
+    };
+
+    SystemData* mSystem;
+    std::vector<FileData*> mGames;
+
+    GameSelection mGameSelection;
+    int mGameCount;
+};
+
+#endif // ES_CORE_COMPONENTS_GAME_SELECTOR_COMPONENT_H