+ rearranged files

+ improved ui log
+ added image browser
- image loading fails at the moment
- arrangement of ui in image browser not checked
This commit is contained in:
Timo Niemann
2026-07-01 13:03:58 +02:00
parent 229d828271
commit 4e8282a64b
10 changed files with 358 additions and 80 deletions
+294
View File
@@ -0,0 +1,294 @@
#include <stdbool.h>
#include <stdio.h>
#include <stdint.h>
#include <string.h>
#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;
}
}