|
| 1 | +#include "repository_client.h" |
| 2 | +#include <stdio.h> |
| 3 | +#include <string.h> |
| 4 | +#include "bsp/input.h" |
| 5 | +#include "cJSON.h" |
| 6 | +#include "common/display.h" |
| 7 | +#include "esp_log.h" |
| 8 | +#include "gui_menu.h" |
| 9 | +#include "gui_style.h" |
| 10 | +#include "http_download.h" |
| 11 | +#include "icons.h" |
| 12 | +#include "menu/message_dialog.h" |
| 13 | +#include "pax_codecs.h" |
| 14 | +#include "pax_types.h" |
| 15 | +#include "wifi_connection.h" |
| 16 | + |
| 17 | +#define MAX_PROJECTS 32 |
| 18 | + |
| 19 | +extern bool wifi_stack_get_initialized(void); |
| 20 | + |
| 21 | +static const char* TAG = "Repository client"; |
| 22 | + |
| 23 | +static pax_buf_t icons[MAX_PROJECTS] = {0}; |
| 24 | + |
| 25 | +#if defined(CONFIG_BSP_TARGET_KAMI) |
| 26 | +#define ICON_WIDTH 32 |
| 27 | +#define ICON_HEIGHT 32 |
| 28 | +#define ICON_BUFFER_SIZE (ICON_WIDTH * ICON_HEIGHT * 4) // 32x32 pixels, 2 bits per pixel |
| 29 | +#define ICON_COLOR_FORMAT PAX_BUF_2_PAL |
| 30 | +#else |
| 31 | +#define ICON_WIDTH 32 |
| 32 | +#define ICON_HEIGHT 32 |
| 33 | +#define ICON_BUFFER_SIZE (ICON_WIDTH * ICON_HEIGHT * 4) // 32x32 pixels, 4 bytes per pixel (ARGB8888) |
| 34 | +#define ICON_COLOR_FORMAT PAX_BUF_32_8888ARGB |
| 35 | +#endif |
| 36 | + |
| 37 | +// Repository |
| 38 | + |
| 39 | +static char* data_projects = NULL; |
| 40 | +static size_t size_projects = 0; |
| 41 | +static cJSON* json_projects = NULL; |
| 42 | + |
| 43 | +static char* data_information = NULL; |
| 44 | +static size_t size_information = 0; |
| 45 | +static cJSON* json_information = NULL; |
| 46 | + |
| 47 | +static bool load_information(void) { |
| 48 | + if (data_information == NULL) { |
| 49 | + char url[128]; |
| 50 | + sprintf(url, "https://apps.tanmatsu.cloud/v1/information"); |
| 51 | + bool success = download_ram(url, (uint8_t**)&data_information, &size_information); |
| 52 | + if (!success) return false; |
| 53 | + } |
| 54 | + if (data_information == NULL) return false; |
| 55 | + json_information = cJSON_ParseWithLength(data_information, size_information); |
| 56 | + if (json_information == NULL) return false; |
| 57 | + return true; |
| 58 | +} |
| 59 | + |
| 60 | +static void free_information(void) { |
| 61 | + if (json_information != NULL) { |
| 62 | + cJSON_Delete(json_information); |
| 63 | + json_information = NULL; |
| 64 | + } |
| 65 | + if (data_information != NULL) { |
| 66 | + free(data_information); |
| 67 | + data_information = NULL; |
| 68 | + size_information = 0; |
| 69 | + } |
| 70 | +} |
| 71 | + |
| 72 | +static bool load_projects(void) { |
| 73 | + if (data_projects == NULL) { |
| 74 | + char url[128]; |
| 75 | + sprintf(url, "https://apps.tanmatsu.cloud/v1/projects?amount=%u", MAX_PROJECTS); |
| 76 | + bool success = download_ram(url, (uint8_t**)&data_projects, &size_projects); |
| 77 | + if (!success) return false; |
| 78 | + } |
| 79 | + if (data_projects == NULL) return false; |
| 80 | + json_projects = cJSON_ParseWithLength(data_projects, size_projects); |
| 81 | + if (json_projects == NULL) return false; |
| 82 | + return true; |
| 83 | +} |
| 84 | + |
| 85 | +static void free_projects(void) { |
| 86 | + if (json_projects != NULL) { |
| 87 | + cJSON_Delete(json_projects); |
| 88 | + json_projects = NULL; |
| 89 | + } |
| 90 | + if (data_projects != NULL) { |
| 91 | + free(data_projects); |
| 92 | + data_projects = NULL; |
| 93 | + size_projects = 0; |
| 94 | + } |
| 95 | +} |
| 96 | + |
| 97 | +static void populate_project_list(menu_t* menu) { |
| 98 | + cJSON* entry_obj; |
| 99 | + int i = 0; |
| 100 | + cJSON_ArrayForEach(entry_obj, json_projects) { |
| 101 | + cJSON* slug_obj = cJSON_GetObjectItem(entry_obj, "slug"); |
| 102 | + cJSON* project_obj = cJSON_GetObjectItem(entry_obj, "project"); |
| 103 | + if (project_obj == NULL) { |
| 104 | + ESP_LOGE(TAG, "Project object is NULL for entry %d", i); |
| 105 | + continue; |
| 106 | + } |
| 107 | + cJSON* name_obj = cJSON_GetObjectItem(project_obj, "name"); |
| 108 | + if (name_obj == NULL) { |
| 109 | + ESP_LOGE(TAG, "Name object is NULL for entry %d", i); |
| 110 | + continue; |
| 111 | + } |
| 112 | + printf("Project [%s]: %s\r\n", slug_obj->valuestring, name_obj->valuestring); |
| 113 | + menu_insert_item_icon(menu, name_obj->valuestring, NULL, (void*)i, -1, get_icon(ICON_DOWNLOADING)); |
| 114 | + |
| 115 | + void* buffer = heap_caps_calloc(1, ICON_BUFFER_SIZE, MALLOC_CAP_SPIRAM); |
| 116 | + if (buffer == NULL) { |
| 117 | + ESP_LOGE(TAG, "Failed to allocate memory for icon for entry %u", i); |
| 118 | + continue; |
| 119 | + } |
| 120 | + pax_buf_init(&icons[i], buffer, ICON_WIDTH, ICON_HEIGHT, ICON_COLOR_FORMAT); |
| 121 | +#if defined(CONFIG_BSP_TARGET_KAMI) |
| 122 | + icons[i].palette = palette; |
| 123 | + icons[i].palette_size = sizeof(palette) / sizeof(pax_col_t); |
| 124 | +#endif |
| 125 | + /*if (!pax_insert_png_buf(&icons[i], icon_buf, icon_size, 0, 0, 0)) { |
| 126 | + ESP_LOGE(TAG, "Failed to decode icon for entry %u", i); |
| 127 | + }*/ |
| 128 | + |
| 129 | + i++; |
| 130 | + } |
| 131 | +} |
| 132 | + |
| 133 | +static void render(pax_buf_t* buffer, gui_theme_t* theme, menu_t* menu, bool partial, bool icons) { |
| 134 | + int header_height = theme->header.height + (theme->header.vertical_margin * 2); |
| 135 | + int footer_height = theme->footer.height + (theme->footer.vertical_margin * 2); |
| 136 | + |
| 137 | + pax_vec2_t position = { |
| 138 | + .x0 = theme->menu.horizontal_margin + theme->menu.horizontal_padding, |
| 139 | + .y0 = header_height + theme->menu.vertical_margin + theme->menu.vertical_padding, |
| 140 | + .x1 = pax_buf_get_width(buffer) - theme->menu.horizontal_margin - theme->menu.horizontal_padding, |
| 141 | + .y1 = pax_buf_get_height(buffer) - footer_height - theme->menu.vertical_margin - theme->menu.vertical_padding, |
| 142 | + }; |
| 143 | + |
| 144 | + if (!partial || icons) { |
| 145 | + render_base_screen_statusbar(buffer, theme, !partial, !partial || icons, !partial, |
| 146 | + ((gui_header_field_t[]){{get_icon(ICON_REPOSITORY), "Repository"}}), 1, |
| 147 | + ((gui_header_field_t[]){{get_icon(ICON_ESC), "/"}, {get_icon(ICON_F1), "Back"}}), |
| 148 | + 2, ((gui_header_field_t[]){{NULL, "↑ / ↓ Navigate ⏎ Select"}}), 1); |
| 149 | + } |
| 150 | + menu_render(buffer, menu, position, theme, partial); |
| 151 | + display_blit_buffer(buffer); |
| 152 | +} |
| 153 | + |
| 154 | +void menu_repository_client(pax_buf_t* buffer, gui_theme_t* theme) { |
| 155 | + if (!wifi_stack_get_initialized()) { |
| 156 | + ESP_LOGE(TAG, "WiFi stack not initialized"); |
| 157 | + return; |
| 158 | + } |
| 159 | + |
| 160 | + if (!wifi_connection_is_connected()) { |
| 161 | + if (wifi_connect_try_all() != ESP_OK) { |
| 162 | + ESP_LOGE(TAG, "Not connected to WiFi"); |
| 163 | + return; |
| 164 | + } |
| 165 | + } |
| 166 | + |
| 167 | + bool success = load_information(); |
| 168 | + if (!success) { |
| 169 | + ESP_LOGE(TAG, "Failed to load repository information"); |
| 170 | + return; |
| 171 | + } |
| 172 | + |
| 173 | + success = load_projects(); |
| 174 | + if (!success) { |
| 175 | + ESP_LOGE(TAG, "Failed to load projects"); |
| 176 | + return; |
| 177 | + } |
| 178 | + |
| 179 | + QueueHandle_t input_event_queue = NULL; |
| 180 | + ESP_ERROR_CHECK(bsp_input_get_queue(&input_event_queue)); |
| 181 | + |
| 182 | + menu_t menu = {0}; |
| 183 | + menu_initialize(&menu); |
| 184 | + populate_project_list(&menu); |
| 185 | + |
| 186 | + render(buffer, theme, &menu, false, true); |
| 187 | + while (1) { |
| 188 | + bsp_input_event_t event; |
| 189 | + if (xQueueReceive(input_event_queue, &event, pdMS_TO_TICKS(1000)) == pdTRUE) { |
| 190 | + switch (event.type) { |
| 191 | + case INPUT_EVENT_TYPE_NAVIGATION: { |
| 192 | + if (event.args_navigation.state) { |
| 193 | + switch (event.args_navigation.key) { |
| 194 | + case BSP_INPUT_NAVIGATION_KEY_ESC: |
| 195 | + case BSP_INPUT_NAVIGATION_KEY_F1: |
| 196 | + case BSP_INPUT_NAVIGATION_KEY_GAMEPAD_B: |
| 197 | + menu_free(&menu); |
| 198 | + return; |
| 199 | + case BSP_INPUT_NAVIGATION_KEY_UP: |
| 200 | + menu_navigate_previous(&menu); |
| 201 | + render(buffer, theme, &menu, true, false); |
| 202 | + break; |
| 203 | + case BSP_INPUT_NAVIGATION_KEY_DOWN: |
| 204 | + menu_navigate_next(&menu); |
| 205 | + render(buffer, theme, &menu, true, false); |
| 206 | + break; |
| 207 | + case BSP_INPUT_NAVIGATION_KEY_RETURN: |
| 208 | + case BSP_INPUT_NAVIGATION_KEY_GAMEPAD_A: |
| 209 | + case BSP_INPUT_NAVIGATION_KEY_JOYSTICK_PRESS: { |
| 210 | + void* arg = menu_get_callback_args(&menu, menu_get_position(&menu)); |
| 211 | + // execute_action(buffer, (menu_home_action_t)arg, theme); |
| 212 | + render(buffer, theme, &menu, false, true); |
| 213 | + break; |
| 214 | + } |
| 215 | + default: |
| 216 | + break; |
| 217 | + } |
| 218 | + } |
| 219 | + break; |
| 220 | + } |
| 221 | + default: |
| 222 | + break; |
| 223 | + } |
| 224 | + } else { |
| 225 | + render(buffer, theme, &menu, true, true); |
| 226 | + } |
| 227 | + } |
| 228 | + |
| 229 | + free_projects(); |
| 230 | + free_information(); |
| 231 | +} |
0 commit comments