From 5d15eb0215dc3435776049d0ccde2e4134f65f6d Mon Sep 17 00:00:00 2001
From: altalk23 <45172705+altalk23@users.noreply.github.com>
Date: Tue, 5 Sep 2023 02:22:57 +0300
Subject: [PATCH] Implement UI for multiple version downloading (very cursed)

---
 loader/include/Geode/loader/Index.hpp         |  10 +
 loader/src/loader/Index.cpp                   |  33 ++-
 loader/src/ui/internal/info/ModInfoPopup.cpp  | 241 +++++++-----------
 loader/src/ui/internal/info/ModInfoPopup.hpp  |  12 +-
 .../src/ui/internal/list/InstallListCell.cpp  | 176 +++++++++----
 .../src/ui/internal/list/InstallListCell.hpp  |  37 ++-
 .../src/ui/internal/list/InstallListPopup.cpp |  72 +++++-
 .../src/ui/internal/list/InstallListPopup.hpp |  21 ++
 loader/src/ui/internal/list/ModListLayer.cpp  |   2 +-
 9 files changed, 399 insertions(+), 205 deletions(-)

diff --git a/loader/include/Geode/loader/Index.hpp b/loader/include/Geode/loader/Index.hpp
index 5d48430f..fda97552 100644
--- a/loader/include/Geode/loader/Index.hpp
+++ b/loader/include/Geode/loader/Index.hpp
@@ -178,12 +178,22 @@ namespace geode {
          * Get all featured index items
          */
         std::vector<IndexItemHandle> getFeaturedItems() const;
+        /**
+         * Get all latest index items
+         */
+        std::vector<IndexItemHandle> getLatestItems() const;
         /**
          * Get all index items by a developer
          */
         std::vector<IndexItemHandle> getItemsByDeveloper(
             std::string const& name
         ) const;
+        /**
+         * Get all index items for a specific mod
+         */
+        std::vector<IndexItemHandle> getItemsByModID(
+            std::string const& modID
+        ) const;
         /**
          * Check if an item with this ID is found on the index, and optionally 
          * provide the version sought after
diff --git a/loader/src/loader/Index.cpp b/loader/src/loader/Index.cpp
index a8e7ea9b..d4b0519f 100644
--- a/loader/src/loader/Index.cpp
+++ b/loader/src/loader/Index.cpp
@@ -183,7 +183,14 @@ Result<IndexItemHandle> IndexItem::Impl::create(ghc::filesystem::path const& roo
 }
 
 bool IndexItem::Impl::isInstalled() const {
-    return ghc::filesystem::exists(dirs::getModsDir() / (m_metadata.getID() + ".geode"));
+    if (!Loader::get()->isModInstalled(m_metadata.getID())) {
+        return false;
+    }
+    auto installed = Loader::get()->getInstalledMod(m_metadata.getID());
+    if (installed->getVersion() != m_metadata.getVersion()) {
+        return false;
+    }
+    return true;
 }
 
 // Helpers
@@ -449,6 +456,14 @@ std::vector<IndexItemHandle> Index::getItems() const {
     return res;
 }
 
+std::vector<IndexItemHandle> Index::getLatestItems() const {
+    std::vector<IndexItemHandle> res;
+    for (auto& [modID, versions] : m_impl->m_items) {
+        res.push_back(this->getMajorItem(modID));
+    }
+    return res;
+}
+
 std::vector<IndexItemHandle> Index::getFeaturedItems() const {
     std::vector<IndexItemHandle> res;
     for (auto& items : map::values(m_impl->m_items)) {
@@ -475,6 +490,18 @@ std::vector<IndexItemHandle> Index::getItemsByDeveloper(
     return res;
 }
 
+std::vector<IndexItemHandle> Index::getItemsByModID(
+    std::string const& modID
+) const {
+    std::vector<IndexItemHandle> res;
+    if (m_impl->m_items.count(modID)) {
+        for (auto& [versionStr, item] : m_impl->m_items[modID]) {
+            res.push_back(item);
+        }
+    }
+    return res;
+}
+
 bool Index::isKnownItem(
     std::string const& id,
     std::optional<VersionInfo> version
@@ -758,6 +785,10 @@ void Index::cancelInstall(IndexItemHandle item) {
 }
 
 void Index::install(IndexInstallList const& list) {
+    if (list.list.empty()) {
+        ModInstallEvent(list.target->getMetadata().getID(), UpdateFinished()).post();
+        return;
+    }
     Loader::get()->queueInMainThread([this, list]() {
         m_impl->installNext(0, list);
     });
diff --git a/loader/src/ui/internal/info/ModInfoPopup.cpp b/loader/src/ui/internal/info/ModInfoPopup.cpp
index 4173072d..10f1f38d 100644
--- a/loader/src/ui/internal/info/ModInfoPopup.cpp
+++ b/loader/src/ui/internal/info/ModInfoPopup.cpp
@@ -277,6 +277,95 @@ void ModInfoPopup::setInstallStatus(std::optional<UpdateProgress> const& progres
     }
 }
 
+void ModInfoPopup::popupInstallItem(IndexItemHandle item) {
+    auto deps = item->getMetadata().getDependencies();
+    enum class DepState {
+        None,
+        HasOnlyRequired,
+        HasOptional
+    } depState = DepState::None;
+    for (auto const& dep : deps) {
+        // resolved means it's already installed, so
+        // no need to ask the user whether they want to install it
+        if (Loader::get()->isModLoaded(dep.id))
+            continue;
+        if (dep.importance != ModMetadata::Dependency::Importance::Required) {
+            depState = DepState::HasOptional;
+            break;
+        }
+        depState = DepState::HasOnlyRequired;
+    }
+
+    std::string content;
+    char const* btn1;
+    char const* btn2;
+    switch (depState) {
+        case DepState::None:
+            content = fmt::format(
+                "Are you sure you want to install <cg>{}</c>?",
+                item->getMetadata().getName()
+            );
+            btn1 = "Info";
+            btn2 = "Install";
+            break;
+        case DepState::HasOnlyRequired:
+            content =
+                "Installing this mod requires other mods to be installed. "
+                "Would you like to <cy>proceed</c> with the installation or "
+                "<cb>view</c> which mods are going to be installed?";
+            btn1 = "View";
+            btn2 = "Proceed";
+            break;
+        case DepState::HasOptional:
+            content =
+                "This mod recommends installing other mods alongside it. "
+                "Would you like to continue with <cy>recommended settings</c> or "
+                "<cb>customize</c> which mods to install?";
+            btn1 = "Customize";
+            btn2 = "Recommended";
+            break;
+    }
+
+    createQuickPopup("Confirm Install", content, btn1, btn2, 320.f, [&](FLAlertLayer*, bool btn2) {
+        if (btn2) {
+            auto canInstall = Index::get()->canInstall(m_item);
+            if (!canInstall) {
+                FLAlertLayer::create(
+                    "Unable to Install",
+                    canInstall.unwrapErr(),
+                    "OK"
+                )->show();
+                return;
+            }
+            this->preInstall();
+            Index::get()->install(m_item);
+        }
+        else {
+            InstallListPopup::create(m_item, [&](IndexInstallList const& list) {
+                this->preInstall();
+                Index::get()->install(list);
+            })->show();
+        }
+    }, true, true);
+}
+
+void ModInfoPopup::preInstall() {
+    if (m_latestVersionLabel) {
+        m_latestVersionLabel->setVisible(false);
+    }
+    this->setInstallStatus(UpdateProgress(0, "Starting install"));
+
+    m_installBtn->setTarget(
+        this, menu_selector(ModInfoPopup::onCancelInstall)
+    );
+    m_installBtnSpr->setString("Cancel");
+    m_installBtnSpr->setBG("GJ_button_06.png", false);
+}
+
+void ModInfoPopup::onCancelInstall(CCObject*) {
+    Index::get()->cancelInstall(m_item);
+}
+
 // LocalModInfoPopup
 LocalModInfoPopup::LocalModInfoPopup()
   : m_installListener(
@@ -493,61 +582,7 @@ void LocalModInfoPopup::onUpdateProgress(ModInstallEvent* event) {
 }
 
 void LocalModInfoPopup::onUpdate(CCObject*) {
-    auto list = Index::get()->getInstallList(m_item);
-    if (!list) {
-        return FLAlertLayer::create(
-            "Unable to Update",
-            list.unwrapErr(),
-            "OK"
-        )->show();
-    }
-    auto layer = FLAlertLayer::create(
-        this,
-        "Confirm Update",
-        fmt::format(
-            "The following mods will be updated:\n {}",
-            // le nest
-            ranges::join(
-                ranges::map<std::vector<std::string>>(
-                    list.unwrap().list,
-                    [](IndexItemHandle handle) {
-                        return fmt::format(
-                            " - <cr>{}</c> (<cy>{}</c>)",
-                            handle->getMetadata().getName(),
-                            handle->getMetadata().getID()
-                        );
-                    }
-                ),
-                "\n "
-            )
-        ),
-        "Cancel", "OK"
-    );
-    layer->setTag(TAG_CONFIRM_UPDATE);
-    layer->show();
-}
-
-void LocalModInfoPopup::onCancel(CCObject*) {
-    Index::get()->cancelInstall(m_item);
-}
-
-void LocalModInfoPopup::doUpdate() {
-    if (m_latestVersionLabel) {
-        m_latestVersionLabel->setVisible(false);
-    }
-
-    if (m_minorVersionLabel) {
-        m_minorVersionLabel->setVisible(false);
-    }
-    this->setInstallStatus(UpdateProgress(0, "Starting update"));
-
-    m_installBtn->setTarget(
-        this, menu_selector(LocalModInfoPopup::onCancel)
-    );
-    m_installBtnSpr->setString("Cancel");
-    m_installBtnSpr->setBG("GJ_button_06.png", false);
-
-    Index::get()->install(m_item);
+    this->popupInstallItem(m_item);
 }
 
 void LocalModInfoPopup::onUninstall(CCObject*) {
@@ -631,12 +666,6 @@ void LocalModInfoPopup::FLAlert_Clicked(FLAlertLayer* layer, bool btn2) {
             }
             this->onClose(nullptr);
         } break;
-
-        case TAG_CONFIRM_UPDATE: {
-            if (btn2) {
-                this->doUpdate();
-            }
-        } break;
     }
 }
 
@@ -687,7 +716,8 @@ bool IndexItemInfoPopup::init(IndexItemHandle item, ModListLayer* list) {
 
     if (!ModInfoPopup::init(item->getMetadata(), list)) return false;
 
-    if (item->isInstalled()) return true;
+    // bruh why is this here if we are allowing for browsing already installed mods
+    // if (item->isInstalled()) return true;
 
     m_installBtnSpr = IconButtonSprite::create(
         "GE_button_01.png"_spr,
@@ -750,92 +780,7 @@ void IndexItemInfoPopup::onInstallProgress(ModInstallEvent* event) {
 }
 
 void IndexItemInfoPopup::onInstall(CCObject*) {
-    auto deps = m_item->getMetadata().getDependencies();
-    enum class DepState {
-        None,
-        HasOnlyRequired,
-        HasOptional
-    } depState = DepState::None;
-    for (auto const& item : deps) {
-        // resolved means it's already installed, so
-        // no need to ask the user whether they want to install it
-        if (Loader::get()->isModLoaded(item.id))
-            continue;
-        if (item.importance != ModMetadata::Dependency::Importance::Required) {
-            depState = DepState::HasOptional;
-            break;
-        }
-        depState = DepState::HasOnlyRequired;
-    }
-
-    std::string content;
-    char const* btn1;
-    char const* btn2;
-    switch (depState) {
-        case DepState::None:
-            content = fmt::format(
-                "Are you sure you want to install <cg>{}</c>?",
-                m_item->getMetadata().getName()
-            );
-            btn1 = "Info";
-            btn2 = "Install";
-            break;
-        case DepState::HasOnlyRequired:
-            content =
-                "Installing this mod requires other mods to be installed. "
-                "Would you like to <cy>proceed</c> with the installation or "
-                "<cb>view</c> which mods are going to be installed?";
-            btn1 = "View";
-            btn2 = "Proceed";
-            break;
-        case DepState::HasOptional:
-            content =
-                "This mod recommends installing other mods alongside it. "
-                "Would you like to continue with <cy>recommended settings</c> or "
-                "<cb>customize</c> which mods to install?";
-            btn1 = "Customize";
-            btn2 = "Recommended";
-            break;
-    }
-
-    createQuickPopup("Confirm Install", content, btn1, btn2, 320.f, [&](FLAlertLayer*, bool btn2) {
-        if (btn2) {
-            auto canInstall = Index::get()->canInstall(m_item);
-            if (!canInstall) {
-                FLAlertLayer::create(
-                    "Unable to Install",
-                    canInstall.unwrapErr(),
-                    "OK"
-                )->show();
-                return;
-            }
-            this->preInstall();
-            Index::get()->install(m_item);
-        }
-        else {
-            InstallListPopup::create(m_item, [&](IndexInstallList const& list) {
-                this->preInstall();
-                Index::get()->install(list);
-            })->show();
-        }
-    }, true, true);
-}
-
-void IndexItemInfoPopup::preInstall() {
-    if (m_latestVersionLabel) {
-        m_latestVersionLabel->setVisible(false);
-    }
-    this->setInstallStatus(UpdateProgress(0, "Starting install"));
-
-    m_installBtn->setTarget(
-        this, menu_selector(IndexItemInfoPopup::onCancel)
-    );
-    m_installBtnSpr->setString("Cancel");
-    m_installBtnSpr->setBG("GJ_button_06.png", false);
-}
-
-void IndexItemInfoPopup::onCancel(CCObject*) {
-    Index::get()->cancelInstall(m_item);
+    this->popupInstallItem(m_item);
 }
 
 CCNode* IndexItemInfoPopup::createLogo(CCSize const& size) {
diff --git a/loader/src/ui/internal/info/ModInfoPopup.hpp b/loader/src/ui/internal/info/ModInfoPopup.hpp
index 062230a0..7870dc9c 100644
--- a/loader/src/ui/internal/info/ModInfoPopup.hpp
+++ b/loader/src/ui/internal/info/ModInfoPopup.hpp
@@ -38,6 +38,7 @@ protected:
     MDTextArea* m_detailsArea;
     MDTextArea* m_changelogArea = nullptr;
     Scrollbar* m_scrollbar;
+    IndexItemHandle m_item;
 
     void onChangelog(CCObject*);
     void onRepository(CCObject*);
@@ -51,13 +52,16 @@ protected:
 
     void setInstallStatus(std::optional<UpdateProgress> const& progress);
 
+    void popupInstallItem(IndexItemHandle item);
+    void preInstall();
+    void onCancelInstall(CCObject*);
+
     virtual CCNode* createLogo(CCSize const& size) = 0;
     virtual ModMetadata getMetadata() const = 0;
 };
 
 class LocalModInfoPopup : public ModInfoPopup, public FLAlertLayerProtocol {
 protected:
-    IndexItemHandle m_item;
     EventListener<ModInstallFilter> m_installListener;
     Mod* m_mod;
 
@@ -74,8 +78,6 @@ protected:
 
     void onUpdateProgress(ModInstallEvent* event);
     void onUpdate(CCObject*);
-    void onCancel(CCObject*);
-    void doUpdate();
 
 
     void FLAlert_Clicked(FLAlertLayer*, bool) override;
@@ -91,16 +93,12 @@ public:
 
 class IndexItemInfoPopup : public ModInfoPopup {
 protected:
-    IndexItemHandle m_item;
     EventListener<ModInstallFilter> m_installListener;
 
     bool init(IndexItemHandle item, ModListLayer* list);
 
     void onInstallProgress(ModInstallEvent* event);
     void onInstall(CCObject*);
-    void onCancel(CCObject*);
-
-    void preInstall();
 
     CCNode* createLogo(CCSize const& size) override;
     ModMetadata getMetadata() const override;
diff --git a/loader/src/ui/internal/list/InstallListCell.cpp b/loader/src/ui/internal/list/InstallListCell.cpp
index 0ba4be3e..07daca20 100644
--- a/loader/src/ui/internal/list/InstallListCell.cpp
+++ b/loader/src/ui/internal/list/InstallListCell.cpp
@@ -8,7 +8,6 @@
 #include <Geode/ui/GeodeUI.hpp>
 #include <loader/LoaderImpl.hpp>
 #include <utility>
-#include "../info/TagNode.hpp"
 #include "../info/DevProfilePopup.hpp"
 
 // InstallListCell
@@ -27,8 +26,11 @@ void InstallListCell::setupInfo(
     std::variant<VersionInfo, ComparableVersionInfo> version,
     bool inactive
 ) {
+    m_inactive = inactive;
     m_menu = CCMenu::create();
-    m_menu->setPosition(m_width - 10.f, m_height / 2);
+    m_menu->setPosition(0, 0);
+    m_menu->setAnchorPoint({ .0f, .0f });
+    m_menu->setContentSize({m_width, m_height});
     this->addChild(m_menu);
 
     auto logoSize = this->getLogoSize();
@@ -41,15 +43,15 @@ void InstallListCell::setupInfo(
     }
     this->addChild(logoSpr);
 
-    auto titleLabel = CCLabelBMFont::create(name.c_str(), "bigFont.fnt");
-    titleLabel->setAnchorPoint({ .0f, .5f });
-    titleLabel->setPositionX(m_height / 2 + logoSize / 2 + 13.f);
-    titleLabel->setPositionY(m_height / 2);
-    titleLabel->limitLabelWidth(m_width / 2 - 70.f, .4f, .1f);
+    m_titleLabel = CCLabelBMFont::create(name.c_str(), "bigFont.fnt");
+    m_titleLabel->setAnchorPoint({ .0f, .5f });
+    m_titleLabel->setPositionX(m_height / 2 + logoSize / 2 + 13.f);
+    m_titleLabel->setPositionY(m_height / 2);
+    m_titleLabel->limitLabelWidth(m_width / 2 - 70.f, .4f, .1f);
     if (inactive) {
-        titleLabel->setColor({ 163, 163, 163 });
+        m_titleLabel->setColor({ 163, 163, 163 });
     }
-    this->addChild(titleLabel);
+    this->addChild(m_titleLabel);
 
     m_developerBtn = nullptr;
     if (developer) {
@@ -64,43 +66,55 @@ void InstallListCell::setupInfo(
             creatorLabel, this, menu_selector(InstallListCell::onViewDev)
         );
         m_developerBtn->setPosition(
-            titleLabel->getPositionX() + titleLabel->getScaledContentSize().width + 3.f +
-                creatorLabel->getScaledContentSize().width / 2 -
-                m_menu->getPositionX(),
-            -0.5f
+            m_titleLabel->getPositionX() + m_titleLabel->getScaledContentSize().width + 3.f +
+                creatorLabel->getScaledContentSize().width / 2,
+            m_height / 2
         );
         m_menu->addChild(m_developerBtn);
     }
 
-    auto versionLabel = CCLabelBMFont::create(
+    this->setupVersion(version);
+}
+
+void InstallListCell::setupVersion(std::variant<VersionInfo, ComparableVersionInfo> version) {
+    if (m_versionLabel) {
+        m_versionLabel->removeFromParent();
+        m_versionLabel = nullptr;
+    }
+    if (m_tagLabel) {
+        m_tagLabel->removeFromParent();
+        m_tagLabel = nullptr;
+    }
+
+    m_versionLabel = CCLabelBMFont::create(
         std::holds_alternative<VersionInfo>(version) ?
             std::get<VersionInfo>(version).toString(false).c_str() :
             std::get<ComparableVersionInfo>(version).toString().c_str(),
         "bigFont.fnt"
     );
-    versionLabel->setAnchorPoint({ .0f, .5f });
-    versionLabel->setScale(.2f);
-    versionLabel->setPosition(
-        titleLabel->getPositionX() + titleLabel->getScaledContentSize().width + 3.f +
+    m_versionLabel->setAnchorPoint({ .0f, .5f });
+    m_versionLabel->setScale(.2f);
+    m_versionLabel->setPosition(
+        m_titleLabel->getPositionX() + m_titleLabel->getScaledContentSize().width + 3.f +
             (m_developerBtn ? m_developerBtn->getScaledContentSize().width + 3.f : 0.f),
-        titleLabel->getPositionY() - 1.f
+        m_titleLabel->getPositionY() - 1.f
     );
-    versionLabel->setColor({ 0, 255, 0 });
-    if (inactive) {
-        versionLabel->setColor({ 0, 163, 0 });
+    m_versionLabel->setColor({ 0, 255, 0 });
+    if (m_inactive) {
+        m_versionLabel->setColor({ 0, 163, 0 });
     }
-    this->addChild(versionLabel);
+    this->addChild(m_versionLabel);
 
     if (!std::holds_alternative<VersionInfo>(version)) return;
     if (auto tag = std::get<VersionInfo>(version).getTag()) {
-        auto tagLabel = TagNode::create(tag->toString());
-        tagLabel->setAnchorPoint({.0f, .5f});
-        tagLabel->setScale(.2f);
-        tagLabel->setPosition(
-            versionLabel->getPositionX() + versionLabel->getScaledContentSize().width + 3.f,
-            versionLabel->getPositionY()
+        m_tagLabel = TagNode::create(tag->toString());
+        m_tagLabel->setAnchorPoint({.0f, .5f});
+        m_tagLabel->setScale(.2f);
+        m_tagLabel->setPosition(
+            m_versionLabel->getPositionX() + m_versionLabel->getScaledContentSize().width + 3.f,
+            m_versionLabel->getPositionY()
         );
-        this->addChild(tagLabel);
+        this->addChild(m_tagLabel);
     }
 }
 
@@ -134,7 +148,7 @@ bool ModInstallListCell::init(Mod* mod, InstallListPopup* list, CCSize const& si
     this->setupInfo(mod->getMetadata(), true);
     auto message = CCLabelBMFont::create("Installed", "bigFont.fnt");
     message->setAnchorPoint({ 1.f, .5f });
-    message->setPositionX(m_menu->getPositionX());
+    message->setPositionX(m_width - 10.0f);
     message->setPositionY(16.f);
     message->setScale(0.4f);
     message->setColor({ 163, 163, 163 });
@@ -174,23 +188,38 @@ bool IndexItemInstallListCell::init(
         return false;
     m_item = item;
     this->setupInfo(item->getMetadata(), item->isInstalled());
-    if (item->isInstalled()) {
-        auto message = CCLabelBMFont::create("Installed", "bigFont.fnt");
-        message->setAnchorPoint({ 1.f, .5f });
-        message->setPositionX(m_menu->getPositionX());
-        message->setPositionY(16.f);
-        message->setScale(0.4f);
-        message->setColor({ 163, 163, 163 });
-        this->addChild(message);
-        return true;
-    }
+
+    // TODO: show installed label properly
+    // if (item->isInstalled()) {
+    //     auto message = CCLabelBMFont::create("Installed", "bigFont.fnt");
+    //     message->setAnchorPoint({ 1.f, .5f });
+    //     message->setPositionX(m_width - 10.0f);
+    //     message->setPositionY(16.f);
+    //     message->setScale(0.4f);
+    //     message->setColor({ 163, 163, 163 });
+    //     this->addChild(message);
+    //     return true;
+    // }
 
     m_toggle = CCMenuItemToggler::createWithStandardSprites(
         m_layer,
         menu_selector(InstallListPopup::onCellToggle),
         .6f
     );
-    m_toggle->setPosition(-m_toggle->getScaledContentSize().width / 2, 0.f);
+    m_toggle->setAnchorPoint({1.f, .5f});
+    m_toggle->setPosition(m_width - 5, m_height / 2);
+
+    // recycling sprites in my Geode?? noo never
+    auto versionSelectSpr = EditorButtonSprite::createWithSpriteFrameName(
+        "filters.png"_spr, 1.0f, EditorBaseColor::Gray
+    );
+    versionSelectSpr->setScale(.7f);
+
+    auto versionSelectBtn =
+        CCMenuItemSpriteExtra::create(versionSelectSpr, this, menu_selector(IndexItemInstallListCell::onSelectVersion));
+    versionSelectBtn->setAnchorPoint({1.f, .5f});
+    versionSelectBtn->setPosition({m_toggle->getPositionX() - m_toggle->getContentSize().width - 5, m_height / 2});
+    m_menu->addChild(versionSelectBtn);
 
     switch (importance) {
         case ModMetadata::Dependency::Importance::Required:
@@ -207,13 +236,18 @@ bool IndexItemInstallListCell::init(
             break;
     }
 
+    if (item->isInstalled()) {
+        m_toggle->setClickable(false);
+        m_toggle->toggle(true);
+    }
+
     if (m_item->getAvailablePlatforms().count(GEODE_PLATFORM_TARGET) == 0) {
         m_toggle->setClickable(false);
         m_toggle->toggle(false);
 
         auto message = CCLabelBMFont::create("N/A", "bigFont.fnt");
         message->setAnchorPoint({ 1.f, .5f });
-        message->setPositionX(m_menu->getPositionX() - m_toggle->getScaledContentSize().width - 5.f);
+        message->setPositionX(m_width - 5.f);
         message->setPositionY(16.f);
         message->setScale(0.4f);
         message->setColor({ 240, 31, 31 });
@@ -269,6 +303,15 @@ IndexItemHandle IndexItemInstallListCell::getItem() {
     return m_item;
 }
 
+void IndexItemInstallListCell::setVersionFromItem(IndexItemHandle item) {
+    this->setupVersion(item->getMetadata().getVersion());
+    m_item = item;
+}
+
+void IndexItemInstallListCell::onSelectVersion(CCObject*) {
+    SelectVersionPopup::create(m_item->getMetadata().getID(), this)->show();
+}
+
 // UnknownInstallListCell
 
 bool UnknownInstallListCell::init(
@@ -317,3 +360,50 @@ std::string UnknownInstallListCell::getID() const {
 std::string UnknownInstallListCell::getDeveloper() const {
     return "";
 }
+
+// SelectVersionCell
+
+bool SelectVersionCell::init(IndexItemHandle item, SelectVersionPopup* versionPopup, CCSize const& size) {
+    if (!InstallListCell::init(nullptr, size))
+        return false;
+    m_item = item;
+    m_versionPopup = versionPopup;
+    this->setupInfo(item->getMetadata(), item->isInstalled());
+
+    auto selectSpr = ButtonSprite::create(
+        "Select", 0, 0, "bigFont.fnt", "GJ_button_01.png", 0, .6f
+    );
+    selectSpr->setScale(.6f);
+
+    auto selectBtn = CCMenuItemSpriteExtra::create(
+        selectSpr, this, menu_selector(SelectVersionCell::onSelected)
+    );
+    selectBtn->setAnchorPoint({1.f, .5f});
+    selectBtn->setPosition({m_width - 5, m_height / 2});
+    m_menu->addChild(selectBtn);
+
+    return true;
+}
+
+void SelectVersionCell::onSelected(CCObject*) {
+    m_versionPopup->selectItem(m_item);
+}
+
+SelectVersionCell* SelectVersionCell::create(IndexItemHandle item, SelectVersionPopup* versionPopup, CCSize const& size) {
+    auto ret = new SelectVersionCell();
+    if (ret->init(item, versionPopup, size)) {
+        return ret;
+    }
+    CC_SAFE_DELETE(ret);
+    return nullptr;
+}
+
+CCNode* SelectVersionCell::createLogo(CCSize const& size) {
+    return geode::createIndexItemLogo(m_item, size);
+}
+std::string SelectVersionCell::getID() const {
+    return m_item->getMetadata().getID();
+}
+std::string SelectVersionCell::getDeveloper() const {
+    return m_item->getMetadata().getDeveloper();
+}
\ No newline at end of file
diff --git a/loader/src/ui/internal/list/InstallListCell.hpp b/loader/src/ui/internal/list/InstallListCell.hpp
index 582ca24d..f2fd677c 100644
--- a/loader/src/ui/internal/list/InstallListCell.hpp
+++ b/loader/src/ui/internal/list/InstallListCell.hpp
@@ -6,6 +6,8 @@
 #include <Geode/loader/ModMetadata.hpp>
 #include <Geode/loader/Index.hpp>
 
+#include "../info/TagNode.hpp"
+
 using namespace geode::prelude;
 
 class InstallListPopup;
@@ -17,10 +19,14 @@ class InstallListCell : public CCLayer {
 protected:
     float m_width;
     float m_height;
-    InstallListPopup* m_layer;
-    CCMenu* m_menu;
-    CCMenuItemSpriteExtra* m_developerBtn;
+    InstallListPopup* m_layer = nullptr;
+    CCMenu* m_menu = nullptr;
+    CCMenuItemSpriteExtra* m_developerBtn = nullptr;
+    CCLabelBMFont* m_titleLabel = nullptr;
+    CCLabelBMFont* m_versionLabel = nullptr;
+    TagNode* m_tagLabel = nullptr;
     CCMenuItemToggler* m_toggle = nullptr;
+    bool m_inactive = false;
 
     void setupInfo(
         std::string name,
@@ -29,6 +35,8 @@ protected:
         bool inactive
     );
 
+    void setupVersion(std::variant<VersionInfo, ComparableVersionInfo> version);
+
     bool init(InstallListPopup* list, CCSize const& size);
     void setupInfo(ModMetadata const& metadata, bool inactive);
     void draw() override;
@@ -90,6 +98,9 @@ public:
     [[nodiscard]] std::string getDeveloper() const override;
 
     IndexItemHandle getItem();
+    void setVersionFromItem(IndexItemHandle item);
+
+    void onSelectVersion(CCObject*);
 };
 
 /**
@@ -112,3 +123,23 @@ public:
     [[nodiscard]] std::string getID() const override;
     [[nodiscard]] std::string getDeveloper() const override;
 };
+
+class SelectVersionPopup;
+/**
+ * Select version list item
+ */
+class SelectVersionCell : public InstallListCell {
+protected:
+    IndexItemHandle m_item;
+    SelectVersionPopup* m_versionPopup;
+
+    bool init(IndexItemHandle item, SelectVersionPopup* versionPopup, CCSize const& size);
+
+    void onSelected(CCObject*);
+public:
+    static SelectVersionCell* create(IndexItemHandle item, SelectVersionPopup* versionPopup, CCSize const& size);
+
+    CCNode* createLogo(CCSize const& size) override;
+    [[nodiscard]] std::string getID() const override;
+    [[nodiscard]] std::string getDeveloper() const override;
+};
\ No newline at end of file
diff --git a/loader/src/ui/internal/list/InstallListPopup.cpp b/loader/src/ui/internal/list/InstallListPopup.cpp
index fa639355..e561ca9b 100644
--- a/loader/src/ui/internal/list/InstallListPopup.cpp
+++ b/loader/src/ui/internal/list/InstallListPopup.cpp
@@ -112,7 +112,8 @@ CCArray* InstallListPopup::createCells(std::unordered_map<std::string, InstallLi
         queued.insert(item.id);
 
         // installed
-        if (item.mod && !item.mod->isUninstalled()) {
+        // TODO: we should be able to select a different version even if its installed
+        if (/*item.mod && !item.mod->isUninstalled()*/item.mod->getMetadata().getID() == "geode.loader") {
             bottom.push_back(ModInstallListCell::create(item.mod, this, this->getCellSize()));
             for (auto const& dep : item.mod->getMetadata().getDependencies()) {
                 queue.push(dep);
@@ -204,7 +205,7 @@ void InstallListPopup::onInstall(cocos2d::CCObject* obj) {
     CCArray* entries = m_list->m_entries;
     for (size_t i = entries->count(); i > 0; i--) {
         auto* itemCell = typeinfo_cast<IndexItemInstallListCell*>(entries->objectAtIndex(i - 1));
-        if (!itemCell || !itemCell->isIncluded())
+        if (!itemCell || !itemCell->isIncluded() || itemCell->getItem()->isInstalled())
             continue;
         IndexItemHandle item = itemCell->getItem();
         list.list.push_back(item);
@@ -227,3 +228,70 @@ InstallListPopup* InstallListPopup::create(
     ret->autorelease();
     return ret;
 }
+
+// SelectVersionPopup
+
+bool SelectVersionPopup::setup(std::string const& modID, IndexItemInstallListCell* installCell) {
+    m_modID = modID;
+    m_installCell = installCell;
+
+    this->setTitle("Select Version");
+
+    this->createList();
+
+    return true;
+}
+
+void SelectVersionPopup::createList() {
+    auto winSize = CCDirector::sharedDirector()->getWinSize();
+
+    if (m_listParent) {
+        m_listParent->removeFromParent();
+    }
+
+    m_listParent = CCNode::create();
+    m_mainLayer->addChild(m_listParent);
+
+    auto items = this->createCells();
+    m_list = ListView::create(
+        items,
+        this->getCellSize().height,
+        this->getListSize().width,
+        this->getListSize().height
+    );
+    m_list->setPosition(winSize / 2 - m_list->getScaledContentSize() / 2);
+    m_listParent->addChild(m_list);
+
+    addListBorders(m_listParent, winSize / 2, m_list->getScaledContentSize());
+}
+
+CCArray* SelectVersionPopup::createCells() {
+    auto cells = CCArray::create();
+    for (auto& item : ranges::reverse(Index::get()->getItemsByModID(m_modID))) {
+        cells->addObject(SelectVersionCell::create(item, this, this->getCellSize()));
+    }
+    return cells;
+}
+
+CCSize SelectVersionPopup::getCellSize() const {
+    return { getListSize().width, 30.f };
+}
+CCSize SelectVersionPopup::getListSize() const {
+    return { 340.f, 170.f };
+}
+
+void SelectVersionPopup::selectItem(IndexItemHandle item) {
+    this->onBtn2(nullptr);
+
+    m_installCell->setVersionFromItem(item);
+}
+
+SelectVersionPopup* SelectVersionPopup::create(std::string const& modID, IndexItemInstallListCell* installCell) {
+    auto ret = new SelectVersionPopup();
+    if (!ret->init(380.f, 250.f, modID, installCell)) {
+        CC_SAFE_DELETE(ret);
+        return nullptr;
+    }
+    ret->autorelease();
+    return ret;
+}
\ No newline at end of file
diff --git a/loader/src/ui/internal/list/InstallListPopup.hpp b/loader/src/ui/internal/list/InstallListPopup.hpp
index 7802bccb..fbab3f5c 100644
--- a/loader/src/ui/internal/list/InstallListPopup.hpp
+++ b/loader/src/ui/internal/list/InstallListPopup.hpp
@@ -28,3 +28,24 @@ public:
 
     static InstallListPopup* create(IndexItemHandle item, MiniFunction<void(IndexInstallList const&)> onInstall);
 };
+
+class SelectVersionPopup : public Popup<std::string const&, IndexItemInstallListCell*> {
+protected:
+    std::string m_modID;
+    CCNode* m_listParent;
+    ListView* m_list;
+    IndexItemInstallListCell* m_installCell;
+
+    bool setup(std::string const& modID, IndexItemInstallListCell* installCell) override;
+
+    void createList();
+    CCArray* createCells();
+    CCSize getCellSize() const;
+    CCSize getListSize() const;
+
+    
+public:
+    void selectItem(IndexItemHandle item);
+
+    static SelectVersionPopup* create(std::string const& modID, IndexItemInstallListCell* installCell);
+};
diff --git a/loader/src/ui/internal/list/ModListLayer.cpp b/loader/src/ui/internal/list/ModListLayer.cpp
index 3ff3b930..3a5cf1d3 100644
--- a/loader/src/ui/internal/list/ModListLayer.cpp
+++ b/loader/src/ui/internal/list/ModListLayer.cpp
@@ -198,7 +198,7 @@ CCArray* ModListLayer::createModCells(ModListType type, ModListQuery const& quer
             std::multimap<int, IndexItemHandle> sorted;
 
             auto index = Index::get();
-            for (auto const& item : index->getItems()) {
+            for (auto const& item : index->getLatestItems()) {
                 if (auto match = queryMatch(query, item)) {
                     sorted.insert({ match.value(), item });
                 }