From 4e4cdcdf61f89eda6b3017c4973101d39d2804b6 Mon Sep 17 00:00:00 2001 From: WerWolv Date: Sun, 5 Nov 2023 19:57:29 +0100 Subject: [PATCH] feat: Added release notes and commits to about page --- lib/libimhex/include/hex/api/imhex_api.hpp | 2 +- lib/libimhex/source/api/imhex_api.cpp | 9 +- .../include/content/views/view_about.hpp | 8 +- plugins/builtin/romfs/lang/en_US.json | 3 + .../source/content/views/view_about.cpp | 313 +++++++++++++++--- 5 files changed, 284 insertions(+), 51 deletions(-) diff --git a/lib/libimhex/include/hex/api/imhex_api.hpp b/lib/libimhex/include/hex/api/imhex_api.hpp index 742e997e8..33bbed253 100644 --- a/lib/libimhex/include/hex/api/imhex_api.hpp +++ b/lib/libimhex/include/hex/api/imhex_api.hpp @@ -522,7 +522,7 @@ namespace hex { * @brief Gets the current ImHex version * @return ImHex version */ - std::string getImHexVersion(); + std::string getImHexVersion(bool withBuildType = true); /** * @brief Gets the current git commit hash diff --git a/lib/libimhex/source/api/imhex_api.cpp b/lib/libimhex/source/api/imhex_api.cpp index 2e1723037..2082b18e3 100644 --- a/lib/libimhex/source/api/imhex_api.cpp +++ b/lib/libimhex/source/api/imhex_api.cpp @@ -608,9 +608,14 @@ namespace hex { #endif } - std::string getImHexVersion() { + std::string getImHexVersion(bool withBuildType) { #if defined IMHEX_VERSION - return IMHEX_VERSION; + if (withBuildType) + return IMHEX_VERSION; + else { + auto version = std::string(IMHEX_VERSION); + return version.substr(0, version.find('-')); + } #else return "Unknown"; #endif diff --git a/plugins/builtin/include/content/views/view_about.hpp b/plugins/builtin/include/content/views/view_about.hpp index d162c4d08..72faeceb2 100644 --- a/plugins/builtin/include/content/views/view_about.hpp +++ b/plugins/builtin/include/content/views/view_about.hpp @@ -4,6 +4,7 @@ #include #include +#include #include #include @@ -21,7 +22,7 @@ namespace hex::plugin::builtin { [[nodiscard]] bool hasViewMenuItemEntry() const override { return false; } [[nodiscard]] ImVec2 getMinSize() const override { - return scaled({ 600, 350 }); + return scaled({ 700, 400 }); } private: @@ -33,9 +34,14 @@ namespace hex::plugin::builtin { void drawContributorPage(); void drawLibraryCreditsPage(); void drawPathsPage(); + void drawReleaseNotesPage(); + void drawCommitHistoryPage(); void drawLicensePage(); ImGui::Texture m_logoTexture; + + std::future> m_releaseNoteRequest, m_commitHistoryRequest; + u32 m_clickCount = 0; }; } \ No newline at end of file diff --git a/plugins/builtin/romfs/lang/en_US.json b/plugins/builtin/romfs/lang/en_US.json index 0d35a3a11..841c1b4a5 100644 --- a/plugins/builtin/romfs/lang/en_US.json +++ b/plugins/builtin/romfs/lang/en_US.json @@ -96,6 +96,7 @@ "hex.builtin.common.little": "Little", "hex.builtin.common.little_endian": "Little Endian", "hex.builtin.common.load": "Load", + "hex.builtin.common.loading": "Loading...", "hex.builtin.common.match_selection": "Match Selection", "hex.builtin.common.name": "Name", "hex.builtin.common.no": "No", @@ -832,12 +833,14 @@ "hex.builtin.view.hashes.table.name": "Name", "hex.builtin.view.hashes.table.result": "Result", "hex.builtin.view.hashes.table.type": "Type", + "hex.builtin.view.help.about.commits": "Commit History", "hex.builtin.view.help.about.contributor": "Contributors", "hex.builtin.view.help.about.donations": "Donations", "hex.builtin.view.help.about.libs": "Libraries used", "hex.builtin.view.help.about.license": "License", "hex.builtin.view.help.about.name": "About", "hex.builtin.view.help.about.paths": "ImHex Directories", + "hex.builtin.view.help.about.release_notes": "Release Notes", "hex.builtin.view.help.about.source": "Source code available on GitHub:", "hex.builtin.view.help.about.thanks": "If you like my work, please consider donating to keep the project going. Thanks a lot <3", "hex.builtin.view.help.about.translator": "Translated by WerWolv", diff --git a/plugins/builtin/source/content/views/view_about.cpp b/plugins/builtin/source/content/views/view_about.cpp index dcf73fe94..8f8da68f8 100644 --- a/plugins/builtin/source/content/views/view_about.cpp +++ b/plugins/builtin/source/content/views/view_about.cpp @@ -1,15 +1,18 @@ #include "content/views/view_about.hpp" +#include #include #include #include #include #include +#include #include #include +#include namespace hex::plugin::builtin { @@ -65,10 +68,13 @@ namespace hex::plugin::builtin { this->m_logoTexture = ImGui::Texture(romfs::get("assets/common/logo.png").span()); ImGui::Image(this->m_logoTexture, scaled({ 64, 64 })); + if (ImGui::IsItemHovered() && ImGui::IsItemClicked()) { + this->m_clickCount += 1; + } ImGui::TableNextColumn(); // Draw basic information about ImHex and its version - ImGui::TextFormatted("ImHex Hex Editor v{} by WerWolv " ICON_FA_CODE_BRANCH, ImHexApi::System::getImHexVersion()); + ImGui::TextFormatted("ImHex Hex Editor v{} by WerWolv " ICON_FA_CODE_BRANCH, ImHexApi::System::getImHexVersion()); ImGui::SameLine(); @@ -77,7 +83,7 @@ namespace hex::plugin::builtin { hex::openWebpage("https://github.com/WerWolv/ImHex/commit/" + ImHexApi::System::getCommitHash(true)); // Draw the build date and time - ImGui::TextFormatted(ICON_FA_BUILDING " {}, {}", __DATE__, __TIME__); + ImGui::TextFormatted("{}, {}", __DATE__, __TIME__); // Draw the author of the current translation ImGui::TextUnformatted("hex.builtin.view.help.about.translator"_lang); @@ -221,11 +227,260 @@ namespace hex::plugin::builtin { } } + void ViewAbout::drawReleaseNotesPage() { + static std::string releaseTitle; + static std::vector releaseNotes; + + // Set up the request to get the release notes the first time the page is opened + AT_FIRST_TIME { + static HttpRequest request("GET", GitHubApiURL + std::string("/releases/tags/v") + ImHexApi::System::getImHexVersion(false)); + + this->m_releaseNoteRequest = request.execute(); + }; + + // Wait for the request to finish and parse the response + if (this->m_releaseNoteRequest.valid()) { + if (this->m_releaseNoteRequest.wait_for(std::chrono::seconds(0)) == std::future_status::ready) { + auto response = this->m_releaseNoteRequest.get(); + nlohmann::json json; + + if (response.isSuccess()) { + // A valid response was received, parse it + try { + json = nlohmann::json::parse(response.getData()); + + // Get the release title + releaseTitle = json["name"].get(); + + // Get the release notes and split it into lines + auto body = json["body"].get(); + releaseNotes = wolv::util::splitString(body, "\r\n"); + } catch (std::exception &e) { + releaseNotes.push_back("## Error: " + std::string(e.what())); + } + } else { + // An error occurred, display it + releaseNotes.push_back("## HTTP Error: " + std::to_string(response.getStatusCode())); + } + } else { + // Draw a spinner while the release notes are loading + ImGui::TextSpinner("hex.builtin.common.loading"_lang); + } + } + + + // Function to handle drawing of a regular text line + static const auto drawRegularLine = [](const std::string &line) { + ImGui::Bullet(); + ImGui::SameLine(); + + // Check if the line contains bold text + auto boldStart = line.find("**"); + if (boldStart != std::string::npos) { + // Find the end of the bold text + auto boldEnd = line.find("**", boldStart + 2); + + // Draw the line with the bold text highlighted + ImGui::TextUnformatted(line.substr(0, boldStart).c_str()); + ImGui::SameLine(0, 0); + ImGui::TextColored(ImGui::GetCustomColorVec4(ImGuiCustomCol_Highlight), line.substr(boldStart + 2, boldEnd - boldStart - 2).c_str()); + ImGui::SameLine(0, 0); + ImGui::TextUnformatted(line.substr(boldEnd + 2).c_str()); + } else { + // Draw the line normally + ImGui::TextUnformatted(line.c_str()); + } + }; + + // Draw the release title + if (!releaseTitle.empty()) { + auto title = hex::format("v{}: {}", ImHexApi::System::getImHexVersion(false), releaseTitle); + ImGui::Header(title.c_str(), true); + ImGui::Separator(); + } + + // Draw the release notes and format them using parts of the GitHub Markdown syntax + // This is not a full implementation of the syntax, but it's enough to make the release notes look good. + for (const auto &line : releaseNotes) { + if (line.starts_with("## ")) { + // Draw H2 Header + ImGui::Header(line.substr(3).c_str()); + } else if (line.starts_with("### ")) { + // Draw H3 Header + ImGui::Header(line.substr(4).c_str()); + } else if (line.starts_with("- ")) { + // Draw bullet point + drawRegularLine(line.substr(2)); + } else if (line.starts_with(" - ")) { + // Draw further indented bullet point + ImGui::Indent(); + ImGui::Indent(); + drawRegularLine(line.substr(6)); + ImGui::Unindent(); + ImGui::Unindent(); + } + } + } + + void ViewAbout::drawCommitHistoryPage() { + struct Commit { + std::string hash; + std::string message; + std::string description; + std::string author; + std::string date; + std::string url; + }; + + static std::vector commits; + + // Set up the request to get the commit history the first time the page is opened + AT_FIRST_TIME { + static HttpRequest request("GET", GitHubApiURL + std::string("/commits?per_page=100")); + this->m_commitHistoryRequest = request.execute(); + }; + + // Wait for the request to finish and parse the response + if (this->m_commitHistoryRequest.valid()) { + if (this->m_commitHistoryRequest.wait_for(std::chrono::seconds(0)) == std::future_status::ready) { + auto response = this->m_commitHistoryRequest.get(); + nlohmann::json json; + + if (response.isSuccess()) { + // A valid response was received, parse it + try { + json = nlohmann::json::parse(response.getData()); + + for (auto &commit : json) { + const auto message = commit["commit"]["message"].get(); + + // Split commit title and description. They're separated by two newlines. + const auto messageEnd = message.find("\n\n"); + + auto commitTitle = messageEnd == std::string::npos ? message : message.substr(0, messageEnd); + auto commitDescription = messageEnd == std::string::npos ? "" : message.substr(commitTitle.size() + 2); + + auto url = commit["html_url"].get(); + auto sha = commit["sha"].get(); + auto date = commit["commit"]["author"]["date"].get(); + auto author = hex::format("{} <{}>", + commit["commit"]["author"]["name"].get(), + commit["commit"]["author"]["email"].get() + ); + + // Move the commit data into the list of commits + commits.emplace_back( + std::move(sha), + std::move(commitTitle), + std::move(commitDescription), + std::move(author), + std::move(date), + std::move(url) + ); + } + + } catch (std::exception &e) { + commits.emplace_back( + "hex.builtin.common.error"_lang, + e.what(), + "", + "", + "" + ); + } + } else { + // An error occurred, display it + commits.emplace_back( + "hex.builtin.common.error"_lang, + "HTTP " + std::to_string(response.getStatusCode()), + "", + "", + "" + ); + } + } else { + // Draw a spinner while the commits are loading + ImGui::TextSpinner("hex.builtin.common.loading"_lang); + } + } + + // Draw commits table + if (!commits.empty()) { + if (ImGui::BeginTable("##commits", 2, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_SizingFixedFit | ImGuiTableFlags_ScrollY)) { + // Draw commits + for (const auto &commit : commits) { + ImGui::PushID(commit.hash.c_str()); + ImGui::TableNextRow(); + + // Draw hover tooltip + ImGui::TableNextColumn(); + if (ImGui::Selectable("##commit", false, ImGuiSelectableFlags_SpanAllColumns)) { + hex::openWebpage(commit.url); + } + + if (ImGui::IsItemHovered()) { + if (ImGui::BeginTooltip()) { + // Draw author and commit date + ImGui::TextFormattedColored(ImGui::GetCustomColorVec4(ImGuiCustomCol_Highlight), "{}", commit.author); + ImGui::SameLine(); + ImGui::TextFormatted("@ {}", commit.date.c_str()); + + // Draw description if there is one + if (!commit.description.empty()) { + ImGui::Separator(); + ImGui::TextFormatted("{}", commit.description); + } + + ImGui::EndTooltip(); + } + + } + + // Draw commit hash + ImGui::SameLine(0, 0); + ImGui::TextFormattedColored(ImGui::GetCustomColorVec4(ImGuiCustomCol_Highlight), "{}", commit.hash.substr(0, 7)); + + // Draw the commit message + ImGui::TableNextColumn(); + + const ImColor color = [&]{ + if (commit.hash == ImHexApi::System::getCommitHash(true)) + return ImGui::GetStyleColorVec4(ImGuiCol_HeaderActive); + else + return ImGui::GetStyleColorVec4(ImGuiCol_Text); + }(); + ImGui::TextFormattedColored(color, commit.message); + + ImGui::PopID(); + } + + ImGui::EndTable(); + } + } + } + void ViewAbout::drawLicensePage() { ImGui::TextFormattedWrapped("{}", romfs::get("licenses/LICENSE").string()); } void ViewAbout::drawAboutPopup() { + struct Tab { + using Function = void (ViewAbout::*)(); + + const char *unlocalizedName; + Function function; + }; + + constexpr std::array Tabs = { + Tab { "ImHex", &ViewAbout::drawAboutMainPage }, + Tab { "hex.builtin.view.help.about.contributor", &ViewAbout::drawContributorPage }, + Tab { "hex.builtin.view.help.about.libs", &ViewAbout::drawLibraryCreditsPage }, + Tab { "hex.builtin.view.help.about.paths", &ViewAbout::drawPathsPage }, + Tab { "hex.builtin.view.help.about.release_notes", &ViewAbout::drawReleaseNotesPage }, + Tab { "hex.builtin.view.help.about.commits", &ViewAbout::drawCommitHistoryPage }, + Tab { "hex.builtin.view.help.about.license", &ViewAbout::drawLicensePage }, + }; + if (ImGui::BeginPopupModal(View::toWindowName("hex.builtin.view.help.about.name").c_str(), &this->m_aboutWindowOpen)) { // Allow the window to be closed by pressing ESC @@ -233,54 +488,18 @@ namespace hex::plugin::builtin { ImGui::CloseCurrentPopup(); if (ImGui::BeginTabBar("about_tab_bar")) { + // Draw all tabs + for (const auto &[unlocalizedName, function] : Tabs) { + if (ImGui::BeginTabItem(LangEntry(unlocalizedName))) { + ImGui::NewLine(); - // Draw main ImHex tab - if (ImGui::BeginTabItem("ImHex")) { - if (ImGui::BeginChild(1)) { - this->drawAboutMainPage(); - } - ImGui::EndChild(); - ImGui::EndTabItem(); - } + if (ImGui::BeginChild(1)) { + (this->*function)(); + } + ImGui::EndChild(); - // Draw contributor tab - if (ImGui::BeginTabItem("hex.builtin.view.help.about.contributor"_lang)) { - ImGui::NewLine(); - if (ImGui::BeginChild(1)) { - this->drawContributorPage(); + ImGui::EndTabItem(); } - ImGui::EndChild(); - ImGui::EndTabItem(); - } - - // Draw libraries tab - if (ImGui::BeginTabItem("hex.builtin.view.help.about.libs"_lang)) { - ImGui::NewLine(); - if (ImGui::BeginChild(1)) { - this->drawLibraryCreditsPage(); - } - ImGui::EndChild(); - ImGui::EndTabItem(); - } - - // Draw paths tab - if (ImGui::BeginTabItem("hex.builtin.view.help.about.paths"_lang)) { - ImGui::NewLine(); - if (ImGui::BeginChild(1)) { - this->drawPathsPage(); - } - ImGui::EndChild(); - ImGui::EndTabItem(); - } - - // Draw license tab - if (ImGui::BeginTabItem("hex.builtin.view.help.about.license"_lang)) { - ImGui::NewLine(); - if (ImGui::BeginChild(1)) { - this->drawLicensePage(); - } - ImGui::EndChild(); - ImGui::EndTabItem(); } ImGui::EndTabBar();