mirror of
https://github.com/geode-sdk/geode.git
synced 2025-03-28 13:52:07 -04:00
more work on index
- fix index not being a static instance - fix small stuff related mods list UI including index updating and error messages - fix Unzip directories not being created relatively properly
This commit is contained in:
parent
82c3179885
commit
037602ecea
8 changed files with 143 additions and 115 deletions
loader
include/Geode/loader
src
loader
ui/internal/list
utils
|
@ -41,33 +41,12 @@ namespace geode {
|
|||
IndexUpdateFilter();
|
||||
};
|
||||
|
||||
namespace impl {
|
||||
// The reason sources have private implementation events that are
|
||||
// turned into the global IndexUpdateEvent is because it makes it much
|
||||
// simpler to keep track of progress, what errors were received, etc.
|
||||
// without having to store a ton of members
|
||||
|
||||
struct GEODE_DLL IndexSource final {
|
||||
std::string repository;
|
||||
bool isUpToDate = false;
|
||||
|
||||
std::string dirname() const;
|
||||
};
|
||||
|
||||
struct GEODE_DLL SourceUpdateEvent : public Event {
|
||||
const IndexSource& source;
|
||||
const UpdateStatus status;
|
||||
SourceUpdateEvent(IndexSource const& src, const UpdateStatus status);
|
||||
};
|
||||
|
||||
class GEODE_DLL SourceUpdateFilter : public EventFilter<SourceUpdateEvent> {
|
||||
public:
|
||||
using Callback = void(SourceUpdateEvent*);
|
||||
|
||||
ListenerResult handle(std::function<Callback> fn, SourceUpdateEvent* event);
|
||||
SourceUpdateFilter();
|
||||
};
|
||||
}
|
||||
struct IndexSourceImpl;
|
||||
struct GEODE_DLL IndexSourceImplDeleter {
|
||||
void operator()(IndexSourceImpl* src);
|
||||
};
|
||||
struct SourceUpdateEvent;
|
||||
using IndexSourcePtr = std::unique_ptr<IndexSourceImpl, IndexSourceImplDeleter>;
|
||||
|
||||
struct GEODE_DLL IndexItem {
|
||||
std::string sourceRepository;
|
||||
|
@ -96,17 +75,17 @@ namespace geode {
|
|||
// getting the latest version of a mod as easy as items.rbegin())
|
||||
using ItemVersions = std::map<size_t, IndexItemHandle>;
|
||||
|
||||
std::vector<impl::IndexSource> m_sources;
|
||||
std::vector<IndexSourcePtr> m_sources;
|
||||
std::unordered_map<std::string, UpdateStatus> m_sourceStatuses;
|
||||
std::atomic<bool> m_triedToUpdate = false;
|
||||
std::unordered_map<std::string, ItemVersions> m_items;
|
||||
|
||||
Index();
|
||||
|
||||
void onSourceUpdate(impl::SourceUpdateEvent* event);
|
||||
void checkSourceUpdates(impl::IndexSource& src);
|
||||
void downloadSource(impl::IndexSource& src);
|
||||
void updateSourceFromLocal(impl::IndexSource& src);
|
||||
void onSourceUpdate(SourceUpdateEvent* event);
|
||||
void checkSourceUpdates(IndexSourceImpl* src);
|
||||
void downloadSource(IndexSourceImpl* src);
|
||||
void updateSourceFromLocal(IndexSourceImpl* src);
|
||||
void cleanupItems();
|
||||
|
||||
public:
|
||||
|
@ -114,7 +93,7 @@ namespace geode {
|
|||
|
||||
void addSource(std::string const& repository);
|
||||
void removeSource(std::string const& repository);
|
||||
std::vector<impl::IndexSource> getSources() const;
|
||||
std::vector<std::string> getSources() const;
|
||||
|
||||
std::vector<IndexItemHandle> getItems() const;
|
||||
bool isKnownItem(std::string const& id, std::optional<size_t> version) const;
|
||||
|
|
|
@ -9,6 +9,48 @@
|
|||
USE_GEODE_NAMESPACE();
|
||||
using namespace geode::impl;
|
||||
|
||||
// The reason sources have private implementation events that are
|
||||
// turned into the global IndexUpdateEvent is because it makes it much
|
||||
// simpler to keep track of progress, what errors were received, etc.
|
||||
// without having to store a ton of members
|
||||
|
||||
struct geode::IndexSourceImpl final {
|
||||
std::string repository;
|
||||
bool isUpToDate = false;
|
||||
|
||||
std::string dirname() const {
|
||||
return string::replace(this->repository, "/", "_");
|
||||
}
|
||||
|
||||
ghc::filesystem::path path() const {
|
||||
return dirs::getIndexDir() / this->dirname();
|
||||
}
|
||||
};
|
||||
|
||||
void IndexSourceImplDeleter::operator()(IndexSourceImpl* src) {
|
||||
delete src;
|
||||
}
|
||||
|
||||
struct geode::SourceUpdateEvent : public Event {
|
||||
IndexSourceImpl* source;
|
||||
const UpdateStatus status;
|
||||
SourceUpdateEvent(IndexSourceImpl* src, const UpdateStatus status)
|
||||
: source(src), status(status) {}
|
||||
};
|
||||
|
||||
class SourceUpdateFilter : public EventFilter<SourceUpdateEvent> {
|
||||
public:
|
||||
using Callback = void(SourceUpdateEvent*);
|
||||
|
||||
ListenerResult handle(std::function<Callback> fn, SourceUpdateEvent* event) {
|
||||
fn(event);
|
||||
return ListenerResult::Propagate;
|
||||
}
|
||||
SourceUpdateFilter() {}
|
||||
};
|
||||
|
||||
// Save data
|
||||
|
||||
struct IndexSourceSaveData {
|
||||
std::string downloadedCommitSHA;
|
||||
};
|
||||
|
@ -19,27 +61,6 @@ struct IndexSaveData {
|
|||
};
|
||||
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(IndexSaveData, sources);
|
||||
|
||||
std::string IndexSource::dirname() const {
|
||||
return string::replace(this->repository, "/", "_");
|
||||
}
|
||||
|
||||
// SourceUpdateEvent
|
||||
|
||||
SourceUpdateEvent::SourceUpdateEvent(
|
||||
IndexSource const& src,
|
||||
const UpdateStatus status
|
||||
) : source(src), status(status) {}
|
||||
|
||||
ListenerResult SourceUpdateFilter::handle(
|
||||
std::function<Callback> fn,
|
||||
SourceUpdateEvent* event
|
||||
) {
|
||||
fn(event);
|
||||
return ListenerResult::Propagate;
|
||||
}
|
||||
|
||||
SourceUpdateFilter::SourceUpdateFilter() {}
|
||||
|
||||
// ModInstallEvent
|
||||
|
||||
ListenerResult ModInstallFilter::handle(std::function<Callback> fn, ModInstallEvent* event) {
|
||||
|
@ -120,30 +141,34 @@ Index::Index() {
|
|||
}
|
||||
|
||||
Index* Index::get() {
|
||||
auto inst = new Index();
|
||||
static auto inst = new Index();
|
||||
return inst;
|
||||
}
|
||||
|
||||
void Index::addSource(std::string const& repository) {
|
||||
m_sources.push_back(IndexSource {
|
||||
m_sources.emplace_back(new IndexSourceImpl {
|
||||
.repository = repository
|
||||
});
|
||||
}
|
||||
|
||||
void Index::removeSource(std::string const& repository) {
|
||||
ranges::remove(m_sources, [repository](IndexSource const& src) {
|
||||
return src.repository == repository;
|
||||
ranges::remove(m_sources, [repository](IndexSourcePtr const& src) {
|
||||
return src->repository == repository;
|
||||
});
|
||||
}
|
||||
|
||||
std::vector<IndexSource> Index::getSources() const {
|
||||
return m_sources;
|
||||
std::vector<std::string> Index::getSources() const {
|
||||
std::vector<std::string> res;
|
||||
for (auto& src : m_sources) {
|
||||
res.push_back(src->repository);
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
void Index::onSourceUpdate(SourceUpdateEvent* event) {
|
||||
// save status for aggregating SourceUpdateEvents to a single global
|
||||
// IndexUpdateEvent
|
||||
m_sourceStatuses[event->source.repository] = event->status;
|
||||
m_sourceStatuses[event->source->repository] = event->status;
|
||||
|
||||
// figure out aggregate event
|
||||
enum { Finished, Progress, Failed, } whatToPost = Finished;
|
||||
|
@ -203,33 +228,39 @@ void Index::onSourceUpdate(SourceUpdateEvent* event) {
|
|||
}
|
||||
}
|
||||
|
||||
void Index::checkSourceUpdates(IndexSource& src) {
|
||||
if (src.isUpToDate) {
|
||||
void Index::checkSourceUpdates(IndexSourceImpl* src) {
|
||||
if (src->isUpToDate) {
|
||||
return this->updateSourceFromLocal(src);
|
||||
}
|
||||
SourceUpdateEvent(src, UpdateProgress(0, "Checking status")).post();
|
||||
auto data = Mod::get()->getSavedMutable<IndexSaveData>("index");
|
||||
auto oldSHA = data.sources[src.repository].downloadedCommitSHA;
|
||||
auto oldSHA = data.sources[src->repository].downloadedCommitSHA;
|
||||
web::AsyncWebRequest()
|
||||
.join(fmt::format("index-update-{}", src.repository))
|
||||
.join(fmt::format("index-update-{}", src->repository))
|
||||
.header(fmt::format("If-None-Match: \"{}\"", oldSHA))
|
||||
.header("Accept: application/vnd.github.sha")
|
||||
.fetch(fmt::format("https://api.github.com/repos/{}/commits/main", src.repository))
|
||||
.fetch(fmt::format("https://api.github.com/repos/{}/commits/main", src->repository))
|
||||
.text()
|
||||
.then([this, &src, oldSHA](std::string const& newSHA) {
|
||||
// if no new hash was given (rate limited) or the new hash is the
|
||||
// same as old, then just update from local cache
|
||||
if (newSHA.empty() || oldSHA == newSHA) {
|
||||
.then([this, src, oldSHA](std::string const& newSHA) {
|
||||
// check if should just be updated from local cache
|
||||
if (
|
||||
// if no new hash was given (rate limited) or the new hash is the
|
||||
// same as old
|
||||
(newSHA.empty() || oldSHA == newSHA) &&
|
||||
// make sure the downloaded local copy actually exists
|
||||
ghc::filesystem::exists(src->path()) &&
|
||||
ghc::filesystem::exists(src->path() / "config.json")
|
||||
) {
|
||||
this->updateSourceFromLocal(src);
|
||||
}
|
||||
// otherwise save hash and download source
|
||||
else {
|
||||
auto data = Mod::get()->getSavedMutable<IndexSaveData>("index");
|
||||
data.sources[src.repository].downloadedCommitSHA = newSHA;
|
||||
data.sources[src->repository].downloadedCommitSHA = newSHA;
|
||||
this->downloadSource(src);
|
||||
}
|
||||
})
|
||||
.expect([&src](std::string const& err) {
|
||||
.expect([src](std::string const& err) {
|
||||
SourceUpdateEvent(
|
||||
src,
|
||||
UpdateError(fmt::format("Error checking for updates: {}", err))
|
||||
|
@ -237,17 +268,17 @@ void Index::checkSourceUpdates(IndexSource& src) {
|
|||
});
|
||||
}
|
||||
|
||||
void Index::downloadSource(IndexSource& src) {
|
||||
void Index::downloadSource(IndexSourceImpl* src) {
|
||||
SourceUpdateEvent(src, UpdateProgress(0, "Beginning download")).post();
|
||||
|
||||
auto targetFile = dirs::getIndexDir() / fmt::format("{}.zip", src.dirname());
|
||||
auto targetFile = dirs::getIndexDir() / fmt::format("{}.zip", src->dirname());
|
||||
|
||||
web::AsyncWebRequest()
|
||||
.join(fmt::format("index-download-{}", src.repository))
|
||||
.fetch(fmt::format("https://github.com/{}/zipball/main", src.repository))
|
||||
.join(fmt::format("index-download-{}", src->repository))
|
||||
.fetch(fmt::format("https://github.com/{}/zipball/main", src->repository))
|
||||
.into(targetFile)
|
||||
.then([this, &src, targetFile](auto) {
|
||||
auto targetDir = dirs::getIndexDir() / src.dirname();
|
||||
.then([this, src, targetFile](auto) {
|
||||
auto targetDir = src->path();
|
||||
// delete old unzipped index
|
||||
try {
|
||||
if (ghc::filesystem::exists(targetDir)) {
|
||||
|
@ -272,12 +303,12 @@ void Index::downloadSource(IndexSource& src) {
|
|||
// update index
|
||||
this->updateSourceFromLocal(src);
|
||||
})
|
||||
.expect([&src](std::string const& err) {
|
||||
.expect([src](std::string const& err) {
|
||||
SourceUpdateEvent(
|
||||
src, UpdateError(fmt::format("Error downloading: {}", err))
|
||||
).post();
|
||||
})
|
||||
.progress([&src](auto&, double now, double total) {
|
||||
.progress([src](auto&, double now, double total) {
|
||||
SourceUpdateEvent(
|
||||
src,
|
||||
UpdateProgress(
|
||||
|
@ -288,12 +319,12 @@ void Index::downloadSource(IndexSource& src) {
|
|||
});
|
||||
}
|
||||
|
||||
void Index::updateSourceFromLocal(IndexSource& src) {
|
||||
void Index::updateSourceFromLocal(IndexSourceImpl* src) {
|
||||
SourceUpdateEvent(src, UpdateProgress(100, "Updating local cache")).post();
|
||||
// delete old items from this url if such exist
|
||||
for (auto& [_, versions] : m_items) {
|
||||
for (auto it = versions.begin(); it != versions.end(); ) {
|
||||
if (it->second->sourceRepository == src.repository) {
|
||||
if (it->second->sourceRepository == src->repository) {
|
||||
it = versions.erase(it);
|
||||
} else {
|
||||
++it;
|
||||
|
@ -304,10 +335,8 @@ void Index::updateSourceFromLocal(IndexSource& src) {
|
|||
|
||||
// read directory and add new items
|
||||
try {
|
||||
for (auto& dir : ghc::filesystem::directory_iterator(
|
||||
dirs::getIndexDir() / src.dirname()
|
||||
)) {
|
||||
auto addRes = IndexItem::createFromDir(src.repository, dir);
|
||||
for (auto& dir : ghc::filesystem::directory_iterator(src->path() / "mods")) {
|
||||
auto addRes = IndexItem::createFromDir(src->repository, dir);
|
||||
if (!addRes) {
|
||||
log::warn("Unable to add index item from {}: {}", dir, addRes.unwrapErr());
|
||||
continue;
|
||||
|
@ -328,12 +357,14 @@ void Index::updateSourceFromLocal(IndexSource& src) {
|
|||
});
|
||||
}
|
||||
} catch(std::exception& e) {
|
||||
log::warn("Unable to read index source {}: {}", src.dirname(), e.what());
|
||||
SourceUpdateEvent(src, fmt::format(
|
||||
"Unable to read source {}", src->repository
|
||||
)).post();
|
||||
return;
|
||||
}
|
||||
|
||||
// mark source as finished
|
||||
src.isUpToDate = true;
|
||||
src->isUpToDate = true;
|
||||
SourceUpdateEvent(src, UpdateFinished()).post();
|
||||
}
|
||||
|
||||
|
@ -350,7 +381,7 @@ void Index::cleanupItems() {
|
|||
|
||||
bool Index::isUpToDate() const {
|
||||
for (auto& source : m_sources) {
|
||||
if (!source.isUpToDate) {
|
||||
if (!source->isUpToDate) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
@ -377,9 +408,9 @@ void Index::update(bool force) {
|
|||
// update sources
|
||||
for (auto& src : m_sources) {
|
||||
if (force) {
|
||||
this->downloadSource(src);
|
||||
this->downloadSource(src.get());
|
||||
} else {
|
||||
this->checkSourceUpdates(src);
|
||||
this->checkSourceUpdates(src.get());
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
@ -30,7 +30,7 @@ VersionInfo Loader::getVersion() {
|
|||
}
|
||||
|
||||
VersionInfo Loader::minModVersion() {
|
||||
return VersionInfo { 0, 7, 0 };
|
||||
return VersionInfo { 0, 6, 1 };
|
||||
}
|
||||
|
||||
VersionInfo Loader::maxModVersion() {
|
||||
|
|
|
@ -22,6 +22,8 @@ static ModListLayer* g_instance = nullptr;
|
|||
bool ModListLayer::init() {
|
||||
if (!CCLayer::init()) return false;
|
||||
|
||||
m_indexListener.bind(this, &ModListLayer::onIndexUpdate);
|
||||
|
||||
auto winSize = CCDirector::sharedDirector()->getWinSize();
|
||||
|
||||
// create background
|
||||
|
@ -214,22 +216,23 @@ void ModListLayer::reloadList() {
|
|||
m_list->removeFromParent();
|
||||
}
|
||||
|
||||
auto items = ModListView::modsForType(g_tab);
|
||||
|
||||
// create new list
|
||||
auto list = ModListView::create(g_tab, m_display);
|
||||
auto list = ModListView::create(items, m_display);
|
||||
list->setLayer(this);
|
||||
|
||||
// set list status
|
||||
// auto status = list->getStatusAsString();
|
||||
// if (status.size()) {
|
||||
// m_listLabel->setVisible(true);
|
||||
// m_listLabel->setString(status.c_str());
|
||||
// }
|
||||
// else {
|
||||
if (!items->count()) {
|
||||
m_listLabel->setVisible(true);
|
||||
m_listLabel->setString("No mods found");
|
||||
} else {
|
||||
m_listLabel->setVisible(false);
|
||||
// }
|
||||
}
|
||||
|
||||
// update index if needed
|
||||
if (g_tab == ModListType::Download && !Index::get()->isUpToDate()) {
|
||||
if (g_tab == ModListType::Download && !Index::get()->hasTriedToUpdate()) {
|
||||
m_listLabel->setVisible(true);
|
||||
m_listLabel->setString("Updating index...");
|
||||
if (!m_loadingCircle) {
|
||||
m_loadingCircle = LoadingCircle::create();
|
||||
|
@ -316,6 +319,18 @@ void ModListLayer::onCheckForUpdates(CCObject*) {
|
|||
Index::get()->update();
|
||||
}
|
||||
|
||||
void ModListLayer::onIndexUpdate(IndexUpdateEvent* event) {
|
||||
std::visit(makeVisitor {
|
||||
[&](UpdateProgress const& prog) {},
|
||||
[&](UpdateFinished const&) {
|
||||
this->reloadList();
|
||||
},
|
||||
[&](UpdateError const& error) {
|
||||
this->reloadList();
|
||||
}
|
||||
}, event->status);
|
||||
}
|
||||
|
||||
void ModListLayer::textChanged(CCTextInputNode* input) {
|
||||
this->reloadList();
|
||||
}
|
||||
|
|
|
@ -27,6 +27,7 @@ protected:
|
|||
LoadingCircle* m_loadingCircle = nullptr;
|
||||
CCMenuItemSpriteExtra* m_filterBtn;
|
||||
ModListDisplay m_display = ModListDisplay::Concise;
|
||||
EventListener<IndexUpdateFilter> m_indexListener;
|
||||
|
||||
virtual ~ModListLayer();
|
||||
|
||||
|
@ -43,6 +44,7 @@ protected:
|
|||
void keyDown(enumKeyCodes) override;
|
||||
void textChanged(CCTextInputNode*) override;
|
||||
void createSearchControl();
|
||||
void onIndexUpdate(IndexUpdateEvent* event);
|
||||
|
||||
friend class SearchFilterPopup;
|
||||
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
#include <Geode/binding/CCMenuItemSpriteExtra.hpp>
|
||||
#include <Geode/binding/TableView.hpp>
|
||||
#include <Geode/binding/CCMenuItemToggler.hpp>
|
||||
#include <Geode/binding/CCContentLayer.hpp>
|
||||
#include <Geode/loader/Mod.hpp>
|
||||
#include <Geode/utils/casts.hpp>
|
||||
#include <Geode/utils/cocos.hpp>
|
||||
|
@ -101,7 +102,7 @@ bool ModListView::init(CCArray* mods, ModListDisplay display) {
|
|||
return CustomListView::init(mods, BoomListType::Default, 358.f, 190.f);
|
||||
}
|
||||
|
||||
CCArray* ModListView::getModsForType(ModListType type) {
|
||||
CCArray* ModListView::modsForType(ModListType type) {
|
||||
auto mods = CCArray::create();
|
||||
switch (type) {
|
||||
default:
|
||||
|
@ -149,7 +150,7 @@ ModListView* ModListView::create(CCArray* mods, ModListDisplay display) {
|
|||
}
|
||||
|
||||
ModListView* ModListView::create(ModListType type, ModListDisplay display) {
|
||||
return ModListView::create(getModsForType(type), display);
|
||||
return ModListView::create(modsForType(type), display);
|
||||
}
|
||||
|
||||
void ModListView::setLayer(ModListLayer* layer) {
|
||||
|
|
|
@ -59,7 +59,7 @@ protected:
|
|||
public:
|
||||
static ModListView* create(CCArray* mods, ModListDisplay display);
|
||||
static ModListView* create(ModListType type, ModListDisplay display);
|
||||
static CCArray* getModsForType(ModListType type);
|
||||
static CCArray* modsForType(ModListType type);
|
||||
|
||||
void updateAllStates(ModListCell* except = nullptr);
|
||||
void setLayer(ModListLayer* layer);
|
||||
|
|
|
@ -285,19 +285,19 @@ Result<> Unzip::extractTo(Path const& name, Path const& path) {
|
|||
Result<> Unzip::extractAllTo(Path const& dir) {
|
||||
GEODE_UNWRAP(file::createDirectoryAll(dir));
|
||||
for (auto& [entry, info] : m_impl->entries()) {
|
||||
if (info.isDirectory) {
|
||||
GEODE_UNWRAP(file::createDirectoryAll(entry));
|
||||
} else {
|
||||
// make sure zip files like root/../../file.txt don't get extracted to
|
||||
// avoid zip attacks
|
||||
if (!ghc::filesystem::relative(dir / entry, dir).empty()) {
|
||||
GEODE_UNWRAP(this->extractTo(entry, dir / entry));
|
||||
// make sure zip files like root/../../file.txt don't get extracted to
|
||||
// avoid zip attacks
|
||||
if (!ghc::filesystem::relative(dir / entry, dir).empty()) {
|
||||
if (info.isDirectory) {
|
||||
GEODE_UNWRAP(file::createDirectoryAll(dir / entry));
|
||||
} else {
|
||||
log::error(
|
||||
"Zip entry '{}' is not contained within zip bounds",
|
||||
dir / entry
|
||||
);
|
||||
GEODE_UNWRAP(this->extractTo(entry, dir / entry));
|
||||
}
|
||||
} else {
|
||||
log::error(
|
||||
"Zip entry '{}' is not contained within zip bounds",
|
||||
dir / entry
|
||||
);
|
||||
}
|
||||
}
|
||||
return Ok();
|
||||
|
|
Loading…
Add table
Reference in a new issue