From 9877b944684c0fa8f3ffbdb38102be1a8c90fff3 Mon Sep 17 00:00:00 2001 From: griffi-gh Date: Mon, 14 Oct 2024 17:54:36 +0200 Subject: [PATCH 01/11] Initial commit --- src/src/ui_gtk3_levelbrowser.cc | 18 ++++++++++ src/src/ui_gtk3_levelbrowser.hh | 64 +++++++++++++++++++++++++++++++++ 2 files changed, 82 insertions(+) create mode 100644 src/src/ui_gtk3_levelbrowser.cc create mode 100644 src/src/ui_gtk3_levelbrowser.hh diff --git a/src/src/ui_gtk3_levelbrowser.cc b/src/src/ui_gtk3_levelbrowser.cc new file mode 100644 index 00000000..a684a734 --- /dev/null +++ b/src/src/ui_gtk3_levelbrowser.cc @@ -0,0 +1,18 @@ +#include "ui_gtk3_levelbrowser.hh" + +#if !defined(GTK3_LEVEL_BROWSER_DISABLE) && defined(TMS_BACKEND_PC) && !defined(NO_UI) && !defined(TMS_BACKEND_EMSCRIPTEN) + +// TODO maybe vendor? +#include +using json = nlohmann::json; + +GtkCommunityDialog::GtkCommunityDialog() { + +} + +GtkCommunityDialog::~GtkCommunityDialog() { + +} + +#endif + diff --git a/src/src/ui_gtk3_levelbrowser.hh b/src/src/ui_gtk3_levelbrowser.hh new file mode 100644 index 00000000..589ac38e --- /dev/null +++ b/src/src/ui_gtk3_levelbrowser.hh @@ -0,0 +1,64 @@ +#pragma once + +#include +#if !defined(GTK3_LEVEL_BROWSER_DISABLE) && defined(TMS_BACKEND_PC) && !defined(NO_UI) && !defined(TMS_BACKEND_EMSCRIPTEN) + +#include +#include +// #include "optional.hh" + +#define COMMUNITY_LEVELS_PER_PAGE 16 + +class CommunityUser { + uint32_t id; + std::string name; + std::string customcolor; // TODO store as u32 instead +}; + +class CommunityRecentLevel { + uint32_t id; + std::string title; + CommunityUser u; +}; + +// {"id":1,"cat":1,"title":"not so smiley man","description":"finally\r\n\r\nnot so smiley man","author":1,"time":1608998715,"parent":null,"revision":1,"revision_time":null,"likes":21,"visibility":0,"views":433,"downloads":484,"platform":"Linux","u_id":1,"u_name":"ROllerozxa","u_customcolor":"31C03B"} + +class CommunityLevel { + uint32_t id; + + uint8_t cat; + + std::string title; + std::string description; + + uint32_t author; + + uint32_t time; + uint32_t parent; + + uint32_t revision; + uint32_t revision_time; + + uint8_t visibility; + + uint32_t views; + uint32_t likes; + uint32_t downloads; + + std::string platform; + + CommunityUser u; +}; + +class GtkCommunityState { + uint16_t cur_page; + // std::unordered_map cache_thumbnails; + // std::unordered_map cache_pages; +}; + +class GtkCommunityDialog { + GtkCommunityDialog(); + ~GtkCommunityDialog(); +}; + +#endif From 69659bb6f36b28b4d1b3918079963adb7f1bcba5 Mon Sep 17 00:00:00 2001 From: griffi-gh Date: Mon, 14 Oct 2024 20:29:31 +0200 Subject: [PATCH 02/11] add parse and request methods --- src/src/ui_gtk3_levelbrowser.cc | 85 +++++++++++++++++++++++-- src/src/ui_gtk3_levelbrowser.hh | 108 +++++++++++++++++--------------- 2 files changed, 138 insertions(+), 55 deletions(-) diff --git a/src/src/ui_gtk3_levelbrowser.cc b/src/src/ui_gtk3_levelbrowser.cc index a684a734..ab884150 100644 --- a/src/src/ui_gtk3_levelbrowser.cc +++ b/src/src/ui_gtk3_levelbrowser.cc @@ -1,17 +1,94 @@ #include "ui_gtk3_levelbrowser.hh" +#include "tms/backend/print.h" -#if !defined(GTK3_LEVEL_BROWSER_DISABLE) && defined(TMS_BACKEND_PC) && !defined(NO_UI) && !defined(TMS_BACKEND_EMSCRIPTEN) +#ifdef GTK3_LEVEL_BROWSER_ENABLE -// TODO maybe vendor? +#include "main.hh" +#include #include +#include + using json = nlohmann::json; -GtkCommunityDialog::GtkCommunityDialog() { +template +std::unique_ptr get_nullable(const nlohmann::json &j, const std::string &field_name) { + if (j.contains(field_name) && !j.at(field_name).is_null()) { + return std::make_unique(j.at(field_name).get()); + } + return nullptr; +} + +api::user::user(json &j) { + id = j["id"]; + name = j.at("name").get(); + customcolor = get_nullable(j, "customcolor"); +} + +api::recent_level::recent_level(json &j): u(j) { + id = j["id"]; + title = j["title"].get(); +} +api::level::level(json &j): u(j) { + id = j["id"]; + cat = j["cat"]; + title = j["title"].get(); + description = j["description"].get(); + author = j["author"]; + time = j["time"]; + parent = get_nullable(j, "parent"); + revision = j["revision"]; + revision_time = get_nullable(j, "revision_time"); + visibility = j["visibility"]; + views = j["views"]; + likes = j["likes"]; + downloads = j["downloads"]; + platform = j["platform"].get(); } -GtkCommunityDialog::~GtkCommunityDialog() { +// TODO move to network.cc? + +// TODO non-blocking request + +std::vector api::get_recent_levels(uint32_t offset, uint32_t limit) { + if (!P.curl) tms_fatalf("curl not initialized"); + SDL_LockMutex(P.curl_mutex); + + std::vector levels; + + char url[256]; + snprintf(url, 255, "https://%s/api/latest_levels?offset=%u&limit=%u", P.community_host, offset, limit); + + curl_easy_setopt(P.curl, CURLOPT_URL, url); + + std::string response; + curl_easy_setopt(P.curl, CURLOPT_WRITEDATA, &response); + + // TODO handle error + CURLcode res = curl_easy_perform(P.curl); + if (res != CURLE_OK) tms_fatalf("[fuck] curl error"); + + // TODO handle error + long http_code = 0; + curl_easy_getinfo(P.curl, CURLINFO_RESPONSE_CODE, &http_code); + if (http_code != 200) tms_fatalf("[fuck] HTTP error %ld", http_code); + + SDL_UnlockMutex(P.curl_mutex); + + json data = json::parse(response); + + // Data contains an array of api::recent_level + + for (const auto& item : data) { + levels.emplace_back(item); + } + + return levels; +} +// TODO implement this +api::level api::get_level(uint32_t id) { + tms_fatalf("not implemented"); exit(1); } #endif diff --git a/src/src/ui_gtk3_levelbrowser.hh b/src/src/ui_gtk3_levelbrowser.hh index 589ac38e..99fb5981 100644 --- a/src/src/ui_gtk3_levelbrowser.hh +++ b/src/src/ui_gtk3_levelbrowser.hh @@ -1,64 +1,70 @@ #pragma once -#include -#if !defined(GTK3_LEVEL_BROWSER_DISABLE) && defined(TMS_BACKEND_PC) && !defined(NO_UI) && !defined(TMS_BACKEND_EMSCRIPTEN) +#if !defined(GTK3_LEVEL_BROWSER_DISABLE) && defined(TMS_BACKEND_PC) && !defined(TMS_BACKEND_EMSCRIPTEN) && !defined(NO_UI) && defined(BUILD_CURL) + #define GTK3_LEVEL_BROWSER_ENABLE +#endif + +#ifdef GTK3_LEVEL_BROWSER_ENABLE +#include #include #include -// #include "optional.hh" - -#define COMMUNITY_LEVELS_PER_PAGE 16 - -class CommunityUser { - uint32_t id; - std::string name; - std::string customcolor; // TODO store as u32 instead -}; - -class CommunityRecentLevel { - uint32_t id; - std::string title; - CommunityUser u; -}; - -// {"id":1,"cat":1,"title":"not so smiley man","description":"finally\r\n\r\nnot so smiley man","author":1,"time":1608998715,"parent":null,"revision":1,"revision_time":null,"likes":21,"visibility":0,"views":433,"downloads":484,"platform":"Linux","u_id":1,"u_name":"ROllerozxa","u_customcolor":"31C03B"} - -class CommunityLevel { - uint32_t id; +#include - uint8_t cat; +using json = nlohmann::json; - std::string title; - std::string description; - - uint32_t author; - - uint32_t time; - uint32_t parent; - - uint32_t revision; - uint32_t revision_time; - - uint8_t visibility; - - uint32_t views; - uint32_t likes; - uint32_t downloads; - - std::string platform; +#define COMMUNITY_LEVELS_PER_PAGE 16 - CommunityUser u; +namespace api { + struct user { + uint32_t id; + std::string name; + std::unique_ptr customcolor; // TODO store as u32 instead + + user(json &j); + }; + + struct recent_level { + uint32_t id; + std::string title; + struct user u; + + recent_level(json &j); + }; + + struct level { + uint32_t id; + uint8_t cat; + std::string title; + std::string description; + uint32_t author; + uint32_t time; + std::unique_ptr parent; + uint32_t revision; + std::unique_ptr revision_time; + uint32_t likes; + uint8_t visibility; + uint32_t views; + uint32_t downloads; + std::string platform; + struct user u; + + level(json &j); + }; + + std::vector get_recent_levels(uint32_t offset, uint32_t limit); + level get_level(uint32_t id); }; -class GtkCommunityState { - uint16_t cur_page; - // std::unordered_map cache_thumbnails; - // std::unordered_map cache_pages; -}; +// class gtk_community_state { +// uint16_t cur_page; +// // std::unordered_map cache_thumbnails; +// // std::unordered_map cache_pages; +// }; -class GtkCommunityDialog { - GtkCommunityDialog(); - ~GtkCommunityDialog(); -}; +// class gtk_community_dialog { +// gtk_community_dialog(); +// ~gtk_community_dialog(); +// }; #endif From 9b4b655a5eb5df9a1b69ac637380b383945b43c0 Mon Sep 17 00:00:00 2001 From: griffi-gh Date: Mon, 14 Oct 2024 23:22:26 +0200 Subject: [PATCH 03/11] update cmake --- CMakeLists.txt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index 6d15a2af..8440d44c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -129,6 +129,11 @@ if(NOT SCREENSHOT_BUILD) include_directories( ${GLEW_INCLUDE_DIRS} ${GTK3_INCLUDE_DIRS}) + + # Only requrired by the level browser + # TODO: make the gtk3 level browser optional + find_package(nlohmann_json 3.2.0 REQUIRED) + include_directories(${NLOHMANN_JSON_INCLUDE_DIRS}) endif() find_package(CURL REQUIRED) From 94a8314481202ac8de1a1d74dc79d2fad4a9ad74 Mon Sep 17 00:00:00 2001 From: griffi-gh Date: Mon, 14 Oct 2024 23:23:39 +0200 Subject: [PATCH 04/11] add ui open call path (gtk3) --- src/src/ui.hh | 5 +++++ src/src/ui_gtk3.hh | 10 ++++++++++ src/src/ui_gtk3_levelbrowser.cc | 4 ++++ src/src/ui_gtk3_levelbrowser.hh | 6 ++++++ 4 files changed, 25 insertions(+) diff --git a/src/src/ui.hh b/src/src/ui.hh index e9115f39..43de73b7 100644 --- a/src/src/ui.hh +++ b/src/src/ui.hh @@ -73,6 +73,11 @@ #define DIALOG_DECORATION 161 #define DIALOG_SFXEMITTER_2 162 +// Open native level browser dialog +// ..or open community host if it's not +// supported by the current backend +#define DIALOG_HC_LEVEL_BROWSER 163 + #define CLOSE_ALL_DIALOGS 200 #define CLOSE_ABSOLUTELY_ALL_DIALOGS 201 #define CLOSE_REGISTER_DIALOG 202 diff --git a/src/src/ui_gtk3.hh b/src/src/ui_gtk3.hh index dcccd6ae..0ec83d6d 100644 --- a/src/src/ui_gtk3.hh +++ b/src/src/ui_gtk3.hh @@ -1,4 +1,5 @@ +#include "main.hh" #ifdef TMS_BACKEND_PC // fuckgtk3 @@ -8,6 +9,8 @@ #include #include +#include "ui_gtk3_levelbrowser.hh" + #ifdef USE_GTK_SOURCE_VIEW #include #endif @@ -11603,6 +11606,13 @@ ui::open_dialog(int num, void *data/*=0*/) break; case DIALOG_PROMPT_SETTINGS: gdk_threads_add_idle(_open_prompt_settings_dialog, 0); break; + case DIALOG_HC_LEVEL_BROWSER: + #ifdef GTK3_LEVEL_BROWSER_ENABLE + gdk_threads_add_idle(open_community_level_browser, 0); + #else + ui::open_url(P.community_host); + #endif + default: tms_warnf("Unhandled dialog ID: %d", num); break; diff --git a/src/src/ui_gtk3_levelbrowser.cc b/src/src/ui_gtk3_levelbrowser.cc index ab884150..44f8625b 100644 --- a/src/src/ui_gtk3_levelbrowser.cc +++ b/src/src/ui_gtk3_levelbrowser.cc @@ -91,5 +91,9 @@ api::level api::get_level(uint32_t id) { tms_fatalf("not implemented"); exit(1); } +void open_community_level_browser() { + tms_infof("====== Open level browser ======"); +} + #endif diff --git a/src/src/ui_gtk3_levelbrowser.hh b/src/src/ui_gtk3_levelbrowser.hh index 99fb5981..a023eaf9 100644 --- a/src/src/ui_gtk3_levelbrowser.hh +++ b/src/src/ui_gtk3_levelbrowser.hh @@ -56,6 +56,12 @@ namespace api { level get_level(uint32_t id); }; +// DO NOT USE THIS FUNCTION DIRECTLY +// Use ui::open_dialog(DIALOG_HC_LEVEL_BROWSER) instead! +// +// (If calling from ui::open_dialog, make sure to wrap it in gdk_threads_add_idle!) +void open_community_level_browser(); + // class gtk_community_state { // uint16_t cur_page; // // std::unordered_map cache_thumbnails; From 80007e1144b6074256f6fb5109c73512f9171ee1 Mon Sep 17 00:00:00 2001 From: griffi-gh Date: Mon, 14 Oct 2024 23:25:13 +0200 Subject: [PATCH 05/11] change call to HC_LEVEL_BROWSER --- src/src/menu_main.cc | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/src/menu_main.cc b/src/src/menu_main.cc index f77c93dc..2262be6e 100644 --- a/src/src/menu_main.cc +++ b/src/src/menu_main.cc @@ -24,10 +24,9 @@ menu_main::widget_clicked(principia_wdg *w, uint8_t button_id, int pid) P.add_action(ACTION_GOTO_CREATE, 1); break; - case BTN_BROWSE_COMMUNITY: { - COMMUNITY_URL(""); - ui::open_url(url); - } break; + case BTN_BROWSE_COMMUNITY: + ui::open_dialog(DIALOG_HC_LEVEL_BROWSER); + break; case BTN_UPDATE: { COMMUNITY_URL("download"); From 7e267da7c20eb6818392ad211d411bb14979248d Mon Sep 17 00:00:00 2001 From: griffi-gh Date: Mon, 14 Oct 2024 23:29:03 +0200 Subject: [PATCH 06/11] change args to const --- src/src/ui_gtk3_levelbrowser.hh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/src/ui_gtk3_levelbrowser.hh b/src/src/ui_gtk3_levelbrowser.hh index a023eaf9..5fef6957 100644 --- a/src/src/ui_gtk3_levelbrowser.hh +++ b/src/src/ui_gtk3_levelbrowser.hh @@ -21,7 +21,7 @@ namespace api { std::string name; std::unique_ptr customcolor; // TODO store as u32 instead - user(json &j); + user(const json &j); }; struct recent_level { @@ -29,7 +29,7 @@ namespace api { std::string title; struct user u; - recent_level(json &j); + recent_level(const json &j); }; struct level { @@ -49,7 +49,7 @@ namespace api { std::string platform; struct user u; - level(json &j); + level(const json &j); }; std::vector get_recent_levels(uint32_t offset, uint32_t limit); From e5f57055c6a2206c6a17f7b1b7e94f029bd8411b Mon Sep 17 00:00:00 2001 From: griffi-gh Date: Mon, 14 Oct 2024 23:34:36 +0200 Subject: [PATCH 07/11] fix stuff --- src/src/ui_gtk3.hh | 1 + src/src/ui_gtk3_levelbrowser.cc | 10 ++++++---- src/src/ui_gtk3_levelbrowser.hh | 3 ++- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/src/ui_gtk3.hh b/src/src/ui_gtk3.hh index 0ec83d6d..9cc9a5d0 100644 --- a/src/src/ui_gtk3.hh +++ b/src/src/ui_gtk3.hh @@ -11612,6 +11612,7 @@ ui::open_dialog(int num, void *data/*=0*/) #else ui::open_url(P.community_host); #endif + break; default: tms_warnf("Unhandled dialog ID: %d", num); diff --git a/src/src/ui_gtk3_levelbrowser.cc b/src/src/ui_gtk3_levelbrowser.cc index 44f8625b..223000f5 100644 --- a/src/src/ui_gtk3_levelbrowser.cc +++ b/src/src/ui_gtk3_levelbrowser.cc @@ -7,6 +7,7 @@ #include #include #include +#include using json = nlohmann::json; @@ -18,18 +19,18 @@ std::unique_ptr get_nullable(const nlohmann::json &j, const std::string &fiel return nullptr; } -api::user::user(json &j) { +api::user::user(const json &j) { id = j["id"]; name = j.at("name").get(); customcolor = get_nullable(j, "customcolor"); } -api::recent_level::recent_level(json &j): u(j) { +api::recent_level::recent_level(const json &j): u(j) { id = j["id"]; title = j["title"].get(); } -api::level::level(json &j): u(j) { +api::level::level(const json &j): u(j) { id = j["id"]; cat = j["cat"]; title = j["title"].get(); @@ -91,8 +92,9 @@ api::level api::get_level(uint32_t id) { tms_fatalf("not implemented"); exit(1); } -void open_community_level_browser() { +gboolean open_community_level_browser(gpointer _) { tms_infof("====== Open level browser ======"); + return false; } #endif diff --git a/src/src/ui_gtk3_levelbrowser.hh b/src/src/ui_gtk3_levelbrowser.hh index 5fef6957..5045fa3d 100644 --- a/src/src/ui_gtk3_levelbrowser.hh +++ b/src/src/ui_gtk3_levelbrowser.hh @@ -10,6 +10,7 @@ #include #include #include +#include using json = nlohmann::json; @@ -60,7 +61,7 @@ namespace api { // Use ui::open_dialog(DIALOG_HC_LEVEL_BROWSER) instead! // // (If calling from ui::open_dialog, make sure to wrap it in gdk_threads_add_idle!) -void open_community_level_browser(); +gboolean open_community_level_browser(gpointer _); // class gtk_community_state { // uint16_t cur_page; From d644d39c24f2d0f5b4d8e08235be89c740c2042d Mon Sep 17 00:00:00 2001 From: griffi-gh Date: Tue, 15 Oct 2024 00:39:26 +0200 Subject: [PATCH 08/11] it kinda works --- src/src/ui_gtk3_levelbrowser.cc | 224 +++++++++++++++++++++++--------- src/src/ui_gtk3_levelbrowser.hh | 17 ++- 2 files changed, 179 insertions(+), 62 deletions(-) diff --git a/src/src/ui_gtk3_levelbrowser.cc b/src/src/ui_gtk3_levelbrowser.cc index 223000f5..30eba1ef 100644 --- a/src/src/ui_gtk3_levelbrowser.cc +++ b/src/src/ui_gtk3_levelbrowser.cc @@ -1,99 +1,203 @@ #include "ui_gtk3_levelbrowser.hh" -#include "tms/backend/print.h" +#include + #ifdef GTK3_LEVEL_BROWSER_ENABLE #include "main.hh" +#include "tms/backend/print.h" + #include #include #include #include +#include -using json = nlohmann::json; +namespace api { + using json = nlohmann::json; -template -std::unique_ptr get_nullable(const nlohmann::json &j, const std::string &field_name) { - if (j.contains(field_name) && !j.at(field_name).is_null()) { - return std::make_unique(j.at(field_name).get()); + template + std::unique_ptr get_nullable(const nlohmann::json &j, const std::string &field_name) { + if (j.contains(field_name) && !j.at(field_name).is_null()) { + return std::make_unique(j.at(field_name).get()); + } + return nullptr; } - return nullptr; -} -api::user::user(const json &j) { - id = j["id"]; - name = j.at("name").get(); - customcolor = get_nullable(j, "customcolor"); -} + user::user(const json &j) { + id = j["u_id"]; + name = j.at("u_name").get(); + customcolor = get_nullable(j, "u_customcolor"); + } -api::recent_level::recent_level(const json &j): u(j) { - id = j["id"]; - title = j["title"].get(); -} + recent_level::recent_level(const json &j): u(j) { + id = j["id"]; + title = j["title"].get(); + } -api::level::level(const json &j): u(j) { - id = j["id"]; - cat = j["cat"]; - title = j["title"].get(); - description = j["description"].get(); - author = j["author"]; - time = j["time"]; - parent = get_nullable(j, "parent"); - revision = j["revision"]; - revision_time = get_nullable(j, "revision_time"); - visibility = j["visibility"]; - views = j["views"]; - likes = j["likes"]; - downloads = j["downloads"]; - platform = j["platform"].get(); -} + // recent_level::recent_level(const struct level l): u(l.u) { + // id = l.id; + // title = l.title; + // } + + level::level(const json &j): u(j) { + id = j["id"]; + cat = j["cat"]; + title = j["title"].get(); + description = j["description"].get(); + author = j["author"]; + time = j["time"]; + parent = get_nullable(j, "parent"); + revision = j["revision"]; + revision_time = get_nullable(j, "revision_time"); + visibility = j["visibility"]; + views = j["views"]; + likes = j["likes"]; + downloads = j["downloads"]; + platform = j["platform"].get(); + } + + // TODO move to network.cc? -// TODO move to network.cc? + // TODO non-blocking request -// TODO non-blocking request + static size_t WriteCallback(void *contents, size_t size, size_t nmemb, void *userp) { + ((std::string*)userp)->append((char*)contents, size * nmemb); + return size * nmemb; + } -std::vector api::get_recent_levels(uint32_t offset, uint32_t limit) { - if (!P.curl) tms_fatalf("curl not initialized"); - SDL_LockMutex(P.curl_mutex); + static std::vector get_recent_levels(uint32_t offset, uint32_t limit) { + if (!P.curl) tms_fatalf("curl not initialized"); + SDL_LockMutex(P.curl_mutex); - std::vector levels; + std::vector levels; - char url[256]; - snprintf(url, 255, "https://%s/api/latest_levels?offset=%u&limit=%u", P.community_host, offset, limit); + char url[256]; + snprintf(url, 255, "https://%s/api/latest_levels?offset=%u&limit=%u", P.community_host, offset, limit); - curl_easy_setopt(P.curl, CURLOPT_URL, url); + curl_easy_setopt(P.curl, CURLOPT_URL, url); - std::string response; - curl_easy_setopt(P.curl, CURLOPT_WRITEDATA, &response); + std::string response; + curl_easy_setopt(P.curl, CURLOPT_WRITEFUNCTION, WriteCallback); + curl_easy_setopt(P.curl, CURLOPT_WRITEDATA, &response); - // TODO handle error - CURLcode res = curl_easy_perform(P.curl); - if (res != CURLE_OK) tms_fatalf("[fuck] curl error"); + // TODO handle error + CURLcode res = curl_easy_perform(P.curl); + if (res != CURLE_OK) tms_fatalf("[fuck] curl error"); - // TODO handle error - long http_code = 0; - curl_easy_getinfo(P.curl, CURLINFO_RESPONSE_CODE, &http_code); - if (http_code != 200) tms_fatalf("[fuck] HTTP error %ld", http_code); + // TODO handle error + long http_code = 0; + curl_easy_getinfo(P.curl, CURLINFO_RESPONSE_CODE, &http_code); + if (http_code != 200) tms_fatalf("[fuck] HTTP error %ld", http_code); - SDL_UnlockMutex(P.curl_mutex); + SDL_UnlockMutex(P.curl_mutex); - json data = json::parse(response); + json data = json::parse(response); - // Data contains an array of api::recent_level + // Data contains an array of api::recent_level - for (const auto& item : data) { - levels.emplace_back(item); + for (const auto& item : data) { + levels.emplace_back(item); + } + + return levels; } - return levels; + // TODO implement this + static struct level get_level(uint32_t id) { + tms_fatalf("not implemented"); exit(1); + } } -// TODO implement this -api::level api::get_level(uint32_t id) { - tms_fatalf("not implemented"); exit(1); +namespace gtk_community { + // Callback function for clicking on the level title + static void on_level_clicked(GtkWidget *widget, gpointer data) { + g_print("Level clicked: %s\n", (char *)data); + } + + // Callback function for clicking on the username + static void on_username_clicked(GtkWidget *widget, gpointer data) { + g_print("Username clicked: %s\n", (char *)data); + } + + static GtkWidget* create_level_tile(const api::recent_level &level) { + // TODO this is placeholder + const char *title = level.title.c_str(); + const char *username = level.u.name.c_str(); + // Create a box to hold the tile elements vertically + GtkWidget *box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 5); + + // Placeholder for the level image (just a blank box for now) + GtkWidget *image = gtk_drawing_area_new(); + gtk_widget_set_size_request(image, 100, 100); // Placeholder size + gtk_widget_set_name(image, "level-image"); + + // Level title (clickable button) + GtkWidget *level_button = gtk_button_new_with_label(title); + g_signal_connect(level_button, "clicked", G_CALLBACK(on_level_clicked), (gpointer)title); + + // Username (clickable label) + // TODO set user color + GtkWidget *username_button = gtk_button_new_with_label(username); + gpointer user_id = (gpointer)(size_t)level.u.id; + g_signal_connect(username_button, "clicked", G_CALLBACK(on_username_clicked), (gpointer)user_id); + + // Pack elements into the box + gtk_box_pack_start(GTK_BOX(box), image, FALSE, FALSE, 0); + gtk_box_pack_start(GTK_BOX(box), level_button, FALSE, FALSE, 0); + gtk_box_pack_start(GTK_BOX(box), username_button, FALSE, FALSE, 0); + + return box; + } + + static GtkWidget* create_dialog(const std::vector &levels) { + GtkWidget *dialog = gtk_dialog_new_with_buttons( + "Community Levels", + NULL, + (GtkDialogFlags)(GTK_DIALOG_DESTROY_WITH_PARENT | GTK_DIALOG_MODAL), + ("_Close"), + GTK_RESPONSE_CLOSE, + NULL + ); + g_signal_connect(dialog, "response", G_CALLBACK(gtk_widget_destroy), NULL); + + // Get the content area of the dialog + GtkWidget *content_area = gtk_dialog_get_content_area(GTK_DIALOG(dialog)); + + // Create a grid to hold the level tiles + GtkWidget *grid = gtk_grid_new(); + gtk_grid_set_row_spacing(GTK_GRID(grid), 10); + gtk_grid_set_column_spacing(GTK_GRID(grid), 10); + gtk_container_add(GTK_CONTAINER(content_area), grid); + + // Add level tiles to the grid + int num_levels = 6; + int rows = 2; // 2 rows for now + int cols = 3; // 3 columns for now + + for (int i = 0; i < num_levels; i++) { + GtkWidget *level_tile = create_level_tile(levels[i]); + + // Attach each tile in the grid + int row = i / cols; + int col = i % cols; + gtk_grid_attach(GTK_GRID(grid), level_tile, col, row, 1, 1); + } + + return dialog; + } } gboolean open_community_level_browser(gpointer _) { tms_infof("====== Open level browser ======"); + + tms_infof("Fetching recent levels..."); + std::vector levels = api::get_recent_levels(0, 6); + + tms_infof("Creating dialog..."); + GtkWidget *dialog = gtk_community::create_dialog(levels); + gtk_widget_show_all(dialog); + return false; } diff --git a/src/src/ui_gtk3_levelbrowser.hh b/src/src/ui_gtk3_levelbrowser.hh index 5045fa3d..6b46cf65 100644 --- a/src/src/ui_gtk3_levelbrowser.hh +++ b/src/src/ui_gtk3_levelbrowser.hh @@ -1,5 +1,6 @@ #pragma once +#include #if !defined(GTK3_LEVEL_BROWSER_DISABLE) && defined(TMS_BACKEND_PC) && !defined(TMS_BACKEND_EMSCRIPTEN) && !defined(NO_UI) && defined(BUILD_CURL) #define GTK3_LEVEL_BROWSER_ENABLE #endif @@ -11,6 +12,7 @@ #include #include #include +#include using json = nlohmann::json; @@ -23,6 +25,7 @@ namespace api { std::unique_ptr customcolor; // TODO store as u32 instead user(const json &j); + // user(uint32_t id, std::string name); }; struct recent_level { @@ -31,6 +34,8 @@ namespace api { struct user u; recent_level(const json &j); + // recent_level(uint32_t id, const std::string &title, const struct user &u); + // recent_level(const struct level &l); }; struct level { @@ -53,10 +58,18 @@ namespace api { level(const json &j); }; - std::vector get_recent_levels(uint32_t offset, uint32_t limit); - level get_level(uint32_t id); + static std::vector get_recent_levels(uint32_t offset, uint32_t limit); + static level get_level(uint32_t id); }; +namespace gtk_community { + static void on_level_clicked(GtkWidget *widget, gpointer data); + static void on_username_clicked(GtkWidget *widget, gpointer data); + // Creates a single level tile (image, title, and username) + static GtkWidget *create_level_tile(const api::recent_level &level); + static GtkWidget *create_dialog(const std::vector &levels); +} + // DO NOT USE THIS FUNCTION DIRECTLY // Use ui::open_dialog(DIALOG_HC_LEVEL_BROWSER) instead! // From 85ba7478e3ce7b586c07c9051f788d303ab6fa36 Mon Sep 17 00:00:00 2001 From: griffi-gh Date: Tue, 15 Oct 2024 01:18:54 +0200 Subject: [PATCH 09/11] make buttons work --- src/src/ui_gtk3_levelbrowser.cc | 27 +++++++++++++++++++++------ 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/src/src/ui_gtk3_levelbrowser.cc b/src/src/ui_gtk3_levelbrowser.cc index 30eba1ef..98ad1861 100644 --- a/src/src/ui_gtk3_levelbrowser.cc +++ b/src/src/ui_gtk3_levelbrowser.cc @@ -1,12 +1,14 @@ #include "ui_gtk3_levelbrowser.hh" -#include - #ifdef GTK3_LEVEL_BROWSER_ENABLE #include "main.hh" +#include "const.hh" +#include "network.hh" +#include "ui.hh" #include "tms/backend/print.h" +#include #include #include #include @@ -112,12 +114,21 @@ namespace api { namespace gtk_community { // Callback function for clicking on the level title static void on_level_clicked(GtkWidget *widget, gpointer data) { - g_print("Level clicked: %s\n", (char *)data); - } + uint32_t level_id = (size_t)data; + g_print("Level clicked: %d\n", level_id); + _play_id = level_id; + _play_type = LEVEL_DB; + P.add_action(ACTION_OPEN_PLAY, 0); + } // Callback function for clicking on the username static void on_username_clicked(GtkWidget *widget, gpointer data) { - g_print("Username clicked: %s\n", (char *)data); + uint32_t user_id = (size_t)data; + g_print("Level clicked: %d\n", user_id); + { + COMMUNITY_URL("user/%d", user_id); + ui::open_url(url); + }; } static GtkWidget* create_level_tile(const api::recent_level &level) { @@ -134,7 +145,8 @@ namespace gtk_community { // Level title (clickable button) GtkWidget *level_button = gtk_button_new_with_label(title); - g_signal_connect(level_button, "clicked", G_CALLBACK(on_level_clicked), (gpointer)title); + gpointer level_id = (gpointer)(size_t)level.id; + g_signal_connect(level_button, "clicked", G_CALLBACK(on_level_clicked), (gpointer)level_id); // Username (clickable label) // TODO set user color @@ -155,10 +167,13 @@ namespace gtk_community { "Community Levels", NULL, (GtkDialogFlags)(GTK_DIALOG_DESTROY_WITH_PARENT | GTK_DIALOG_MODAL), + ("_Open Full Website"), + GTK_RESPONSE_ACCEPT, ("_Close"), GTK_RESPONSE_CLOSE, NULL ); + gtk_window_set_keep_above(GTK_WINDOW(dialog), true); g_signal_connect(dialog, "response", G_CALLBACK(gtk_widget_destroy), NULL); // Get the content area of the dialog From dcfd9d948245c5435c5cfd9ed5dcb50f2ee74345 Mon Sep 17 00:00:00 2001 From: griffi-gh Date: Tue, 15 Oct 2024 10:34:18 +0200 Subject: [PATCH 10/11] make it fancy --- src/src/ui_gtk3.hh | 4 + src/src/ui_gtk3_levelbrowser.cc | 261 ++++++++++++++++++++++++++++---- src/src/ui_gtk3_levelbrowser.hh | 7 + 3 files changed, 246 insertions(+), 26 deletions(-) diff --git a/src/src/ui_gtk3.hh b/src/src/ui_gtk3.hh index 9cc9a5d0..36ba1679 100644 --- a/src/src/ui_gtk3.hh +++ b/src/src/ui_gtk3.hh @@ -6280,6 +6280,10 @@ int _gtk_loop(void *p) //Load CSS themes load_gtk_css(); +#ifdef GTK3_LEVEL_BROWSER_ENABLE + init_community_level_browser(); +#endif + g_object_set( gtk_settings_get_default(), "gtk-application-prefer-dark-theme", true, diff --git a/src/src/ui_gtk3_levelbrowser.cc b/src/src/ui_gtk3_levelbrowser.cc index 98ad1861..6171481d 100644 --- a/src/src/ui_gtk3_levelbrowser.cc +++ b/src/src/ui_gtk3_levelbrowser.cc @@ -9,11 +9,17 @@ #include "tms/backend/print.h" #include +#include #include #include #include #include #include +#include "pango/pango-layout.h" +#include "gio/gio.h" +#include +#include +#include namespace api { using json = nlohmann::json; @@ -68,6 +74,12 @@ namespace api { return size * nmemb; } + static size_t WriteCallbackBinary(void *contents, size_t size, size_t nmemb, void *userp) { + std::vector *vec = (std::vector*)userp; + vec->insert(vec->end(), (uint8_t*)contents, (uint8_t*)contents + size * nmemb); + return size * nmemb; + } + static std::vector get_recent_levels(uint32_t offset, uint32_t limit) { if (!P.curl) tms_fatalf("curl not initialized"); SDL_LockMutex(P.curl_mutex); @@ -109,6 +121,63 @@ namespace api { static struct level get_level(uint32_t id) { tms_fatalf("not implemented"); exit(1); } + + static std::vector get_level_thumbnail(uint32_t id, bool use_global_curl /* = true */) { + CURL* curl; + + if (use_global_curl) { + // TODO instad of this use the multi API + curl = curl_easy_init(); + } else { + if (!P.curl) tms_fatalf("curl not initialized"); + SDL_LockMutex(P.curl_mutex); + curl = P.curl; + } + + char url[256]; + snprintf(url, 255, "https://%s/thumbs/low/%u.jpg", P.community_host, id); + + curl_easy_setopt(curl, CURLOPT_URL, url); + + std::vector response; + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteCallbackBinary); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response); + + // TODO handle error + CURLcode res = curl_easy_perform(curl); + if (res != CURLE_OK) tms_fatalf("[fuck] curl error"); + + // TODO handle error + long http_code = 0; + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code); + if (http_code != 200) tms_fatalf("[fuck] HTTP error %ld", http_code); + + // Sleep to simulate slow connection + // std::this_thread::sleep_for(std::chrono::seconds(5)); + + if (use_global_curl) { + curl_easy_cleanup(curl); + } else { + SDL_UnlockMutex(P.curl_mutex); + } + + return response; + } +} + +void _api_get_level_thumbnail_async(uint32_t id, GAsyncReadyCallback callback, gpointer user_data) { + GTask *task = g_task_new(NULL, NULL, callback, user_data); + g_task_set_source_tag(task, (gpointer)_api_get_level_thumbnail_async); + g_task_set_task_data(task, (gpointer)(size_t)id, NULL); + + g_task_run_in_thread(task, [](GTask *task, gpointer source_object, gpointer task_data, GCancellable *cancellable) { + uint32_t id = (size_t)task_data; + std::vector thumbnail = api::get_level_thumbnail(id, false); + std::vector *thumbnail_heap = new std::vector(thumbnail); + tms_infof("Async thumbnail loaded %p", thumbnail_heap); + g_task_return_pointer(task, thumbnail_heap, free); + }); + // g_task_return_pointer(task, NULL, g_free); } namespace gtk_community { @@ -135,23 +204,85 @@ namespace gtk_community { // TODO this is placeholder const char *title = level.title.c_str(); const char *username = level.u.name.c_str(); + gpointer user_id = (gpointer)(size_t)level.u.id; + gpointer level_id = (gpointer)(size_t)level.id; + + // Create a flow child (activatable) + // GtkWidget *child = gtk_flow_box_child_new(); + // g_signal_connect(child, "activate", G_CALLBACK(on_level_clicked), (gpointer)level_id); + // g_signal_connect(child, "clicked", G_CALLBACK(on_level_clicked), (gpointer)level_id); + // Create a box to hold the tile elements vertically GtkWidget *box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 5); + GtkStyleContext *context = gtk_widget_get_style_context(GTK_WIDGET(box)); + gtk_style_context_add_class(context, "hc-level-tile"); + + // gtk_container_add(GTK_CONTAINER(child), box); // Placeholder for the level image (just a blank box for now) - GtkWidget *image = gtk_drawing_area_new(); - gtk_widget_set_size_request(image, 100, 100); // Placeholder size - gtk_widget_set_name(image, "level-image"); + GtkWidget *image = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); + gtk_widget_set_size_request(image, 240, 135); // Placeholder size + // Kick off async thumbnail fetch + GAsyncReadyCallback callback = [](GObject *source_object, GAsyncResult *res, gpointer user_data) { + // XXX: some of this may leak memory :< + + GTask *task = G_TASK(res); + std::vector *result = (std::vector*)g_task_propagate_pointer(task, NULL); + tms_infof("Thumbnail loaded %p", result); + GtkWidget *image = GTK_WIDGET(user_data); + + tms_debugf("Image data size: %zu", result->size()); + + GdkPixbufLoader *loader = gdk_pixbuf_loader_new_with_type("jpeg", NULL); + gdk_pixbuf_loader_write(loader, result->data(), result->size(), NULL); + gdk_pixbuf_loader_close(loader, NULL); + GdkPixbuf *pixbuf = gdk_pixbuf_loader_get_pixbuf(loader); + + tms_debugf("pixbuf pointer: %p", pixbuf); + tms_debugf("pixbuf image size: %d x %d", gdk_pixbuf_get_width(pixbuf), gdk_pixbuf_get_height(pixbuf)); + + GtkWidget *image_widget = gtk_image_new_from_pixbuf(pixbuf); + gtk_container_add(GTK_CONTAINER(image), image_widget); + gtk_widget_show_all(image); + + g_object_unref(loader); + delete result; + }; + _api_get_level_thumbnail_async(level.id, callback, image); // Level title (clickable button) - GtkWidget *level_button = gtk_button_new_with_label(title); - gpointer level_id = (gpointer)(size_t)level.id; + GtkWidget *level_label = gtk_label_new(title); + gtk_label_set_xalign(GTK_LABEL(level_label), 0.5); + // gtk_label_set_lines(GTK_LABEL(level_label), 2); + gtk_label_set_ellipsize(GTK_LABEL(level_label), PANGO_ELLIPSIZE_END); + gtk_label_set_width_chars(GTK_LABEL(level_label), 20); + gtk_label_set_max_width_chars(GTK_LABEL(level_label), 20); + + GtkWidget *level_button = gtk_button_new(); + gtk_button_set_relief(GTK_BUTTON(level_button), GTK_RELIEF_NONE); + gtk_container_add(GTK_CONTAINER(level_button), level_label); g_signal_connect(level_button, "clicked", G_CALLBACK(on_level_clicked), (gpointer)level_id); // Username (clickable label) // TODO set user color - GtkWidget *username_button = gtk_button_new_with_label(username); - gpointer user_id = (gpointer)(size_t)level.u.id; + GtkWidget *username_label = gtk_label_new(username); + gtk_label_set_xalign(GTK_LABEL(username_label), 0.5); + gtk_label_set_lines(GTK_LABEL(username_label), 1); + gtk_label_set_ellipsize(GTK_LABEL(username_label), PANGO_ELLIPSIZE_END); + gtk_label_set_width_chars(GTK_LABEL(username_label), 20); + gtk_label_set_max_width_chars(GTK_LABEL(username_label), 20); + // HACK: override color + const static GdkRGBA rgba_color = { + .red = 109. / 255. , + .green = 160. / 255., + .blue = 253 / 255., + .alpha = 1.0, + }; + gtk_widget_override_color(username_label, GTK_STATE_FLAG_NORMAL, &rgba_color); + + GtkWidget *username_button = gtk_button_new(); + gtk_button_set_relief(GTK_BUTTON(username_button), GTK_RELIEF_NONE); + gtk_container_add(GTK_CONTAINER(username_button), username_label); g_signal_connect(username_button, "clicked", G_CALLBACK(on_username_clicked), (gpointer)user_id); // Pack elements into the box @@ -162,7 +293,70 @@ namespace gtk_community { return box; } + static GtkWidget *create_level_grid(const std::vector &levels) { + // Create a grid to hold the level tiles + GtkWidget* flow_box = gtk_flow_box_new(); + gtk_flow_box_set_max_children_per_line(GTK_FLOW_BOX(flow_box), 6); + gtk_flow_box_set_min_children_per_line(GTK_FLOW_BOX(flow_box), 4); + gtk_flow_box_set_selection_mode(GTK_FLOW_BOX(flow_box), GTK_SELECTION_NONE); + // gtk_flow_box_unselect_all(GTK_FLOW_BOX(flow_box)); + // gtk_flow_box_set_activate_on_single_click(GTK_FLOW_BOX(flow_box), true); + // gtk_container_add(GTK_CONTAINER(content_area), flow_box); + + // Add level tiles to the grid + for (auto &level : levels) { + // Create level tile + GtkWidget *level_tile = create_level_tile(level); + + // Attach each tile in the grid + gtk_container_add(GTK_CONTAINER(flow_box), level_tile); + } + + return flow_box; + } + + static GtkWidget* create_shelf(std::string name, GtkWidget *content) { + // Create a box to hold the shelf elements vertically + GtkWidget *box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 5); + + // Shelf header + GtkWidget *header = gtk_label_new(name.c_str()); + // HACK: override font size + PangoFontDescription *font_desc = pango_font_description_new(); + pango_font_description_set_size(font_desc, 24 * PANGO_SCALE); + gtk_widget_override_font(header, font_desc); + gtk_container_add(GTK_CONTAINER(box), header); + + // Shelf content + gtk_container_add(GTK_CONTAINER(box), content); + + return box; + } + + // Create top shelf. It contains: + // Input box + button to open level by ID or search + // (If number is entered, open level by ID, otherwise search) + static GtkWidget* create_top_shelf_content() { + GtkWidget *box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 5); + gtk_widget_set_halign(GTK_WIDGET(box), GTK_ALIGN_CENTER); + gtk_widget_set_valign(GTK_WIDGET(box), GTK_ALIGN_CENTER); + + // Search icon + GtkWidget *search_icon = gtk_image_new_from_icon_name("edit-find-symbolic", GTK_ICON_SIZE_BUTTON); + gtk_container_add(GTK_CONTAINER(box), search_icon); + + // Input box + GtkWidget *entry = gtk_entry_new(); + gtk_entry_set_placeholder_text(GTK_ENTRY(entry), "Enter level ID or search query"); + gtk_entry_set_width_chars(GTK_ENTRY(entry), 40); + gtk_box_pack_start(GTK_BOX(box), entry, FALSE, TRUE, 0); + gtk_box_set_center_widget(GTK_BOX(box), entry); + + return box; + } + static GtkWidget* create_dialog(const std::vector &levels) { + // Create a dialog window GtkWidget *dialog = gtk_dialog_new_with_buttons( "Community Levels", NULL, @@ -179,25 +373,15 @@ namespace gtk_community { // Get the content area of the dialog GtkWidget *content_area = gtk_dialog_get_content_area(GTK_DIALOG(dialog)); - // Create a grid to hold the level tiles - GtkWidget *grid = gtk_grid_new(); - gtk_grid_set_row_spacing(GTK_GRID(grid), 10); - gtk_grid_set_column_spacing(GTK_GRID(grid), 10); - gtk_container_add(GTK_CONTAINER(content_area), grid); + // Add the top shelf + GtkWidget *top_shelf_content = create_top_shelf_content(); + GtkWidget *top_shelf = create_shelf("Open level", top_shelf_content); + gtk_container_add(GTK_CONTAINER(content_area), top_shelf); - // Add level tiles to the grid - int num_levels = 6; - int rows = 2; // 2 rows for now - int cols = 3; // 3 columns for now - - for (int i = 0; i < num_levels; i++) { - GtkWidget *level_tile = create_level_tile(levels[i]); - - // Attach each tile in the grid - int row = i / cols; - int col = i % cols; - gtk_grid_attach(GTK_GRID(grid), level_tile, col, row, 1, 1); - } + // Add the level shelf to the dialog + GtkWidget *level_grid = create_level_grid(levels); + GtkWidget *level_shelf = create_shelf("Recent Levels", level_grid); + gtk_container_add(GTK_CONTAINER(content_area), level_shelf); return dialog; } @@ -207,7 +391,7 @@ gboolean open_community_level_browser(gpointer _) { tms_infof("====== Open level browser ======"); tms_infof("Fetching recent levels..."); - std::vector levels = api::get_recent_levels(0, 6); + std::vector levels = api::get_recent_levels(0, 12); tms_infof("Creating dialog..."); GtkWidget *dialog = gtk_community::create_dialog(levels); @@ -216,5 +400,30 @@ gboolean open_community_level_browser(gpointer _) { return false; } +void init_community_level_browser() { + const gchar* css_global = R"( + .hc-level-tile { + padding: 5px; + border: 1px solid #444; + border-radius: 4px; + background-color: #060606; + } + .hc-level-tile button { + padding: 0; + } + )"; + GtkCssProvider* css_provider = gtk_css_provider_new(); + gtk_css_provider_load_from_data( + css_provider, + css_global, + -1, NULL + ); + gtk_style_context_add_provider_for_screen( + gdk_screen_get_default(), + GTK_STYLE_PROVIDER(css_provider), + GTK_STYLE_PROVIDER_PRIORITY_APPLICATION + ); +} + #endif diff --git a/src/src/ui_gtk3_levelbrowser.hh b/src/src/ui_gtk3_levelbrowser.hh index 6b46cf65..f64b0ae3 100644 --- a/src/src/ui_gtk3_levelbrowser.hh +++ b/src/src/ui_gtk3_levelbrowser.hh @@ -60,6 +60,7 @@ namespace api { static std::vector get_recent_levels(uint32_t offset, uint32_t limit); static level get_level(uint32_t id); + static std::vector get_level_thumbnail(uint32_t id, bool use_global_curl = true); }; namespace gtk_community { @@ -67,6 +68,9 @@ namespace gtk_community { static void on_username_clicked(GtkWidget *widget, gpointer data); // Creates a single level tile (image, title, and username) static GtkWidget *create_level_tile(const api::recent_level &level); + static GtkWidget *create_level_grid(const std::vector &levels); + static GtkWidget *create_top_shelf_content(); + static GtkWidget *create_shelf(std::string name, GtkWidget *content); static GtkWidget *create_dialog(const std::vector &levels); } @@ -76,6 +80,9 @@ namespace gtk_community { // (If calling from ui::open_dialog, make sure to wrap it in gdk_threads_add_idle!) gboolean open_community_level_browser(gpointer _); +// Call in ui::init() after gtk_init() +void init_community_level_browser(); + // class gtk_community_state { // uint16_t cur_page; // // std::unordered_map cache_thumbnails; From ee64206a91677b3e7f1662d8f94bdfad137c549e Mon Sep 17 00:00:00 2001 From: griffi-gh Date: Tue, 15 Oct 2024 10:52:25 +0200 Subject: [PATCH 11/11] add search feature --- src/src/ui_gtk3_levelbrowser.cc | 63 ++++++++++++++++++++++++++++++--- src/src/ui_gtk3_levelbrowser.hh | 5 ++- 2 files changed, 63 insertions(+), 5 deletions(-) diff --git a/src/src/ui_gtk3_levelbrowser.cc b/src/src/ui_gtk3_levelbrowser.cc index 6171481d..f8c8fac7 100644 --- a/src/src/ui_gtk3_levelbrowser.cc +++ b/src/src/ui_gtk3_levelbrowser.cc @@ -11,15 +11,15 @@ #include #include #include +#include #include #include #include #include -#include "pango/pango-layout.h" #include "gio/gio.h" -#include -#include -#include +#include "glib-object.h" +#include "pango/pango-layout.h" +// #include namespace api { using json = nlohmann::json; @@ -188,6 +188,8 @@ namespace gtk_community { _play_id = level_id; _play_type = LEVEL_DB; P.add_action(ACTION_OPEN_PLAY, 0); + + gtk_widget_destroy(global_dialog); } // Callback function for clicking on the username @@ -200,6 +202,57 @@ namespace gtk_community { }; } + // Callback function for activating the search entry + static void on_search_activate(GtkWidget *widget, gpointer data) { + const char *text = gtk_entry_get_text(GTK_ENTRY(widget)); + g_print("Search activated: %s\n", text); + // Check if id is entered + // Valid syntax: + // number or "id:number": open level by ID + // otherwise, use ui::open_url to search on the website + + bool should_play = false; + uint32_t play_db_id; + + // Case 1: text *only* contains ASCII digits + bool is_number = true; + for (int i = 0; text[i] != '\0'; i++) { + if (text[i] < '0' || text[i] > '9') { + is_number = false; + break; + } + } + if (is_number) { + uint32_t level_id = atoi(text); + g_print("Level ID: %d\n", level_id); + should_play = true; + play_db_id = level_id; + } + + // Case 2: starts with id: + if (text[0] == 'i' && text[1] == 'd' && text[2] == ':') { + uint32_t level_id = atoi(text + 3); + g_print("Level ID: %d\n", level_id); + should_play = true; + play_db_id = level_id; + } + + if (should_play) { + _play_id = play_db_id; + _play_type = LEVEL_DB; + P.add_action(ACTION_OPEN_PLAY, 0); + + gtk_widget_destroy(global_dialog); + return; + } + + // Case 3: search on the website + { + COMMUNITY_URL("search?query=%s", text); + ui::open_url(url); + } + } + static GtkWidget* create_level_tile(const api::recent_level &level) { // TODO this is placeholder const char *title = level.title.c_str(); @@ -351,6 +404,7 @@ namespace gtk_community { gtk_entry_set_width_chars(GTK_ENTRY(entry), 40); gtk_box_pack_start(GTK_BOX(box), entry, FALSE, TRUE, 0); gtk_box_set_center_widget(GTK_BOX(box), entry); + g_signal_connect(entry, "activate", G_CALLBACK(on_search_activate), NULL); return box; } @@ -395,6 +449,7 @@ gboolean open_community_level_browser(gpointer _) { tms_infof("Creating dialog..."); GtkWidget *dialog = gtk_community::create_dialog(levels); + gtk_community::global_dialog = dialog; gtk_widget_show_all(dialog); return false; diff --git a/src/src/ui_gtk3_levelbrowser.hh b/src/src/ui_gtk3_levelbrowser.hh index f64b0ae3..7d1fb4bc 100644 --- a/src/src/ui_gtk3_levelbrowser.hh +++ b/src/src/ui_gtk3_levelbrowser.hh @@ -64,9 +64,12 @@ namespace api { }; namespace gtk_community { + static GtkWidget *global_dialog; + static void on_level_clicked(GtkWidget *widget, gpointer data); static void on_username_clicked(GtkWidget *widget, gpointer data); - // Creates a single level tile (image, title, and username) + static void on_search_activate(GtkWidget *widget, gpointer data); + static GtkWidget *create_level_tile(const api::recent_level &level); static GtkWidget *create_level_grid(const std::vector &levels); static GtkWidget *create_top_shelf_content();