ghdl: list all releases
This commit is contained in:
@@ -66,6 +66,8 @@ private:
|
||||
void Sort();
|
||||
void UpdateSubheading();
|
||||
|
||||
void DownloadEntries();
|
||||
|
||||
private:
|
||||
std::vector<Entry> m_entries{};
|
||||
s64 m_index{};
|
||||
|
||||
@@ -142,3 +142,18 @@ constexpr auto cexprHash(const char *str, std::size_t v = 0) noexcept -> std::si
|
||||
__VA_ARGS__ \
|
||||
} \
|
||||
}
|
||||
|
||||
#define JSON_ARR_ITR(member) \
|
||||
if (!yyjson_is_arr(json)) { \
|
||||
return; \
|
||||
} \
|
||||
const auto arr_size = yyjson_arr_size(json); \
|
||||
if (!arr_size) { \
|
||||
return; \
|
||||
} \
|
||||
member.resize(arr_size); \
|
||||
size_t idx, max; \
|
||||
yyjson_val *hit; \
|
||||
yyjson_arr_foreach(json, idx, max, hit) { \
|
||||
from_json(hit, member[idx]); \
|
||||
}
|
||||
|
||||
@@ -29,7 +29,9 @@ namespace {
|
||||
constexpr auto CACHE_PATH = "/switch/sphaira/cache/github";
|
||||
|
||||
auto GenerateApiUrl(const Entry& e) {
|
||||
if (e.tag == "latest") {
|
||||
if (e.tag.empty()) {
|
||||
return "https://api.github.com/repos/" + e.owner + "/" + e.repo + "/releases";
|
||||
} else if (e.tag == "latest") {
|
||||
return "https://api.github.com/repos/" + e.owner + "/" + e.repo + "/releases/latest";
|
||||
} else {
|
||||
return "https://api.github.com/repos/" + e.owner + "/" + e.repo + "/releases/tags/" + e.tag;
|
||||
@@ -74,8 +76,16 @@ void from_json(yyjson_val* json, GhApiAsset& e) {
|
||||
);
|
||||
}
|
||||
|
||||
void from_json(const fs::FsPath& path, GhApiEntry& e) {
|
||||
JSON_INIT_VEC_FILE(path, nullptr, nullptr);
|
||||
// void from_json(const fs::FsPath& path, GhApiEntry& e) {
|
||||
// JSON_INIT_VEC_FILE(path, nullptr, nullptr);
|
||||
// JSON_OBJ_ITR(
|
||||
// JSON_SET_STR(tag_name);
|
||||
// JSON_SET_STR(name);
|
||||
// JSON_SET_ARR_OBJ(assets);
|
||||
// );
|
||||
// }
|
||||
|
||||
void from_json(yyjson_val* json, GhApiEntry& e) {
|
||||
JSON_OBJ_ITR(
|
||||
JSON_SET_STR(tag_name);
|
||||
JSON_SET_STR(name);
|
||||
@@ -83,6 +93,16 @@ void from_json(const fs::FsPath& path, GhApiEntry& e) {
|
||||
);
|
||||
}
|
||||
|
||||
void from_json(const fs::FsPath& path, std::vector<GhApiEntry>& e) {
|
||||
JSON_INIT_VEC_FILE(path, nullptr, nullptr);
|
||||
if (yyjson_is_arr(json)) {
|
||||
JSON_ARR_ITR(e);
|
||||
} else {
|
||||
e.resize(1);
|
||||
from_json(json, e[0]);
|
||||
}
|
||||
}
|
||||
|
||||
auto DownloadApp(ProgressBox* pbox, const GhApiAsset& gh_asset, const AssetEntry* entry) -> Result {
|
||||
static const fs::FsPath temp_file{"/switch/sphaira/cache/github/ghdl.temp"};
|
||||
|
||||
@@ -125,7 +145,7 @@ auto DownloadApp(ProgressBox* pbox, const GhApiAsset& gh_asset, const AssetEntry
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
auto DownloadAssetJson(ProgressBox* pbox, const std::string& url, GhApiEntry& out) -> Result {
|
||||
auto DownloadReleaseJsonJson(ProgressBox* pbox, const std::string& url, std::vector<GhApiEntry>& out) -> Result {
|
||||
// 1. download the json
|
||||
if (!pbox->ShouldExit()) {
|
||||
pbox->NewTransfer("Downloading json"_i18n);
|
||||
@@ -147,7 +167,7 @@ auto DownloadAssetJson(ProgressBox* pbox, const std::string& url, GhApiEntry& ou
|
||||
from_json(result.path, out);
|
||||
}
|
||||
|
||||
R_UNLESS(!out.assets.empty(), Result_GhdlEmptyAsset);
|
||||
R_UNLESS(!out.empty(), Result_GhdlEmptyAsset);
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
@@ -162,16 +182,195 @@ Menu::Menu(u32 flags) : MenuBase{"GitHub"_i18n, flags} {
|
||||
return;
|
||||
}
|
||||
|
||||
DownloadEntries();
|
||||
}}),
|
||||
|
||||
std::make_pair(Button::B, Action{"Back"_i18n, [this](){
|
||||
SetPop();
|
||||
}})
|
||||
);
|
||||
|
||||
const Vec4 v{75, GetY() + 1.f + 42.f, 1220.f-45.f*2, 60};
|
||||
m_list = std::make_unique<List>(1, 8, m_pos, v);
|
||||
}
|
||||
|
||||
Menu::~Menu() {
|
||||
}
|
||||
|
||||
void Menu::Update(Controller* controller, TouchInfo* touch) {
|
||||
MenuBase::Update(controller, touch);
|
||||
m_list->OnUpdate(controller, touch, m_index, m_entries.size(), [this](bool touch, auto i) {
|
||||
if (touch && m_index == i) {
|
||||
FireAction(Button::A);
|
||||
} else {
|
||||
App::PlaySoundEffect(SoundEffect_Focus);
|
||||
SetIndex(i);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void Menu::Draw(NVGcontext* vg, Theme* theme) {
|
||||
MenuBase::Draw(vg, theme);
|
||||
|
||||
const auto& text_col = theme->GetColour(ThemeEntryID_TEXT);
|
||||
|
||||
if (m_entries.empty()) {
|
||||
gfx::drawTextArgs(vg, SCREEN_WIDTH / 2.f, SCREEN_HEIGHT / 2.f, 36.f, NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE, theme->GetColour(ThemeEntryID_TEXT_INFO), "Empty..."_i18n.c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
constexpr float text_xoffset{15.f};
|
||||
|
||||
m_list->Draw(vg, theme, m_entries.size(), [this, text_col](auto* vg, auto* theme, auto v, auto i) {
|
||||
const auto& [x, y, w, h] = v;
|
||||
auto& e = m_entries[i];
|
||||
|
||||
auto text_id = ThemeEntryID_TEXT;
|
||||
if (m_index == i) {
|
||||
text_id = ThemeEntryID_TEXT_SELECTED;
|
||||
gfx::drawRectOutline(vg, theme, 4.f, v);
|
||||
} else {
|
||||
if (i != m_entries.size() - 1) {
|
||||
gfx::drawRect(vg, x, y + h, w, 1.f, theme->GetColour(ThemeEntryID_LINE_SEPARATOR));
|
||||
}
|
||||
}
|
||||
|
||||
nvgSave(vg);
|
||||
nvgIntersectScissor(vg, x + text_xoffset, y, w-(x+text_xoffset+50), h);
|
||||
gfx::drawTextArgs(vg, x + text_xoffset, y + (h / 2.f), 20.f, NVG_ALIGN_LEFT | NVG_ALIGN_MIDDLE, theme->GetColour(text_id), "%s By %s", e.repo.c_str(), e.owner.c_str());
|
||||
nvgRestore(vg);
|
||||
|
||||
if (!e.tag.empty()) {
|
||||
gfx::drawTextArgs(vg, x + w - text_xoffset, y + (h / 2.f), 16.f, NVG_ALIGN_RIGHT | NVG_ALIGN_MIDDLE, theme->GetColour(text_id), "version: %s", e.tag.c_str());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void Menu::OnFocusGained() {
|
||||
MenuBase::OnFocusGained();
|
||||
if (m_entries.empty()) {
|
||||
Scan();
|
||||
}
|
||||
}
|
||||
|
||||
void Menu::SetIndex(s64 index) {
|
||||
m_index = index;
|
||||
if (!m_index) {
|
||||
m_list->SetYoff(0);
|
||||
}
|
||||
|
||||
SetTitleSubHeading(m_entries[m_index].json_path);
|
||||
UpdateSubheading();
|
||||
}
|
||||
|
||||
void Menu::Scan() {
|
||||
m_entries.clear();
|
||||
|
||||
// load from romfs first
|
||||
if (R_SUCCEEDED(romfsInit())) {
|
||||
LoadEntriesFromPath("romfs:/github/");
|
||||
romfsExit();
|
||||
}
|
||||
|
||||
// then load custom entries
|
||||
LoadEntriesFromPath("/config/sphaira/github/");
|
||||
Sort();
|
||||
SetIndex(0);
|
||||
}
|
||||
|
||||
void Menu::LoadEntriesFromPath(const fs::FsPath& path) {
|
||||
auto dir = opendir(path);
|
||||
if (!dir) {
|
||||
return;
|
||||
}
|
||||
ON_SCOPE_EXIT(closedir(dir));
|
||||
|
||||
while (auto d = readdir(dir)) {
|
||||
if (d->d_name[0] == '.') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (d->d_type != DT_REG) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const auto ext = std::strrchr(d->d_name, '.');
|
||||
if (!ext || strcasecmp(ext, ".json")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Entry entry{};
|
||||
const auto full_path = fs::AppendPath(path, d->d_name);
|
||||
from_json(full_path, entry);
|
||||
|
||||
// parse owner and author from url (if needed).
|
||||
if (!entry.url.empty()) {
|
||||
const auto s = entry.url.substr(std::strlen("https://github.com/"));
|
||||
const auto it = s.find('/');
|
||||
if (it != s.npos) {
|
||||
entry.owner = s.substr(0, it);
|
||||
entry.repo = s.substr(it + 1);
|
||||
}
|
||||
}
|
||||
|
||||
// check that we have a owner and repo
|
||||
if (entry.owner.empty() || entry.repo.empty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
entry.json_path = full_path;
|
||||
m_entries.emplace_back(entry);
|
||||
}
|
||||
}
|
||||
|
||||
void Menu::Sort() {
|
||||
const auto sorter = [this](Entry& lhs, Entry& rhs) -> bool {
|
||||
// handle fallback if multiple entries are added with the same name
|
||||
// used for forks of a project.
|
||||
// in the rare case of the user adding the same owner and repo,
|
||||
// fallback to the filepath, which *is* unqiue
|
||||
auto r = strcasecmp(lhs.repo.c_str(), rhs.repo.c_str());
|
||||
if (!r) {
|
||||
r = strcasecmp(lhs.owner.c_str(), rhs.owner.c_str());
|
||||
if (!r) {
|
||||
r = strcasecmp(lhs.json_path, rhs.json_path);
|
||||
}
|
||||
}
|
||||
return r < 0;
|
||||
};
|
||||
|
||||
std::sort(m_entries.begin(), m_entries.end(), sorter);
|
||||
}
|
||||
|
||||
void Menu::UpdateSubheading() {
|
||||
const auto index = m_entries.empty() ? 0 : m_index + 1;
|
||||
this->SetSubHeading(std::to_string(index) + " / " + std::to_string(m_entries.size()));
|
||||
}
|
||||
|
||||
void Menu::DownloadEntries() {
|
||||
// hack
|
||||
static GhApiEntry gh_entry;
|
||||
gh_entry = {};
|
||||
static std::vector<GhApiEntry> gh_entries;
|
||||
gh_entries = {};
|
||||
|
||||
App::Push<ProgressBox>(0, "Downloading "_i18n, GetEntry().repo, [this](auto pbox) -> Result {
|
||||
return DownloadAssetJson(pbox, GenerateApiUrl(GetEntry()), gh_entry);
|
||||
return DownloadReleaseJsonJson(pbox, GenerateApiUrl(GetEntry()), gh_entries);
|
||||
}, [this](Result rc){
|
||||
App::PushErrorBox(rc, "Failed to download json"_i18n);
|
||||
if (R_FAILED(rc) || gh_entries.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (R_SUCCEEDED(rc)) {
|
||||
PopupList::Items entry_items;
|
||||
for (const auto& e : gh_entries) {
|
||||
entry_items.emplace_back(e.name);
|
||||
}
|
||||
|
||||
App::Push<PopupList>("Select release to download for "_i18n + GetEntry().repo, entry_items, [this](auto op_index){
|
||||
if (!op_index) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto& gh_entry = gh_entries[*op_index];
|
||||
const auto& assets = GetEntry().assets;
|
||||
PopupList::Items asset_items;
|
||||
std::vector<const AssetEntry*> asset_ptr;
|
||||
@@ -249,172 +448,8 @@ Menu::Menu(u32 flags) : MenuBase{"GitHub"_i18n, flags} {
|
||||
func();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}}),
|
||||
|
||||
std::make_pair(Button::B, Action{"Back"_i18n, [this](){
|
||||
SetPop();
|
||||
}})
|
||||
);
|
||||
|
||||
const Vec4 v{75, GetY() + 1.f + 42.f, 1220.f-45.f*2, 60};
|
||||
m_list = std::make_unique<List>(1, 8, m_pos, v);
|
||||
}
|
||||
|
||||
Menu::~Menu() {
|
||||
}
|
||||
|
||||
void Menu::Update(Controller* controller, TouchInfo* touch) {
|
||||
MenuBase::Update(controller, touch);
|
||||
m_list->OnUpdate(controller, touch, m_index, m_entries.size(), [this](bool touch, auto i) {
|
||||
if (touch && m_index == i) {
|
||||
FireAction(Button::A);
|
||||
} else {
|
||||
App::PlaySoundEffect(SoundEffect_Focus);
|
||||
SetIndex(i);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void Menu::Draw(NVGcontext* vg, Theme* theme) {
|
||||
MenuBase::Draw(vg, theme);
|
||||
|
||||
const auto& text_col = theme->GetColour(ThemeEntryID_TEXT);
|
||||
|
||||
if (m_entries.empty()) {
|
||||
gfx::drawTextArgs(vg, SCREEN_WIDTH / 2.f, SCREEN_HEIGHT / 2.f, 36.f, NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE, theme->GetColour(ThemeEntryID_TEXT_INFO), "Empty..."_i18n.c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
constexpr float text_xoffset{15.f};
|
||||
|
||||
m_list->Draw(vg, theme, m_entries.size(), [this, text_col](auto* vg, auto* theme, auto v, auto i) {
|
||||
const auto& [x, y, w, h] = v;
|
||||
auto& e = m_entries[i];
|
||||
|
||||
auto text_id = ThemeEntryID_TEXT;
|
||||
if (m_index == i) {
|
||||
text_id = ThemeEntryID_TEXT_SELECTED;
|
||||
gfx::drawRectOutline(vg, theme, 4.f, v);
|
||||
} else {
|
||||
if (i != m_entries.size() - 1) {
|
||||
gfx::drawRect(vg, x, y + h, w, 1.f, theme->GetColour(ThemeEntryID_LINE_SEPARATOR));
|
||||
}
|
||||
}
|
||||
|
||||
nvgSave(vg);
|
||||
nvgIntersectScissor(vg, x + text_xoffset, y, w-(x+text_xoffset+50), h);
|
||||
gfx::drawTextArgs(vg, x + text_xoffset, y + (h / 2.f), 20.f, NVG_ALIGN_LEFT | NVG_ALIGN_MIDDLE, theme->GetColour(text_id), "%s By %s", e.repo.c_str(), e.owner.c_str());
|
||||
nvgRestore(vg);
|
||||
|
||||
gfx::drawTextArgs(vg, x + w - text_xoffset, y + (h / 2.f), 16.f, NVG_ALIGN_RIGHT | NVG_ALIGN_MIDDLE, theme->GetColour(text_id), "version: %s", e.tag.c_str());
|
||||
});
|
||||
}
|
||||
|
||||
void Menu::OnFocusGained() {
|
||||
MenuBase::OnFocusGained();
|
||||
if (m_entries.empty()) {
|
||||
Scan();
|
||||
}
|
||||
}
|
||||
|
||||
void Menu::SetIndex(s64 index) {
|
||||
m_index = index;
|
||||
if (!m_index) {
|
||||
m_list->SetYoff(0);
|
||||
}
|
||||
|
||||
SetTitleSubHeading(m_entries[m_index].json_path);
|
||||
UpdateSubheading();
|
||||
}
|
||||
|
||||
void Menu::Scan() {
|
||||
m_entries.clear();
|
||||
|
||||
// load from romfs first
|
||||
if (R_SUCCEEDED(romfsInit())) {
|
||||
LoadEntriesFromPath("romfs:/github/");
|
||||
romfsExit();
|
||||
}
|
||||
|
||||
// then load custom entries
|
||||
LoadEntriesFromPath("/config/sphaira/github/");
|
||||
Sort();
|
||||
SetIndex(0);
|
||||
}
|
||||
|
||||
void Menu::LoadEntriesFromPath(const fs::FsPath& path) {
|
||||
auto dir = opendir(path);
|
||||
if (!dir) {
|
||||
return;
|
||||
}
|
||||
ON_SCOPE_EXIT(closedir(dir));
|
||||
|
||||
while (auto d = readdir(dir)) {
|
||||
if (d->d_name[0] == '.') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (d->d_type != DT_REG) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const auto ext = std::strrchr(d->d_name, '.');
|
||||
if (!ext || strcasecmp(ext, ".json")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Entry entry{};
|
||||
const auto full_path = fs::AppendPath(path, d->d_name);
|
||||
from_json(full_path, entry);
|
||||
|
||||
// parse owner and author from url (if needed).
|
||||
if (!entry.url.empty()) {
|
||||
const auto s = entry.url.substr(std::strlen("https://github.com/"));
|
||||
const auto it = s.find('/');
|
||||
if (it != s.npos) {
|
||||
entry.owner = s.substr(0, it);
|
||||
entry.repo = s.substr(it + 1);
|
||||
}
|
||||
}
|
||||
|
||||
// check that we have a owner and repo
|
||||
if (entry.owner.empty() || entry.repo.empty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (entry.tag.empty()) {
|
||||
entry.tag = "latest";
|
||||
}
|
||||
|
||||
entry.json_path = full_path;
|
||||
m_entries.emplace_back(entry);
|
||||
}
|
||||
}
|
||||
|
||||
void Menu::Sort() {
|
||||
const auto sorter = [this](Entry& lhs, Entry& rhs) -> bool {
|
||||
// handle fallback if multiple entries are added with the same name
|
||||
// used for forks of a project.
|
||||
// in the rare case of the user adding the same owner and repo,
|
||||
// fallback to the filepath, which *is* unqiue
|
||||
auto r = strcasecmp(lhs.repo.c_str(), rhs.repo.c_str());
|
||||
if (!r) {
|
||||
r = strcasecmp(lhs.owner.c_str(), rhs.owner.c_str());
|
||||
if (!r) {
|
||||
r = strcasecmp(lhs.json_path, rhs.json_path);
|
||||
}
|
||||
}
|
||||
return r < 0;
|
||||
};
|
||||
|
||||
std::sort(m_entries.begin(), m_entries.end(), sorter);
|
||||
}
|
||||
|
||||
void Menu::UpdateSubheading() {
|
||||
const auto index = m_entries.empty() ? 0 : m_index + 1;
|
||||
this->SetSubHeading(std::to_string(index) + " / " + std::to_string(m_entries.size()));
|
||||
}
|
||||
|
||||
} // namespace sphaira::ui::menu::gh
|
||||
|
||||
Reference in New Issue
Block a user