+ added helper scripts
+ moved uis into own files + log is not rerendered in full anymore
This commit is contained in:
@@ -0,0 +1,48 @@
|
|||||||
|
# Helper
|
||||||
|
|
||||||
|
## Introduction
|
||||||
|
|
||||||
|
This subdirectory delivers simple helper programs.
|
||||||
|
|
||||||
|
## progressive_to_normal_jpegs.sh
|
||||||
|
|
||||||
|
Converts progressive jpegs into non progressive ones.
|
||||||
|
|
||||||
|
### Requirements:
|
||||||
|
Ensure jpegtran is installed.
|
||||||
|
- Linux
|
||||||
|
- Arch:
|
||||||
|
>sudo pacman -S libjpeg-turbo
|
||||||
|
- Debian:
|
||||||
|
>sudo apt install libjpeg-turbo-progs
|
||||||
|
- macOS:
|
||||||
|
>brew install jpeg
|
||||||
|
|
||||||
|
### Usage:
|
||||||
|
./progressive_to_normal_jpegs.sh /path/to/images
|
||||||
|
|
||||||
|
## resize.py
|
||||||
|
|
||||||
|
Handy tool to resize images to displays size (480x800).
|
||||||
|
|
||||||
|
### Disclaimer:
|
||||||
|
|
||||||
|
This tool takes 1 command line argument, the directory where the files-to-be-converted are stored.
|
||||||
|
|
||||||
|
### Requirements:
|
||||||
|
|
||||||
|
Virtual Environment (optional)
|
||||||
|
- create a venv:
|
||||||
|
>python -m venv .env
|
||||||
|
- activate venv
|
||||||
|
- Linux/macOS:
|
||||||
|
>source .env/bin/activate
|
||||||
|
- Windows
|
||||||
|
>./.env/Scripts/activate.bat
|
||||||
|
|
||||||
|
Python-Pillow
|
||||||
|
>pip install pillow
|
||||||
|
|
||||||
|
### Usage:
|
||||||
|
|
||||||
|
>python resize.py /path/to/images
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
DIR="$1"
|
||||||
|
|
||||||
|
if [ -z "$DIR" ]; then
|
||||||
|
echo "Usage: $0 /path/to/images"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -d "$DIR" ]; then
|
||||||
|
echo "Error: Directory does not exist: $DIR"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
for i in "$DIR"/*.jpeg "$DIR"/*.jpg; do
|
||||||
|
[ -e "$i" ] || continue
|
||||||
|
jpegtran -copy none -optimize -outfile "${i%.*}_out.${i##*.}" "$i"
|
||||||
|
done
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
from PIL import Image
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
TARGET = (480, 800)
|
||||||
|
INPUT_DIR = sys.argv[1] if len(sys.argv) > 1 else "."
|
||||||
|
EXTENSIONS = {".jpg", ".jpeg", ".png", ".webp", ".bmp"}
|
||||||
|
|
||||||
|
for filename in os.listdir(INPUT_DIR):
|
||||||
|
ext = os.path.splitext(filename)[1].lower()
|
||||||
|
if ext not in EXTENSIONS:
|
||||||
|
continue
|
||||||
|
|
||||||
|
path = os.path.join(INPUT_DIR, filename)
|
||||||
|
img = Image.open(path)
|
||||||
|
|
||||||
|
img.thumbnail(TARGET, Image.LANCZOS)
|
||||||
|
|
||||||
|
img.save(path)
|
||||||
|
print(f"{filename}: {img.size}")
|
||||||
+1
-1
@@ -1,3 +1,3 @@
|
|||||||
idf_component_register(SRCS main.c lcd/lcd.c sd/sd.c lcd/ui.c
|
idf_component_register(SRCS main.c lcd/lcd.c sd/sd.c lcd/ui.c lcd/ui_log.c lcd/ui_image_browser.c
|
||||||
PRIV_REQUIRES spi_flash esp_psram esp_lcd esp_driver_gpio esp_driver_i2c esp_driver_sdspi esp_driver_spi esp_lvgl_port esp_lcd_touch_gt911 fatfs sdmmc
|
PRIV_REQUIRES spi_flash esp_psram esp_lcd esp_driver_gpio esp_driver_i2c esp_driver_sdspi esp_driver_spi esp_lvgl_port esp_lcd_touch_gt911 fatfs sdmmc
|
||||||
INCLUDE_DIRS "." "include")
|
INCLUDE_DIRS "." "include")
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include "lcd/ui_log.h"
|
||||||
|
|
||||||
typedef enum {
|
typedef enum {
|
||||||
UI_VIEW_LOG,
|
UI_VIEW_LOG,
|
||||||
UI_VIEW_IMAGE_BROWSER,
|
UI_VIEW_IMAGE_BROWSER,
|
||||||
} ui_view_t;
|
} ui_view_t;
|
||||||
|
|
||||||
void ui_log_append(const char *text);
|
|
||||||
void ui_log_appendf(const char *format, ...);
|
|
||||||
void create_log_ui(void);
|
void create_log_ui(void);
|
||||||
void create_image_browser_ui(void);
|
void create_image_browser_ui(void);
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <stddef.h>
|
||||||
|
|
||||||
|
#include "lvgl.h"
|
||||||
|
|
||||||
|
// Builds the image-browser view (tileview with tiles, or the empty-state
|
||||||
|
// placeholder) onto the given screen. The caller must hold the LVGL lock.
|
||||||
|
void ui_image_browser_build_view(lv_obj_t *screen);
|
||||||
|
|
||||||
|
// Stops the slideshow and clears browser state; call before the active screen
|
||||||
|
// is cleared.
|
||||||
|
void ui_image_browser_reset(void);
|
||||||
|
|
||||||
|
// Number of images shown by the most recent build (used for logging).
|
||||||
|
size_t ui_image_browser_get_image_count(void);
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "lvgl.h"
|
||||||
|
#include "lcd/ui.h"
|
||||||
|
|
||||||
|
// Shared coordinator helpers used by the log and image-browser view modules.
|
||||||
|
|
||||||
|
// Makes an object toggle between the log and image-browser views when clicked.
|
||||||
|
void ui_attach_toggle(lv_obj_t *object);
|
||||||
|
|
||||||
|
// Returns the currently active view.
|
||||||
|
ui_view_t ui_current_view(void);
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "lvgl.h"
|
||||||
|
|
||||||
|
// Appends a line to the scrollback log. Safe to call before the log view
|
||||||
|
// exists and from any task; the line is always stored in the ring buffer.
|
||||||
|
void ui_log_append(const char *text);
|
||||||
|
void ui_log_appendf(const char *format, ...);
|
||||||
|
|
||||||
|
// Builds the log view (title + textarea) onto the given screen and restores
|
||||||
|
// the ring-buffer history. The caller must hold the LVGL lock.
|
||||||
|
void ui_log_build_view(lv_obj_t *screen);
|
||||||
|
|
||||||
|
// Drops the textarea reference; call before the active screen is cleared.
|
||||||
|
void ui_log_detach(void);
|
||||||
+32
-404
@@ -1,404 +1,58 @@
|
|||||||
#include <errno.h>
|
|
||||||
#include <stdbool.h>
|
|
||||||
#include <stdio.h>
|
|
||||||
#include <stdint.h>
|
|
||||||
#include <stdarg.h>
|
|
||||||
#include <string.h>
|
|
||||||
|
|
||||||
#include "lvgl.h"
|
#include "lvgl.h"
|
||||||
#include "lcd/lcd.h"
|
|
||||||
#include "esp_lvgl_port.h"
|
#include "esp_lvgl_port.h"
|
||||||
#include "esp_log.h"
|
#include "esp_log.h"
|
||||||
|
|
||||||
#include "lcd/ui.h"
|
#include "lcd/ui.h"
|
||||||
#include "sd/sd.h"
|
#include "lcd/ui_internal.h"
|
||||||
|
#include "lcd/ui_log.h"
|
||||||
|
#include "lcd/ui_image_browser.h"
|
||||||
|
|
||||||
#define UI_LOG_MAX_LINES 41
|
|
||||||
#define UI_LOG_LINE_MAX_LENGTH 160
|
|
||||||
#define UI_IMAGE_PATH_PREFIX "A:/sdcard/"
|
|
||||||
#define UI_IMAGE_PATH_MAX_LENGTH 160
|
|
||||||
#define UI_IMAGE_INFO_BAR_HEIGHT 42
|
|
||||||
#define UI_IMAGE_INFO_BAR_BOTTOM_OFFSET 10
|
|
||||||
#define UI_IMAGE_PROBE_HEADER_LENGTH 16
|
|
||||||
#define UI_SLIDESHOW_PERIOD_MS 3000
|
|
||||||
|
|
||||||
static lv_obj_t *log_textarea = NULL;
|
|
||||||
static lv_obj_t *image_tileview = NULL;
|
|
||||||
static lv_timer_t *image_slideshow_timer = NULL;
|
|
||||||
static char log_lines[UI_LOG_MAX_LINES][UI_LOG_LINE_MAX_LENGTH];
|
|
||||||
static size_t log_line_length = 0;
|
|
||||||
static size_t log_line_start = 0;
|
|
||||||
static size_t image_browser_image_count = 0;
|
|
||||||
static size_t image_browser_current_index = 0;
|
|
||||||
static const char *TAG = "ui";
|
static const char *TAG = "ui";
|
||||||
|
|
||||||
static ui_view_t current_view = UI_VIEW_LOG;
|
static ui_view_t current_view = UI_VIEW_LOG;
|
||||||
|
|
||||||
static void ui_create_log_ui_locked(void);
|
static void ui_build_log_view_locked(void);
|
||||||
static void ui_create_image_browser_ui_locked(void);
|
static void ui_build_image_browser_view_locked(void);
|
||||||
|
|
||||||
static size_t ui_log_buffer_index(size_t line_offset){
|
ui_view_t ui_current_view(void){
|
||||||
return (log_line_start + line_offset) % UI_LOG_MAX_LINES;
|
return current_view;
|
||||||
}
|
|
||||||
|
|
||||||
static void ui_log_store_line(const char *text){
|
|
||||||
if(text == NULL) return;
|
|
||||||
|
|
||||||
size_t line_index;
|
|
||||||
if(log_line_length < UI_LOG_MAX_LINES){
|
|
||||||
line_index = ui_log_buffer_index(log_line_length);
|
|
||||||
log_line_length++;
|
|
||||||
}
|
|
||||||
else{
|
|
||||||
line_index = log_line_start;
|
|
||||||
log_line_start = ui_log_buffer_index(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
snprintf(log_lines[line_index], sizeof(log_lines[line_index]), "%s", text);
|
|
||||||
}
|
|
||||||
|
|
||||||
static void ui_log_render_locked(void){
|
|
||||||
if(log_textarea == NULL) return;
|
|
||||||
|
|
||||||
lv_textarea_set_text(log_textarea, "");
|
|
||||||
for(size_t line_offset = 0; line_offset < log_line_length; line_offset++){
|
|
||||||
lv_textarea_add_text(log_textarea, log_lines[ui_log_buffer_index(line_offset)]);
|
|
||||||
lv_textarea_add_text(log_textarea, "\n");
|
|
||||||
}
|
|
||||||
lv_textarea_set_cursor_pos(log_textarea, LV_TEXTAREA_CURSOR_LAST);
|
|
||||||
}
|
|
||||||
|
|
||||||
static void ui_reset_active_screen_locked(void){
|
|
||||||
if(image_slideshow_timer != NULL){
|
|
||||||
lv_timer_delete(image_slideshow_timer);
|
|
||||||
image_slideshow_timer = NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
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_slideshow_timer_cb(lv_timer_t *timer){
|
|
||||||
(void)timer;
|
|
||||||
if(current_view != UI_VIEW_IMAGE_BROWSER) return;
|
|
||||||
ui_image_browser_set_index_locked(image_browser_current_index + 1, LV_ANIM_ON);
|
|
||||||
}
|
|
||||||
|
|
||||||
static void ui_start_image_slideshow_locked(void){
|
|
||||||
if(image_slideshow_timer != NULL || image_browser_image_count <= 1) return;
|
|
||||||
image_slideshow_timer = lv_timer_create(ui_image_slideshow_timer_cb, UI_SLIDESHOW_PERIOD_MS, NULL);
|
|
||||||
}
|
|
||||||
|
|
||||||
static void ui_toggle_view_locked(void){
|
|
||||||
if(current_view == UI_VIEW_IMAGE_BROWSER) ui_create_log_ui_locked();
|
|
||||||
else ui_create_image_browser_ui_locked();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static void ui_toggle_view_event_cb(lv_event_t *event){
|
static void ui_toggle_view_event_cb(lv_event_t *event){
|
||||||
if(lv_event_get_code(event) != LV_EVENT_CLICKED) return;
|
if(lv_event_get_code(event) != LV_EVENT_CLICKED) return;
|
||||||
ui_toggle_view_locked();
|
|
||||||
|
if(current_view == UI_VIEW_IMAGE_BROWSER) ui_build_log_view_locked();
|
||||||
|
else ui_build_image_browser_view_locked();
|
||||||
}
|
}
|
||||||
|
|
||||||
static void ui_add_toggle_click(lv_obj_t *object){
|
void ui_attach_toggle(lv_obj_t *object){
|
||||||
lv_obj_add_flag(object, LV_OBJ_FLAG_CLICKABLE);
|
lv_obj_add_flag(object, LV_OBJ_FLAG_CLICKABLE);
|
||||||
lv_obj_add_event_cb(object, ui_toggle_view_event_cb, LV_EVENT_CLICKED, NULL);
|
lv_obj_add_event_cb(object, ui_toggle_view_event_cb, LV_EVENT_CLICKED, NULL);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void ui_image_browser_value_changed_event_cb(lv_event_t *event){
|
static void ui_reset_active_screen_locked(void){
|
||||||
if(lv_event_get_code(event) != LV_EVENT_VALUE_CHANGED) return;
|
ui_image_browser_reset();
|
||||||
|
ui_log_detach();
|
||||||
lv_obj_t *tileview = lv_event_get_current_target(event);
|
lv_obj_clean(lv_screen_active());
|
||||||
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){
|
static void ui_build_log_view_locked(void){
|
||||||
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 void ui_lowercase_path_extension(char *path){
|
|
||||||
char *extension = strrchr(path, '.');
|
|
||||||
if(extension == NULL) return;
|
|
||||||
|
|
||||||
for(char *current_char = extension + 1; *current_char != '\0'; current_char++){
|
|
||||||
if(*current_char >= 'A' && *current_char <= 'Z') *current_char = (char)(*current_char + ('a' - 'A'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 const char *ui_detect_image_signature(const uint8_t *header, size_t header_length){
|
|
||||||
if(header_length >= 2 && header[0] == 0xff && header[1] == 0xd8) return "jpeg";
|
|
||||||
if(header_length >= 8 && header[0] == 0x89 && header[1] == 'P' && header[2] == 'N' && header[3] == 'G') return "png";
|
|
||||||
if(header_length >= 2 && header[0] == 'B' && header[1] == 'M') return "bmp";
|
|
||||||
return "unknown";
|
|
||||||
}
|
|
||||||
|
|
||||||
static long ui_get_open_file_size(FILE *file){
|
|
||||||
if(file == NULL) return -1;
|
|
||||||
|
|
||||||
const long current_position = ftell(file);
|
|
||||||
if(current_position < 0) return -1;
|
|
||||||
if(fseek(file, 0, SEEK_END) != 0) return -1;
|
|
||||||
|
|
||||||
const long file_size = ftell(file);
|
|
||||||
fseek(file, current_position, SEEK_SET);
|
|
||||||
return file_size;
|
|
||||||
}
|
|
||||||
|
|
||||||
static const char *ui_probe_image_load_failure(const char *filename, const char *image_source){
|
|
||||||
if(filename == NULL || image_source == NULL) return "Invalid image path";
|
|
||||||
|
|
||||||
errno = 0;
|
|
||||||
FILE *file = sd_open_read(filename);
|
|
||||||
if(file == NULL){
|
|
||||||
const int open_errno = errno;
|
|
||||||
ESP_LOGW(TAG, "Image file not readable via sd_open_read: file=%s source=%s errno=%d", filename, image_source, open_errno);
|
|
||||||
ui_log_appendf("Image read failed: %s errno=%d", filename, open_errno);
|
|
||||||
return "File not readable";
|
|
||||||
}
|
|
||||||
|
|
||||||
uint8_t header_bytes[UI_IMAGE_PROBE_HEADER_LENGTH] = {0};
|
|
||||||
const size_t header_length = fread(header_bytes, 1, sizeof(header_bytes), file);
|
|
||||||
const long file_size = ui_get_open_file_size(file);
|
|
||||||
sd_close_file(file);
|
|
||||||
|
|
||||||
const char *detected_format = ui_detect_image_signature(header_bytes, header_length);
|
|
||||||
ESP_LOGW(TAG,
|
|
||||||
"Image probe: file=%s source=%s size=%ld header=%02x %02x %02x %02x %02x %02x %02x %02x detected=%s",
|
|
||||||
filename,
|
|
||||||
image_source,
|
|
||||||
file_size,
|
|
||||||
(unsigned int)header_bytes[0],
|
|
||||||
(unsigned int)header_bytes[1],
|
|
||||||
(unsigned int)header_bytes[2],
|
|
||||||
(unsigned int)header_bytes[3],
|
|
||||||
(unsigned int)header_bytes[4],
|
|
||||||
(unsigned int)header_bytes[5],
|
|
||||||
(unsigned int)header_bytes[6],
|
|
||||||
(unsigned int)header_bytes[7],
|
|
||||||
detected_format);
|
|
||||||
ui_log_appendf("Image probe: %s size=%ld detected=%s", filename, file_size, detected_format);
|
|
||||||
|
|
||||||
lv_image_header_t image_header;
|
|
||||||
const lv_result_t decoder_result = lv_image_decoder_get_info(image_source, &image_header);
|
|
||||||
if(decoder_result != LV_RESULT_OK){
|
|
||||||
ESP_LOGW(TAG, "LVGL decoder rejected image: source=%s result=%d", image_source, (int)decoder_result);
|
|
||||||
ui_log_appendf("Decoder rejected: %s result=%d", filename, (int)decoder_result);
|
|
||||||
return "Decoder rejected image";
|
|
||||||
}
|
|
||||||
|
|
||||||
ESP_LOGW(TAG,
|
|
||||||
"LVGL decoder accepted image but widget has no size: source=%s width=%u height=%u cf=%u stride=%u flags=0x%x",
|
|
||||||
image_source,
|
|
||||||
(unsigned int)image_header.w,
|
|
||||||
(unsigned int)image_header.h,
|
|
||||||
(unsigned int)image_header.cf,
|
|
||||||
(unsigned int)image_header.stride,
|
|
||||||
(unsigned int)image_header.flags);
|
|
||||||
ui_log_appendf("Decoder info OK: %s %ux%u", filename, (unsigned int)image_header.w, (unsigned int)image_header.h);
|
|
||||||
return "Decoder info OK";
|
|
||||||
}
|
|
||||||
|
|
||||||
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){
|
|
||||||
ui_add_toggle_click(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);
|
|
||||||
ui_add_toggle_click(label);
|
|
||||||
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){
|
|
||||||
ui_add_toggle_click(tile);
|
|
||||||
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);
|
|
||||||
ui_add_toggle_click(image_frame);
|
|
||||||
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);
|
|
||||||
ui_add_toggle_click(image);
|
|
||||||
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);
|
|
||||||
ui_lowercase_path_extension(image_source);
|
|
||||||
lv_image_set_src(image, image_source);
|
|
||||||
lv_obj_center(image);
|
|
||||||
|
|
||||||
lv_obj_t *info_bar = lv_obj_create(tile);
|
|
||||||
ui_add_toggle_click(info_bar);
|
|
||||||
lv_obj_set_size(info_bar, LV_PCT(100), UI_IMAGE_INFO_BAR_HEIGHT);
|
|
||||||
lv_obj_align(info_bar, LV_ALIGN_BOTTOM_MID, 0, -UI_IMAGE_INFO_BAR_BOTTOM_OFFSET);
|
|
||||||
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);
|
|
||||||
ui_add_toggle_click(caption);
|
|
||||||
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){
|
|
||||||
const char *load_reason = ui_probe_image_load_failure(filename, image_source);
|
|
||||||
ESP_LOGW(TAG, "Image cannot be loaded by LVGL: %s reason=%s", image_source, load_reason);
|
|
||||||
ui_log_appendf("Image load failed: %s (%s)", filename, load_reason);
|
|
||||||
lv_obj_t *error_label = lv_label_create(tile);
|
|
||||||
ui_add_toggle_click(error_label);
|
|
||||||
lv_label_set_text_fmt(error_label, "Image cannot be loaded\n%s\n%s", filename, load_reason);
|
|
||||||
lv_obj_set_style_text_color(error_label, lv_color_hex(0xffd166), 0);
|
|
||||||
lv_obj_set_style_text_align(error_label, LV_TEXT_ALIGN_CENTER, 0);
|
|
||||||
lv_obj_set_width(error_label, LV_PCT(86));
|
|
||||||
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);
|
|
||||||
ui_create_image_tile_locked(tile, filename, image_index, image_count);
|
|
||||||
image_index++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void ui_log_append(const char *text){
|
|
||||||
if(text == NULL) return;
|
|
||||||
|
|
||||||
ui_log_store_line(text);
|
|
||||||
if(log_textarea == NULL) return;
|
|
||||||
|
|
||||||
lvgl_port_lock(0);
|
|
||||||
ui_log_render_locked();
|
|
||||||
lvgl_port_unlock();
|
|
||||||
}
|
|
||||||
|
|
||||||
void ui_log_appendf(const char *format, ...){
|
|
||||||
if(format == NULL) return;
|
|
||||||
|
|
||||||
char line[UI_LOG_LINE_MAX_LENGTH];
|
|
||||||
va_list args;
|
|
||||||
va_start(args, format);
|
|
||||||
vsnprintf(line, sizeof(line), format, args);
|
|
||||||
va_end(args);
|
|
||||||
|
|
||||||
ui_log_append(line);
|
|
||||||
}
|
|
||||||
|
|
||||||
static void ui_create_log_ui_locked(void){
|
|
||||||
ui_reset_active_screen_locked();
|
ui_reset_active_screen_locked();
|
||||||
|
ui_log_build_view(lv_screen_active());
|
||||||
lv_obj_t *screen = lv_screen_active();
|
|
||||||
ui_add_toggle_click(screen);
|
|
||||||
lv_obj_set_style_bg_color(screen, lv_color_hex(0x101418), 0);
|
|
||||||
|
|
||||||
lv_obj_t *title = lv_label_create(screen);
|
|
||||||
ui_add_toggle_click(title);
|
|
||||||
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);
|
|
||||||
ui_add_toggle_click(log_textarea);
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
ui_log_render_locked();
|
|
||||||
|
|
||||||
current_view = UI_VIEW_LOG;
|
current_view = UI_VIEW_LOG;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void ui_build_image_browser_view_locked(void){
|
||||||
|
ui_reset_active_screen_locked();
|
||||||
|
ui_image_browser_build_view(lv_screen_active());
|
||||||
|
current_view = UI_VIEW_IMAGE_BROWSER;
|
||||||
|
}
|
||||||
|
|
||||||
void create_log_ui(void){
|
void create_log_ui(void){
|
||||||
ESP_LOGI(TAG, "Creating log UI");
|
ESP_LOGI(TAG, "Creating log UI");
|
||||||
ui_log_append("Creating log UI");
|
ui_log_append("Creating log UI");
|
||||||
|
|
||||||
lvgl_port_lock(0);
|
lvgl_port_lock(0);
|
||||||
ui_create_log_ui_locked();
|
ui_build_log_view_locked();
|
||||||
lvgl_port_unlock();
|
lvgl_port_unlock();
|
||||||
|
|
||||||
ui_log_append("Boot UI ready");
|
ui_log_append("Boot UI ready");
|
||||||
@@ -406,43 +60,17 @@ void create_log_ui(void){
|
|||||||
ui_log_append("Log UI created");
|
ui_log_append("Log UI created");
|
||||||
}
|
}
|
||||||
|
|
||||||
static void ui_create_image_browser_ui_locked(void){
|
|
||||||
const size_t image_count = ui_count_sd_images();
|
|
||||||
ui_reset_active_screen_locked();
|
|
||||||
|
|
||||||
lv_obj_t *screen = lv_screen_active();
|
|
||||||
ui_add_toggle_click(screen);
|
|
||||||
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);
|
|
||||||
ui_start_image_slideshow_locked();
|
|
||||||
}
|
|
||||||
|
|
||||||
current_view = UI_VIEW_IMAGE_BROWSER;
|
|
||||||
}
|
|
||||||
|
|
||||||
void create_image_browser_ui(void){
|
void create_image_browser_ui(void){
|
||||||
ESP_LOGI(TAG, "Creating image browser UI");
|
ESP_LOGI(TAG, "Creating image browser UI");
|
||||||
ui_log_append("Creating image browser UI");
|
ui_log_append("Creating image browser UI");
|
||||||
|
|
||||||
lvgl_port_lock(0);
|
lvgl_port_lock(0);
|
||||||
ui_create_image_browser_ui_locked();
|
ui_build_image_browser_view_locked();
|
||||||
lvgl_port_unlock();
|
lvgl_port_unlock();
|
||||||
|
|
||||||
ESP_LOGI(TAG, "Image browser UI created with %u images", (unsigned int)image_browser_image_count);
|
const unsigned int image_count = (unsigned int)ui_image_browser_get_image_count();
|
||||||
ui_log_appendf("Image browser UI created with %u images", (unsigned int)image_browser_image_count);
|
ESP_LOGI(TAG, "Image browser UI created with %u images", image_count);
|
||||||
|
ui_log_appendf("Image browser UI created with %u images", image_count);
|
||||||
}
|
}
|
||||||
|
|
||||||
void ui_switch_view_to(ui_view_t view){
|
void ui_switch_view_to(ui_view_t view){
|
||||||
@@ -451,12 +79,12 @@ void ui_switch_view_to(ui_view_t view){
|
|||||||
switch(view){
|
switch(view){
|
||||||
case UI_VIEW_LOG:
|
case UI_VIEW_LOG:
|
||||||
lvgl_port_lock(0);
|
lvgl_port_lock(0);
|
||||||
ui_create_log_ui_locked();
|
ui_build_log_view_locked();
|
||||||
lvgl_port_unlock();
|
lvgl_port_unlock();
|
||||||
break;
|
break;
|
||||||
case UI_VIEW_IMAGE_BROWSER:
|
case UI_VIEW_IMAGE_BROWSER:
|
||||||
lvgl_port_lock(0);
|
lvgl_port_lock(0);
|
||||||
ui_create_image_browser_ui_locked();
|
ui_build_image_browser_view_locked();
|
||||||
lvgl_port_unlock();
|
lvgl_port_unlock();
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
|
|||||||
@@ -0,0 +1,309 @@
|
|||||||
|
#include <errno.h>
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <stddef.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
#include "lvgl.h"
|
||||||
|
#include "esp_log.h"
|
||||||
|
|
||||||
|
#include "lcd/ui_image_browser.h"
|
||||||
|
#include "lcd/ui_internal.h"
|
||||||
|
#include "lcd/ui_log.h"
|
||||||
|
#include "sd/sd.h"
|
||||||
|
|
||||||
|
#define UI_IMAGE_PATH_PREFIX "A:/sdcard/"
|
||||||
|
#define UI_IMAGE_PATH_MAX_LENGTH 160
|
||||||
|
#define UI_IMAGE_INFO_BAR_HEIGHT 42
|
||||||
|
#define UI_IMAGE_INFO_BAR_BOTTOM_OFFSET 10
|
||||||
|
#define UI_IMAGE_PROBE_HEADER_LENGTH 16
|
||||||
|
#define UI_SLIDESHOW_PERIOD_MS 3000
|
||||||
|
|
||||||
|
static lv_obj_t *image_tileview = NULL;
|
||||||
|
static lv_timer_t *image_slideshow_timer = NULL;
|
||||||
|
static size_t image_browser_image_count = 0;
|
||||||
|
static size_t image_browser_current_index = 0;
|
||||||
|
static const char *TAG = "ui";
|
||||||
|
|
||||||
|
static void ui_image_browser_set_index(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_slideshow_timer_cb(lv_timer_t *timer){
|
||||||
|
(void)timer;
|
||||||
|
if(ui_current_view() != UI_VIEW_IMAGE_BROWSER) return;
|
||||||
|
ui_image_browser_set_index(image_browser_current_index + 1, LV_ANIM_ON);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void ui_start_image_slideshow(void){
|
||||||
|
if(image_slideshow_timer != NULL || image_browser_image_count <= 1) return;
|
||||||
|
image_slideshow_timer = lv_timer_create(ui_image_slideshow_timer_cb, UI_SLIDESHOW_PERIOD_MS, NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 void ui_lowercase_path_extension(char *path){
|
||||||
|
char *extension = strrchr(path, '.');
|
||||||
|
if(extension == NULL) return;
|
||||||
|
|
||||||
|
for(char *current_char = extension + 1; *current_char != '\0'; current_char++){
|
||||||
|
if(*current_char >= 'A' && *current_char <= 'Z') *current_char = (char)(*current_char + ('a' - 'A'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 const char *ui_detect_image_signature(const uint8_t *header, size_t header_length){
|
||||||
|
if(header_length >= 2 && header[0] == 0xff && header[1] == 0xd8) return "jpeg";
|
||||||
|
if(header_length >= 8 && header[0] == 0x89 && header[1] == 'P' && header[2] == 'N' && header[3] == 'G') return "png";
|
||||||
|
if(header_length >= 2 && header[0] == 'B' && header[1] == 'M') return "bmp";
|
||||||
|
return "unknown";
|
||||||
|
}
|
||||||
|
|
||||||
|
static long ui_get_open_file_size(FILE *file){
|
||||||
|
if(file == NULL) return -1;
|
||||||
|
|
||||||
|
const long current_position = ftell(file);
|
||||||
|
if(current_position < 0) return -1;
|
||||||
|
if(fseek(file, 0, SEEK_END) != 0) return -1;
|
||||||
|
|
||||||
|
const long file_size = ftell(file);
|
||||||
|
fseek(file, current_position, SEEK_SET);
|
||||||
|
return file_size;
|
||||||
|
}
|
||||||
|
|
||||||
|
static const char *ui_probe_image_load_failure(const char *filename, const char *image_source){
|
||||||
|
if(filename == NULL || image_source == NULL) return "Invalid image path";
|
||||||
|
|
||||||
|
errno = 0;
|
||||||
|
FILE *file = sd_open_read(filename);
|
||||||
|
if(file == NULL){
|
||||||
|
const int open_errno = errno;
|
||||||
|
ESP_LOGW(TAG, "Image file not readable via sd_open_read: file=%s source=%s errno=%d", filename, image_source, open_errno);
|
||||||
|
ui_log_appendf("Image read failed: %s errno=%d", filename, open_errno);
|
||||||
|
return "File not readable";
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t header_bytes[UI_IMAGE_PROBE_HEADER_LENGTH] = {0};
|
||||||
|
const size_t header_length = fread(header_bytes, 1, sizeof(header_bytes), file);
|
||||||
|
const long file_size = ui_get_open_file_size(file);
|
||||||
|
sd_close_file(file);
|
||||||
|
|
||||||
|
const char *detected_format = ui_detect_image_signature(header_bytes, header_length);
|
||||||
|
ESP_LOGW(TAG,
|
||||||
|
"Image probe: file=%s source=%s size=%ld header=%02x %02x %02x %02x %02x %02x %02x %02x detected=%s",
|
||||||
|
filename,
|
||||||
|
image_source,
|
||||||
|
file_size,
|
||||||
|
(unsigned int)header_bytes[0],
|
||||||
|
(unsigned int)header_bytes[1],
|
||||||
|
(unsigned int)header_bytes[2],
|
||||||
|
(unsigned int)header_bytes[3],
|
||||||
|
(unsigned int)header_bytes[4],
|
||||||
|
(unsigned int)header_bytes[5],
|
||||||
|
(unsigned int)header_bytes[6],
|
||||||
|
(unsigned int)header_bytes[7],
|
||||||
|
detected_format);
|
||||||
|
ui_log_appendf("Image probe: %s size=%ld detected=%s", filename, file_size, detected_format);
|
||||||
|
|
||||||
|
lv_image_header_t image_header;
|
||||||
|
const lv_result_t decoder_result = lv_image_decoder_get_info(image_source, &image_header);
|
||||||
|
if(decoder_result != LV_RESULT_OK){
|
||||||
|
ESP_LOGW(TAG, "LVGL decoder rejected image: source=%s result=%d", image_source, (int)decoder_result);
|
||||||
|
ui_log_appendf("Decoder rejected: %s result=%d", filename, (int)decoder_result);
|
||||||
|
return "Decoder rejected image";
|
||||||
|
}
|
||||||
|
|
||||||
|
ESP_LOGW(TAG,
|
||||||
|
"LVGL decoder accepted image but widget has no size: source=%s width=%u height=%u cf=%u stride=%u flags=0x%x",
|
||||||
|
image_source,
|
||||||
|
(unsigned int)image_header.w,
|
||||||
|
(unsigned int)image_header.h,
|
||||||
|
(unsigned int)image_header.cf,
|
||||||
|
(unsigned int)image_header.stride,
|
||||||
|
(unsigned int)image_header.flags);
|
||||||
|
ui_log_appendf("Decoder info OK: %s %ux%u", filename, (unsigned int)image_header.w, (unsigned int)image_header.h);
|
||||||
|
return "Decoder info OK";
|
||||||
|
}
|
||||||
|
|
||||||
|
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(lv_obj_t *screen){
|
||||||
|
lv_obj_t *label = lv_label_create(screen);
|
||||||
|
ui_attach_toggle(label);
|
||||||
|
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(lv_obj_t *tile, const char *filename, size_t image_index, size_t image_count){
|
||||||
|
ui_attach_toggle(tile);
|
||||||
|
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);
|
||||||
|
ui_attach_toggle(image_frame);
|
||||||
|
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);
|
||||||
|
ui_attach_toggle(image);
|
||||||
|
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);
|
||||||
|
ui_lowercase_path_extension(image_source);
|
||||||
|
lv_image_set_src(image, image_source);
|
||||||
|
lv_obj_center(image);
|
||||||
|
|
||||||
|
lv_obj_t *info_bar = lv_obj_create(tile);
|
||||||
|
ui_attach_toggle(info_bar);
|
||||||
|
lv_obj_set_size(info_bar, LV_PCT(100), UI_IMAGE_INFO_BAR_HEIGHT);
|
||||||
|
lv_obj_align(info_bar, LV_ALIGN_BOTTOM_MID, 0, -UI_IMAGE_INFO_BAR_BOTTOM_OFFSET);
|
||||||
|
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);
|
||||||
|
ui_attach_toggle(caption);
|
||||||
|
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){
|
||||||
|
const char *load_reason = ui_probe_image_load_failure(filename, image_source);
|
||||||
|
ESP_LOGW(TAG, "Image cannot be loaded by LVGL: %s reason=%s", image_source, load_reason);
|
||||||
|
ui_log_appendf("Image load failed: %s (%s)", filename, load_reason);
|
||||||
|
lv_obj_t *error_label = lv_label_create(tile);
|
||||||
|
ui_attach_toggle(error_label);
|
||||||
|
lv_label_set_text_fmt(error_label, "Image cannot be loaded\n%s\n%s", filename, load_reason);
|
||||||
|
lv_obj_set_style_text_color(error_label, lv_color_hex(0xffd166), 0);
|
||||||
|
lv_obj_set_style_text_align(error_label, LV_TEXT_ALIGN_CENTER, 0);
|
||||||
|
lv_obj_set_width(error_label, LV_PCT(86));
|
||||||
|
lv_obj_align(error_label, LV_ALIGN_CENTER, 0, -18);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void ui_create_image_browser_tiles(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);
|
||||||
|
ui_create_image_tile(tile, filename, image_index, image_count);
|
||||||
|
image_index++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ui_image_browser_build_view(lv_obj_t *screen){
|
||||||
|
const size_t image_count = ui_count_sd_images();
|
||||||
|
|
||||||
|
ui_attach_toggle(screen);
|
||||||
|
lv_obj_set_style_bg_color(screen, lv_color_hex(0x070a0d), 0);
|
||||||
|
|
||||||
|
if(image_count == 0){
|
||||||
|
image_browser_image_count = 0;
|
||||||
|
ui_create_empty_image_browser(screen);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
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(image_count);
|
||||||
|
ui_image_browser_set_index(0, LV_ANIM_OFF);
|
||||||
|
ui_start_image_slideshow();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ui_image_browser_reset(void){
|
||||||
|
if(image_slideshow_timer != NULL){
|
||||||
|
lv_timer_delete(image_slideshow_timer);
|
||||||
|
image_slideshow_timer = NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
image_tileview = NULL;
|
||||||
|
image_browser_image_count = 0;
|
||||||
|
image_browser_current_index = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t ui_image_browser_get_image_count(void){
|
||||||
|
return image_browser_image_count;
|
||||||
|
}
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
#include <stdarg.h>
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <stddef.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
|
||||||
|
#include "lvgl.h"
|
||||||
|
#include "esp_lvgl_port.h"
|
||||||
|
|
||||||
|
#include "lcd/ui_log.h"
|
||||||
|
#include "lcd/ui_internal.h"
|
||||||
|
|
||||||
|
#define UI_LOG_MAX_LINES 41
|
||||||
|
#define UI_LOG_LINE_MAX_LENGTH 160
|
||||||
|
|
||||||
|
static lv_obj_t *log_textarea = NULL;
|
||||||
|
static char log_lines[UI_LOG_MAX_LINES][UI_LOG_LINE_MAX_LENGTH];
|
||||||
|
static size_t log_line_length = 0;
|
||||||
|
static size_t log_line_start = 0;
|
||||||
|
|
||||||
|
static size_t ui_log_buffer_index(size_t line_offset){
|
||||||
|
return (log_line_start + line_offset) % UI_LOG_MAX_LINES;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stores text in the ring buffer, sets *out_evicted when the oldest line had to
|
||||||
|
// be dropped, and returns the stored (possibly truncated) line.
|
||||||
|
static const char *ui_log_store_line(const char *text, bool *out_evicted){
|
||||||
|
size_t line_index;
|
||||||
|
bool evicted = false;
|
||||||
|
|
||||||
|
if(log_line_length < UI_LOG_MAX_LINES){
|
||||||
|
line_index = ui_log_buffer_index(log_line_length);
|
||||||
|
log_line_length++;
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
line_index = log_line_start;
|
||||||
|
log_line_start = ui_log_buffer_index(1);
|
||||||
|
evicted = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
snprintf(log_lines[line_index], sizeof(log_lines[line_index]), "%s", text);
|
||||||
|
*out_evicted = evicted;
|
||||||
|
return log_lines[line_index];
|
||||||
|
}
|
||||||
|
|
||||||
|
static void ui_log_append_line_locked(const char *line){
|
||||||
|
lv_textarea_add_text(log_textarea, line);
|
||||||
|
lv_textarea_add_text(log_textarea, "\n");
|
||||||
|
lv_textarea_set_cursor_pos(log_textarea, LV_TEXTAREA_CURSOR_LAST);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void ui_log_render_all_locked(void){
|
||||||
|
lv_textarea_set_text(log_textarea, "");
|
||||||
|
for(size_t line_offset = 0; line_offset < log_line_length; line_offset++){
|
||||||
|
lv_textarea_add_text(log_textarea, log_lines[ui_log_buffer_index(line_offset)]);
|
||||||
|
lv_textarea_add_text(log_textarea, "\n");
|
||||||
|
}
|
||||||
|
lv_textarea_set_cursor_pos(log_textarea, LV_TEXTAREA_CURSOR_LAST);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ui_log_append(const char *text){
|
||||||
|
if(text == NULL) return;
|
||||||
|
|
||||||
|
bool evicted = false;
|
||||||
|
const char *stored_line = ui_log_store_line(text, &evicted);
|
||||||
|
if(log_textarea == NULL) return;
|
||||||
|
|
||||||
|
lvgl_port_lock(0);
|
||||||
|
// Appending a line is O(1); only a dropped oldest line forces a full rebuild.
|
||||||
|
if(evicted) ui_log_render_all_locked();
|
||||||
|
else ui_log_append_line_locked(stored_line);
|
||||||
|
lvgl_port_unlock();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ui_log_appendf(const char *format, ...){
|
||||||
|
if(format == NULL) return;
|
||||||
|
|
||||||
|
char line[UI_LOG_LINE_MAX_LENGTH];
|
||||||
|
va_list args;
|
||||||
|
va_start(args, format);
|
||||||
|
vsnprintf(line, sizeof(line), format, args);
|
||||||
|
va_end(args);
|
||||||
|
|
||||||
|
ui_log_append(line);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ui_log_detach(void){
|
||||||
|
log_textarea = NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ui_log_build_view(lv_obj_t *screen){
|
||||||
|
ui_attach_toggle(screen);
|
||||||
|
lv_obj_set_style_bg_color(screen, lv_color_hex(0x101418), 0);
|
||||||
|
|
||||||
|
lv_obj_t *title = lv_label_create(screen);
|
||||||
|
ui_attach_toggle(title);
|
||||||
|
lv_label_set_text(title, "SD Image Viewer by Timo Niemann");
|
||||||
|
lv_obj_set_style_text_color(title, lv_color_white(), 0);
|
||||||
|
lv_obj_align(title, LV_ALIGN_TOP_MID, 0, 14);
|
||||||
|
|
||||||
|
log_textarea = lv_textarea_create(screen);
|
||||||
|
ui_attach_toggle(log_textarea);
|
||||||
|
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);
|
||||||
|
|
||||||
|
ui_log_render_all_locked();
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user