531 lines
21 KiB
C
531 lines
21 KiB
C
#include <errno.h>
|
|
#include <stdbool.h>
|
|
#include <stddef.h>
|
|
#include <stdint.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
#include "lvgl.h"
|
|
#include "draw/lv_draw_buf_private.h"
|
|
#include "esp_log.h"
|
|
#include "esp_heap_caps.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
|
|
|
|
/* Preloading: images are pre-rendered into full RGB565 buffers in PSRAM so a
|
|
* view switch only swaps a ready-made bitmap (no decode at switch time). */
|
|
#define UI_PREFETCH_SLOT_COUNT 2
|
|
#define UI_PREFETCH_MAX_PIXELS (480 * 800)
|
|
#define UI_PREFETCH_BUFFER_SIZE (UI_PREFETCH_MAX_PIXELS * 2) /* RGB565 = 2 bytes/px */
|
|
|
|
typedef struct {
|
|
uint8_t *buffer; /* raw PSRAM pixel buffer (UI_PREFETCH_BUFFER_SIZE) */
|
|
lv_draw_buf_t draw_buf; /* describes the rendered image for the widget src */
|
|
int32_t image_index; /* image currently rendered here, -1 if none */
|
|
} ui_prefetch_slot_t;
|
|
|
|
static lv_obj_t *image_view = NULL;
|
|
static lv_obj_t *image_widget = NULL;
|
|
static lv_obj_t *image_caption = NULL;
|
|
static lv_obj_t *image_error_label = 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 **image_browser_names = NULL;
|
|
static const char *TAG = "ui";
|
|
|
|
static ui_prefetch_slot_t prefetch_slots[UI_PREFETCH_SLOT_COUNT];
|
|
static lv_obj_t *prefetch_canvas = NULL;
|
|
static int32_t visible_slot = -1;
|
|
static bool prefetch_enabled = false;
|
|
|
|
static void ui_image_browser_display_current(void);
|
|
static void ui_image_browser_load_via_path(void);
|
|
static bool ui_prefetch_render(size_t image_index, ui_prefetch_slot_t *slot);
|
|
|
|
static void ui_image_browser_show_index(size_t image_index){
|
|
if(image_widget == NULL || image_browser_image_count == 0) return;
|
|
|
|
image_browser_current_index = image_index % image_browser_image_count;
|
|
ui_image_browser_display_current();
|
|
}
|
|
|
|
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_show_index(image_browser_current_index + 1);
|
|
}
|
|
|
|
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_gesture_event_cb(lv_event_t *event){
|
|
if(lv_event_get_code(event) != LV_EVENT_GESTURE) return;
|
|
if(image_browser_image_count <= 1) return;
|
|
|
|
lv_indev_t *indev = lv_indev_active();
|
|
if(indev == NULL) return;
|
|
|
|
const lv_dir_t gesture_dir = lv_indev_get_gesture_dir(indev);
|
|
if(gesture_dir == LV_DIR_TOP){
|
|
lv_indev_wait_release(indev);
|
|
ui_image_browser_show_index(image_browser_current_index + 1);
|
|
}
|
|
else if(gesture_dir == LV_DIR_BOTTOM){
|
|
lv_indev_wait_release(indev);
|
|
ui_image_browser_show_index(image_browser_current_index + image_browser_image_count - 1);
|
|
}
|
|
}
|
|
|
|
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 void ui_collect_image_names(size_t image_count){
|
|
if(image_browser_names == NULL) return;
|
|
|
|
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;
|
|
image_browser_names[image_index++] = filename;
|
|
}
|
|
}
|
|
|
|
/* Redirect the image decoder's working buffers (e.g. a full ARGB8888 PNG frame,
|
|
* ~1.5 MB) to PSRAM. Without this they come from LVGL's fixed 64 KB internal
|
|
* pool and large images fail to decode / crash. Only malloc/free are swapped;
|
|
* alignment and stride keep the default behaviour. Applied once, globally. */
|
|
static void *ui_psram_draw_buf_malloc(size_t size, lv_color_format_t color_format){
|
|
LV_UNUSED(color_format);
|
|
return heap_caps_malloc(size + LV_DRAW_BUF_ALIGN - 1, MALLOC_CAP_SPIRAM);
|
|
}
|
|
|
|
static void ui_psram_draw_buf_free(void *buf){
|
|
heap_caps_free(buf);
|
|
}
|
|
|
|
static void ui_image_decoder_use_psram(void){
|
|
static bool configured = false;
|
|
if(configured) return;
|
|
|
|
lv_draw_buf_handlers_t *image_handlers = lv_draw_buf_get_image_handlers();
|
|
if(image_handlers == NULL) return;
|
|
|
|
image_handlers->buf_malloc_cb = ui_psram_draw_buf_malloc;
|
|
image_handlers->buf_free_cb = ui_psram_draw_buf_free;
|
|
configured = true;
|
|
}
|
|
|
|
/* Fully decode/render an image into a slot's PSRAM RGB565 buffer once, so it can
|
|
* later be shown as a raw bitmap (a direct blit, no decoding at switch time). */
|
|
static bool ui_prefetch_render(size_t image_index, ui_prefetch_slot_t *slot){
|
|
if(prefetch_canvas == NULL || slot->buffer == NULL || image_browser_names == NULL) return false;
|
|
|
|
const char *filename = image_browser_names[image_index];
|
|
|
|
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_header_t header;
|
|
if(lv_image_decoder_get_info(image_source, &header) != LV_RESULT_OK) return false;
|
|
|
|
const int32_t width = (int32_t)header.w;
|
|
const int32_t height = (int32_t)header.h;
|
|
if(width <= 0 || height <= 0) return false;
|
|
if((size_t)width * (size_t)height > UI_PREFETCH_MAX_PIXELS) return false;
|
|
|
|
/* Render the image at native size into the slot buffer (black background so
|
|
* transparent PNG pixels match the app background). */
|
|
lv_canvas_set_buffer(prefetch_canvas, slot->buffer, width, height, LV_COLOR_FORMAT_RGB565);
|
|
lv_canvas_fill_bg(prefetch_canvas, lv_color_black(), LV_OPA_COVER);
|
|
|
|
lv_layer_t layer;
|
|
lv_canvas_init_layer(prefetch_canvas, &layer);
|
|
|
|
lv_draw_image_dsc_t draw_dsc;
|
|
lv_draw_image_dsc_init(&draw_dsc);
|
|
draw_dsc.src = image_source;
|
|
|
|
lv_area_t coords = { 0, 0, width - 1, height - 1 };
|
|
lv_draw_image(&layer, &draw_dsc, &coords);
|
|
lv_canvas_finish_layer(prefetch_canvas, &layer);
|
|
|
|
lv_draw_buf_init(&slot->draw_buf, width, height, LV_COLOR_FORMAT_RGB565,
|
|
lv_draw_buf_width_to_stride(width, LV_COLOR_FORMAT_RGB565),
|
|
slot->buffer, UI_PREFETCH_BUFFER_SIZE);
|
|
slot->image_index = (int32_t)image_index;
|
|
return true;
|
|
}
|
|
|
|
/* Pre-render the image following current_index into the non-visible slot. */
|
|
static void ui_prefetch_next(size_t current_index, int32_t current_slot){
|
|
if(!prefetch_enabled || image_browser_image_count <= 1) return;
|
|
|
|
const size_t next_index = (current_index + 1) % image_browser_image_count;
|
|
const int32_t other = (current_slot == 0) ? 1 : 0;
|
|
if(prefetch_slots[other].image_index == (int32_t)next_index) return;
|
|
|
|
ui_prefetch_render(next_index, &prefetch_slots[other]);
|
|
}
|
|
|
|
/* Try to show the current image from a pre-rendered PSRAM buffer. */
|
|
static bool ui_image_browser_display_prefetched(void){
|
|
if(!prefetch_enabled) return false;
|
|
|
|
const size_t index = image_browser_current_index;
|
|
|
|
int32_t slot = -1;
|
|
for(int32_t i = 0; i < UI_PREFETCH_SLOT_COUNT; i++){
|
|
if(prefetch_slots[i].image_index == (int32_t)index){ slot = i; break; }
|
|
}
|
|
|
|
if(slot < 0){
|
|
/* Not prefetched yet: render on demand into the non-visible slot. */
|
|
slot = (visible_slot == 0) ? 1 : 0;
|
|
if(!ui_prefetch_render(index, &prefetch_slots[slot])) return false;
|
|
}
|
|
|
|
lv_image_set_src(image_widget, &prefetch_slots[slot].draw_buf);
|
|
visible_slot = slot;
|
|
|
|
const char *filename = image_browser_names[index];
|
|
if(image_caption != NULL){
|
|
lv_label_set_text_fmt(image_caption, "%u/%u %s",
|
|
(unsigned int)(index + 1),
|
|
(unsigned int)image_browser_image_count, filename);
|
|
}
|
|
|
|
lv_obj_remove_flag(image_widget, LV_OBJ_FLAG_HIDDEN);
|
|
if(image_error_label != NULL) lv_obj_add_flag(image_error_label, LV_OBJ_FLAG_HIDDEN);
|
|
|
|
ui_log_appendf("Shown %u/%u: %s (%dx%d)",
|
|
(unsigned int)(index + 1),
|
|
(unsigned int)image_browser_image_count, filename,
|
|
(int)prefetch_slots[slot].draw_buf.header.w,
|
|
(int)prefetch_slots[slot].draw_buf.header.h);
|
|
|
|
/* Warm up the next image so the following switch is instant too. */
|
|
ui_prefetch_next(index, slot);
|
|
return true;
|
|
}
|
|
|
|
/* Fallback: decode straight from the file (also drives the error diagnostics). */
|
|
static void ui_image_browser_load_via_path(void){
|
|
if(image_widget == NULL || image_browser_names == NULL || image_browser_image_count == 0) return;
|
|
|
|
const char *filename = image_browser_names[image_browser_current_index];
|
|
|
|
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_widget, image_source);
|
|
|
|
if(image_caption != NULL){
|
|
lv_label_set_text_fmt(image_caption, "%u/%u %s",
|
|
(unsigned int)(image_browser_current_index + 1),
|
|
(unsigned int)image_browser_image_count, filename);
|
|
}
|
|
|
|
if(lv_image_get_src_width(image_widget) <= 0 || lv_image_get_src_height(image_widget) <= 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_add_flag(image_widget, LV_OBJ_FLAG_HIDDEN);
|
|
if(image_error_label != NULL){
|
|
lv_label_set_text_fmt(image_error_label, "Image cannot be loaded\n%s\n%s", filename, load_reason);
|
|
lv_obj_remove_flag(image_error_label, LV_OBJ_FLAG_HIDDEN);
|
|
}
|
|
}
|
|
else{
|
|
ui_log_appendf("Loaded %u/%u: %s (%dx%d)",
|
|
(unsigned int)(image_browser_current_index + 1),
|
|
(unsigned int)image_browser_image_count, filename,
|
|
(int)lv_image_get_src_width(image_widget),
|
|
(int)lv_image_get_src_height(image_widget));
|
|
|
|
lv_obj_remove_flag(image_widget, LV_OBJ_FLAG_HIDDEN);
|
|
if(image_error_label != NULL) lv_obj_add_flag(image_error_label, LV_OBJ_FLAG_HIDDEN);
|
|
}
|
|
}
|
|
|
|
static void ui_image_browser_display_current(void){
|
|
if(image_widget == NULL || image_browser_names == NULL || image_browser_image_count == 0) return;
|
|
|
|
if(ui_image_browser_display_prefetched()) return;
|
|
|
|
/* Prefetch unavailable or render failed: fall back to on-demand decoding. */
|
|
visible_slot = -1;
|
|
ui_image_browser_load_via_path();
|
|
}
|
|
|
|
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_browser_image_count = image_count;
|
|
image_browser_current_index = 0;
|
|
image_browser_names = calloc(image_count, sizeof(const char *));
|
|
ui_collect_image_names(image_count);
|
|
|
|
image_view = lv_obj_create(screen);
|
|
ui_attach_toggle(image_view);
|
|
lv_obj_set_size(image_view, LV_PCT(100), LV_PCT(100));
|
|
lv_obj_center(image_view);
|
|
lv_obj_set_style_bg_color(image_view, lv_color_hex(0x070a0d), 0);
|
|
lv_obj_set_style_bg_opa(image_view, LV_OPA_COVER, 0);
|
|
lv_obj_set_style_border_width(image_view, 0, 0);
|
|
lv_obj_set_style_radius(image_view, 0, 0);
|
|
lv_obj_set_style_pad_all(image_view, 0, 0);
|
|
lv_obj_remove_flag(image_view, LV_OBJ_FLAG_SCROLLABLE);
|
|
lv_obj_remove_flag(image_view, LV_OBJ_FLAG_GESTURE_BUBBLE);
|
|
lv_obj_add_event_cb(image_view, ui_image_browser_gesture_event_cb, LV_EVENT_GESTURE, NULL);
|
|
|
|
image_widget = lv_image_create(image_view);
|
|
ui_attach_toggle(image_widget);
|
|
lv_obj_set_size(image_widget, LV_PCT(100), LV_PCT(100));
|
|
lv_image_set_inner_align(image_widget, LV_IMAGE_ALIGN_CONTAIN);
|
|
lv_obj_center(image_widget);
|
|
|
|
lv_obj_t *info_bar = lv_obj_create(image_view);
|
|
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);
|
|
|
|
image_caption = lv_label_create(info_bar);
|
|
ui_attach_toggle(image_caption);
|
|
lv_label_set_long_mode(image_caption, LV_LABEL_LONG_DOT);
|
|
lv_obj_set_width(image_caption, LV_PCT(100));
|
|
lv_obj_set_style_text_color(image_caption, lv_color_hex(0xdde7ef), 0);
|
|
lv_obj_center(image_caption);
|
|
|
|
image_error_label = lv_label_create(image_view);
|
|
ui_attach_toggle(image_error_label);
|
|
lv_label_set_text(image_error_label, "");
|
|
lv_obj_set_style_text_color(image_error_label, lv_color_hex(0xffd166), 0);
|
|
lv_obj_set_style_text_align(image_error_label, LV_TEXT_ALIGN_CENTER, 0);
|
|
lv_obj_set_width(image_error_label, LV_PCT(86));
|
|
lv_obj_align(image_error_label, LV_ALIGN_CENTER, 0, -18);
|
|
lv_obj_add_flag(image_error_label, LV_OBJ_FLAG_HIDDEN);
|
|
|
|
/* Preloading: route decoder buffers to PSRAM and allocate two RGB565 slots
|
|
* plus a hidden canvas used to pre-render images off-screen. */
|
|
ui_image_decoder_use_psram();
|
|
prefetch_enabled = false;
|
|
visible_slot = -1;
|
|
for(int32_t slot_index = 0; slot_index < UI_PREFETCH_SLOT_COUNT; slot_index++){
|
|
prefetch_slots[slot_index].image_index = -1;
|
|
prefetch_slots[slot_index].buffer = heap_caps_malloc(UI_PREFETCH_BUFFER_SIZE, MALLOC_CAP_SPIRAM);
|
|
}
|
|
if(prefetch_slots[0].buffer != NULL && prefetch_slots[1].buffer != NULL){
|
|
prefetch_canvas = lv_canvas_create(image_view);
|
|
lv_obj_add_flag(prefetch_canvas, LV_OBJ_FLAG_HIDDEN);
|
|
lv_obj_remove_flag(prefetch_canvas, LV_OBJ_FLAG_CLICKABLE);
|
|
prefetch_enabled = true;
|
|
}
|
|
else{
|
|
for(int32_t slot_index = 0; slot_index < UI_PREFETCH_SLOT_COUNT; slot_index++){
|
|
heap_caps_free(prefetch_slots[slot_index].buffer);
|
|
prefetch_slots[slot_index].buffer = NULL;
|
|
}
|
|
ESP_LOGW(TAG, "Image preloading disabled: PSRAM buffers unavailable");
|
|
ui_log_append("Preloading disabled (no PSRAM)");
|
|
}
|
|
|
|
ui_image_browser_show_index(0);
|
|
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;
|
|
}
|
|
|
|
/* Drop the widget's reference to a prefetch buffer before freeing it. */
|
|
if(image_widget != NULL) lv_image_set_src(image_widget, NULL);
|
|
|
|
free((void *)image_browser_names);
|
|
image_browser_names = NULL;
|
|
|
|
for(int32_t slot_index = 0; slot_index < UI_PREFETCH_SLOT_COUNT; slot_index++){
|
|
if(prefetch_slots[slot_index].buffer != NULL){
|
|
heap_caps_free(prefetch_slots[slot_index].buffer);
|
|
prefetch_slots[slot_index].buffer = NULL;
|
|
}
|
|
prefetch_slots[slot_index].image_index = -1;
|
|
}
|
|
prefetch_canvas = NULL; /* deleted together with the screen contents */
|
|
prefetch_enabled = false;
|
|
visible_slot = -1;
|
|
|
|
image_view = NULL;
|
|
image_widget = NULL;
|
|
image_caption = NULL;
|
|
image_error_label = 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;
|
|
}
|