#include #include #include #include #include "lvgl.h" #include "lcd/lcd.h" #include "esp_lvgl_port.h" #include "esp_log.h" #include "lcd/ui.h" #include "sd/sd.h" #define UI_LOG_MAX_LINES 41 #define UI_IMAGE_PATH_PREFIX "A:/sdcard/" #define UI_IMAGE_PATH_MAX_LENGTH 160 #define UI_IMAGE_INFO_BAR_HEIGHT 42 static lv_obj_t *log_textarea = NULL; static lv_obj_t *image_tileview = NULL; static size_t log_line_length = 0; static size_t image_browser_image_count = 0; static size_t image_browser_current_index = 0; static const char *TAG = "ui"; static ui_view_t current_view = UI_VIEW_LOG; static void ui_reset_active_screen_locked(void){ lv_obj_t *screen = lv_screen_active(); lv_obj_clean(screen); log_textarea = NULL; image_tileview = NULL; image_browser_image_count = 0; image_browser_current_index = 0; } static void ui_image_browser_set_index_locked(size_t image_index, lv_anim_enable_t anim_en){ if(image_tileview == NULL || image_browser_image_count == 0) return; image_browser_current_index = image_index % image_browser_image_count; lv_tileview_set_tile_by_index(image_tileview, 0, (uint32_t)image_browser_current_index, anim_en); } static void ui_image_browser_touch_event_cb(lv_event_t *event){ if(lv_event_get_code(event) != LV_EVENT_CLICKED) return; ui_image_browser_set_index_locked(image_browser_current_index + 1, LV_ANIM_ON); } static void ui_image_browser_value_changed_event_cb(lv_event_t *event){ if(lv_event_get_code(event) != LV_EVENT_VALUE_CHANGED) return; lv_obj_t *tileview = lv_event_get_current_target(event); lv_obj_t *active_tile = lv_tileview_get_tile_active(tileview); if(active_tile == NULL) return; image_browser_current_index = (size_t)(uintptr_t)lv_obj_get_user_data(active_tile); } static bool ui_char_equals_ignore_case(char left_char, char right_char){ if(left_char >= 'A' && left_char <= 'Z') left_char = (char)(left_char + ('a' - 'A')); if(right_char >= 'A' && right_char <= 'Z') right_char = (char)(right_char + ('a' - 'A')); return left_char == right_char; } static bool ui_has_extension(const char *filename, const char *extension){ if(filename == NULL || extension == NULL) return false; const size_t filename_length = strlen(filename); const size_t extension_length = strlen(extension); if(filename_length < extension_length) return false; const char *filename_extension = filename + filename_length - extension_length; for(size_t char_index = 0; char_index < extension_length; char_index++){ if(!ui_char_equals_ignore_case(filename_extension[char_index], extension[char_index])) return false; } return true; } static bool ui_is_image_file(const char *filename){ return ui_has_extension(filename, ".bmp") || ui_has_extension(filename, ".jpg") || ui_has_extension(filename, ".jpeg") || ui_has_extension(filename, ".png"); } static size_t ui_count_sd_images(void){ const size_t file_count = sd_get_file_count(); size_t image_count = 0; for(size_t file_index = 0; file_index < file_count; file_index++){ const char *filename = sd_get_next_file(); if(ui_is_image_file(filename)) image_count++; } return image_count; } static void ui_create_empty_image_browser_locked(lv_obj_t *screen){ lv_obj_t *label = lv_label_create(screen); lv_label_set_text(label, "No images found on SD card"); lv_obj_set_style_text_color(label, lv_color_hex(0xdde7ef), 0); lv_obj_center(label); } static lv_dir_t ui_get_tile_directions(size_t image_index, size_t image_count){ if(image_count <= 1) return LV_DIR_NONE; if(image_index == 0) return LV_DIR_BOTTOM; if(image_index + 1 == image_count) return LV_DIR_TOP; return (lv_dir_t)(LV_DIR_TOP | LV_DIR_BOTTOM); } static void ui_create_image_tile_locked(lv_obj_t *tile, const char *filename, size_t image_index, size_t image_count){ lv_obj_set_style_bg_color(tile, lv_color_hex(0x070a0d), 0); lv_obj_set_style_pad_all(tile, 0, 0); lv_obj_remove_flag(tile, LV_OBJ_FLAG_SCROLLABLE); lv_obj_t *image_frame = lv_obj_create(tile); lv_obj_set_size(image_frame, LV_PCT(100), LV_PCT(100)); lv_obj_set_style_bg_color(image_frame, lv_color_black(), 0); lv_obj_set_style_border_width(image_frame, 0, 0); lv_obj_set_style_radius(image_frame, 0, 0); lv_obj_set_style_pad_all(image_frame, 0, 0); lv_obj_remove_flag(image_frame, LV_OBJ_FLAG_SCROLLABLE); lv_obj_center(image_frame); lv_obj_t *image = lv_image_create(image_frame); lv_obj_set_size(image, LV_PCT(100), LV_PCT(100)); lv_image_set_inner_align(image, LV_IMAGE_ALIGN_CONTAIN); char image_source[UI_IMAGE_PATH_MAX_LENGTH]; snprintf(image_source, sizeof(image_source), "%s%s", UI_IMAGE_PATH_PREFIX, filename); lv_image_set_src(image, image_source); lv_obj_center(image); lv_obj_t *info_bar = lv_obj_create(tile); lv_obj_set_size(info_bar, LV_PCT(100), UI_IMAGE_INFO_BAR_HEIGHT); lv_obj_align(info_bar, LV_ALIGN_BOTTOM_MID, 0, 0); lv_obj_set_style_bg_color(info_bar, lv_color_hex(0x101418), 0); lv_obj_set_style_bg_opa(info_bar, LV_OPA_80, 0); lv_obj_set_style_border_width(info_bar, 0, 0); lv_obj_set_style_radius(info_bar, 0, 0); lv_obj_set_style_pad_hor(info_bar, 14, 0); lv_obj_set_style_pad_ver(info_bar, 0, 0); lv_obj_remove_flag(info_bar, LV_OBJ_FLAG_SCROLLABLE); lv_obj_t *caption = lv_label_create(info_bar); lv_label_set_long_mode(caption, LV_LABEL_LONG_DOT); lv_obj_set_width(caption, LV_PCT(100)); lv_obj_set_style_text_color(caption, lv_color_hex(0xdde7ef), 0); lv_label_set_text_fmt(caption, "%u/%u %s", (unsigned int)(image_index + 1), (unsigned int)image_count, filename); lv_obj_center(caption); if(lv_image_get_src_width(image) <= 0 || lv_image_get_src_height(image) <= 0){ lv_obj_t *error_label = lv_label_create(tile); lv_label_set_text(error_label, "Image cannot be loaded"); lv_obj_set_style_text_color(error_label, lv_color_hex(0xffd166), 0); lv_obj_align(error_label, LV_ALIGN_CENTER, 0, -18); } } static void ui_create_image_browser_tiles_locked(size_t image_count){ const size_t file_count = sd_get_file_count(); size_t image_index = 0; for(size_t file_index = 0; file_index < file_count && image_index < image_count; file_index++){ const char *filename = sd_get_next_file(); if(!ui_is_image_file(filename)) continue; lv_obj_t *tile = lv_tileview_add_tile(image_tileview, 0, (uint32_t)image_index, ui_get_tile_directions(image_index, image_count)); lv_obj_set_user_data(tile, (void *)(uintptr_t)image_index); lv_obj_add_flag(tile, LV_OBJ_FLAG_CLICKABLE); lv_obj_add_event_cb(tile, ui_image_browser_touch_event_cb, LV_EVENT_CLICKED, NULL); ui_create_image_tile_locked(tile, filename, image_index, image_count); image_index++; } } static void ui_log_delete_first_line(void){ lvgl_port_lock(0); size_t cursor_pos = lv_textarea_get_cursor_pos(log_textarea); lv_textarea_set_cursor_pos(log_textarea, 0); while(1){ const char *log_text = lv_textarea_get_text(log_textarea); if(log_text[0] == '\0') break; char first_char = log_text[0]; lv_textarea_delete_char_forward(log_textarea); if(first_char == '\n') break; } lv_textarea_set_cursor_pos(log_textarea, cursor_pos); lvgl_port_unlock(); } void ui_log_append(const char *text){ if(log_textarea == NULL || text == NULL) return; lvgl_port_lock(0); lv_textarea_add_text(log_textarea, text); lv_textarea_add_text(log_textarea, "\n"); lv_textarea_set_cursor_pos(log_textarea, LV_TEXTAREA_CURSOR_LAST); lvgl_port_unlock(); log_line_length++; while(log_line_length > UI_LOG_MAX_LINES){ ui_log_delete_first_line(); log_line_length--; } } void create_log_ui(void){ ESP_LOGI(TAG, "Creating log UI"); lvgl_port_lock(0); ui_reset_active_screen_locked(); lv_obj_t *screen = lv_screen_active(); lv_obj_set_style_bg_color(screen, lv_color_hex(0x101418), 0); lv_obj_t *title = lv_label_create(screen); lv_label_set_text(title, "SD Image Viewer"); lv_obj_set_style_text_color(title, lv_color_white(), 0); lv_obj_align(title, LV_ALIGN_TOP_MID, 0, 14); if(log_textarea == NULL){ log_textarea = lv_textarea_create(screen); lv_obj_set_size(log_textarea, 440, 730); lv_obj_align(log_textarea, LV_ALIGN_TOP_LEFT, 20, 40); lv_textarea_set_one_line(log_textarea, false); lv_textarea_set_cursor_click_pos(log_textarea, false); lv_textarea_set_text(log_textarea, ""); lv_obj_set_style_bg_color(log_textarea, lv_color_hex(0x182028), 0); lv_obj_set_style_text_color(log_textarea, lv_color_hex(0xdde7ef), 0); lv_obj_set_style_border_color(log_textarea, lv_color_hex(0x4b6478), 0); lv_obj_set_style_radius(log_textarea, 4, 0); lv_obj_set_style_pad_all(log_textarea, 12, 0); } lvgl_port_unlock(); ui_log_append("Boot UI ready"); current_view = UI_VIEW_LOG; ESP_LOGI(TAG, "Log UI created"); } void create_image_browser_ui(void){ ESP_LOGI(TAG, "Creating image browser UI"); const size_t image_count = ui_count_sd_images(); lvgl_port_lock(0); ui_reset_active_screen_locked(); lv_obj_t *screen = lv_screen_active(); lv_obj_set_style_bg_color(screen, lv_color_hex(0x070a0d), 0); if(image_count == 0) ui_create_empty_image_browser_locked(screen); else{ image_tileview = lv_tileview_create(screen); lv_obj_set_size(image_tileview, LV_PCT(100), LV_PCT(100)); lv_obj_set_style_bg_color(image_tileview, lv_color_hex(0x070a0d), 0); lv_obj_set_style_border_width(image_tileview, 0, 0); lv_obj_set_style_pad_all(image_tileview, 0, 0); lv_obj_set_scrollbar_mode(image_tileview, LV_SCROLLBAR_MODE_OFF); lv_obj_add_event_cb(image_tileview, ui_image_browser_value_changed_event_cb, LV_EVENT_VALUE_CHANGED, NULL); image_browser_image_count = image_count; ui_create_image_browser_tiles_locked(image_count); ui_image_browser_set_index_locked(0, LV_ANIM_OFF); } current_view = UI_VIEW_IMAGE_BROWSER; lvgl_port_unlock(); ESP_LOGI(TAG, "Image browser UI created with %u images", (unsigned int)image_count); } void ui_switch_view_to(ui_view_t view){ if(view == current_view) return; switch(view){ case UI_VIEW_LOG: create_log_ui(); break; case UI_VIEW_IMAGE_BROWSER: create_image_browser_ui(); break; default: ESP_LOGW(TAG, "Unknown UI view: %d", view); break; } }