feat(ImmichElias): add person selector like Bible books, 4 persons Elias/Grace/Alicia/Adolfo, picker UI with toolbar, player bottom-only, random pagination, cache, fullscreen
This commit is contained in:
@@ -38,6 +38,20 @@ static const char* TAG = "ImmichElias";
|
||||
#define FALLBACK_W 320
|
||||
#define FALLBACK_H 240
|
||||
|
||||
typedef struct {
|
||||
char id[64];
|
||||
char name[32];
|
||||
char count_str[16];
|
||||
} PersonInfo;
|
||||
|
||||
static const PersonInfo PERSONS[] = {
|
||||
{"cbece6a9-720f-482a-a945-c9ba5ca41fd4", "Elias", "10739"},
|
||||
{"ad40f024-c34b-4ec6-a19e-2fa157b259e5", "Grace", ""},
|
||||
{"6d9e4ae6-143d-43c9-bd42-437e3edfe2df", "Alicia", ""},
|
||||
{"3ddbc731-e857-473c-8226-8af97e5cb86e", "Adolfo", ""},
|
||||
};
|
||||
#define PERSONS_COUNT (sizeof(PERSONS)/sizeof(PERSONS[0]))
|
||||
|
||||
typedef struct {
|
||||
char id[64];
|
||||
char thumbnail_url[256];
|
||||
@@ -150,7 +164,8 @@ static void shuffle_photos(void) {
|
||||
ESP_LOGI(TAG, "Photos shuffled random, count %d", photo_count);
|
||||
}
|
||||
|
||||
static int fetch_person_assets(void) {
|
||||
static int fetch_person_assets(const char* person_id) {
|
||||
if (!person_id || person_id[0]=='\0') person_id = PERSONS[0].id;
|
||||
char* json_buf = heap_caps_malloc(JSON_BUF_SIZE, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
|
||||
if (!json_buf) json_buf = heap_caps_malloc(JSON_BUF_SIZE, MALLOC_CAP_DEFAULT | MALLOC_CAP_8BIT);
|
||||
if (!json_buf) return -1;
|
||||
@@ -158,14 +173,21 @@ static int fetch_person_assets(void) {
|
||||
char url[256];
|
||||
snprintf(url, sizeof(url), "%s/api/search/metadata", IMMICH_BASE_URL);
|
||||
// Pick random page to get variety from 10k+ photos
|
||||
// Estimate total photos 10739, with MAX_PHOTOS 100 => ~107 pages
|
||||
int total_estimate = 10739;
|
||||
// Try to use count from PERSONS if available
|
||||
for (int pi=0; pi<PERSONS_COUNT; pi++) {
|
||||
if (strcmp(PERSONS[pi].id, person_id)==0 && PERSONS[pi].count_str[0]!='\0') {
|
||||
total_estimate = atoi(PERSONS[pi].count_str);
|
||||
if (total_estimate<=0) total_estimate=10739;
|
||||
break;
|
||||
}
|
||||
}
|
||||
int max_page = total_estimate / MAX_PHOTOS;
|
||||
if (max_page < 1) max_page = 1;
|
||||
int random_page = (esp_random() % max_page) + 1;
|
||||
char body[320];
|
||||
snprintf(body, sizeof(body), "{\"personIds\":[\"%s\"],\"size\":%d,\"page\":%d}", IMMICH_PERSON_ID, MAX_PHOTOS, random_page);
|
||||
ESP_LOGI(TAG, "Fetching person assets page %d/%d size %d", random_page, max_page, MAX_PHOTOS);
|
||||
snprintf(body, sizeof(body), "{\"personIds\":[\"%s\"],\"size\":%d,\"page\":%d}", person_id, MAX_PHOTOS, random_page);
|
||||
ESP_LOGI(TAG, "Fetching person %s assets page %d/%d size %d", person_id, random_page, max_page, MAX_PHOTOS);
|
||||
int status = http_post_json(url, body, &hb);
|
||||
if (status != 200) { heap_caps_free(json_buf); return -1; }
|
||||
if (hb.len >= JSON_BUF_SIZE) hb.len = JSON_BUF_SIZE - 1;
|
||||
@@ -247,6 +269,10 @@ typedef struct {
|
||||
AppHandle app;
|
||||
volatile bool visible;
|
||||
volatile bool stop_requested;
|
||||
// Picker (persons) like Bible book selector
|
||||
lv_obj_t* picker_wrapper;
|
||||
lv_obj_t* lst_persons;
|
||||
// Player (photos)
|
||||
lv_obj_t* player_wrapper;
|
||||
lv_obj_t* header_bar;
|
||||
lv_obj_t* ctrl_bar;
|
||||
@@ -275,6 +301,9 @@ typedef struct {
|
||||
int disp_h;
|
||||
int img_w;
|
||||
int img_h;
|
||||
// Current person
|
||||
char current_person_id[64];
|
||||
char current_person_name[32];
|
||||
} AppCtx;
|
||||
|
||||
static bool get_cache_dir_path(AppCtx* ctx, char* out_dir, size_t out_size) {
|
||||
@@ -624,9 +653,11 @@ static void fetch_task_fn(void* arg) {
|
||||
if (!ctx) { vTaskDelete(NULL); return; }
|
||||
if (ctx->stop_requested) { ctx->fetch_task=NULL; ctx->fetch_in_progress=false; vTaskDelete(NULL); return; }
|
||||
ctx->fetch_in_progress=true;
|
||||
snprintf(ctx->status_text, sizeof(ctx->status_text), "Fetching %s photos...", PERSON_NAME);
|
||||
const char* fetch_name = ctx->current_person_name[0] ? ctx->current_person_name : PERSON_NAME;
|
||||
const char* fetch_id = ctx->current_person_id[0] ? ctx->current_person_id : PERSONS[0].id;
|
||||
snprintf(ctx->status_text, sizeof(ctx->status_text), "Fetching %s photos...", fetch_name);
|
||||
ui_update_all(ctx);
|
||||
int n = fetch_person_assets();
|
||||
int n = fetch_person_assets(fetch_id);
|
||||
if (ctx->stop_requested) { ctx->fetch_task=NULL; ctx->fetch_in_progress=false; vTaskDelete(NULL); return; }
|
||||
if (n>0) {
|
||||
ctx->current_idx=0;
|
||||
@@ -720,13 +751,32 @@ static void reload_event_cb(lv_event_t* e) {
|
||||
snprintf(ctx->status_text, sizeof(ctx->status_text), "Reloading %dx%d...", ctx->img_w, ctx->img_h);
|
||||
ui_update_all(ctx); show_chrome(ctx); start_fetch_task(ctx);
|
||||
}
|
||||
static void back_event_cb(lv_event_t* e) { (void)e; tt_app_stop(); }
|
||||
static void show_picker(AppCtx* ctx);
|
||||
static void show_player(AppCtx* ctx);
|
||||
|
||||
static void back_event_cb(lv_event_t* e) {
|
||||
AppCtx* ctx = (AppCtx*)lv_event_get_user_data(e);
|
||||
if (!ctx) { tt_app_stop(); return; }
|
||||
// If in player, go back to person picker like Bible book selector
|
||||
if (ctx->picker_wrapper && ctx->player_wrapper) {
|
||||
if (!tt_lvgl_lock(pdMS_TO_TICKS(500))) { tt_app_stop(); return; }
|
||||
bool player_visible = !lv_obj_has_flag(ctx->player_wrapper, LV_OBJ_FLAG_HIDDEN);
|
||||
tt_lvgl_unlock();
|
||||
if (player_visible) {
|
||||
show_picker(ctx);
|
||||
return;
|
||||
}
|
||||
}
|
||||
tt_app_stop();
|
||||
}
|
||||
|
||||
static void* create_data(void) {
|
||||
AppCtx* ctx = calloc(1, sizeof(AppCtx));
|
||||
if (ctx) {
|
||||
ctx->current_idx=0; ctx->disp_w=FALLBACK_W; ctx->disp_h=FALLBACK_H; ctx->img_w=FALLBACK_W; ctx->img_h=FALLBACK_H;
|
||||
snprintf(ctx->status_text, sizeof(ctx->status_text), "Init %s proxy %s (random)", IMMICH_BASE_URL, RESIZE_PROXY_URL);
|
||||
strncpy(ctx->current_person_id, PERSONS[0].id, sizeof(ctx->current_person_id)-1);
|
||||
strncpy(ctx->current_person_name, PERSONS[0].name, sizeof(ctx->current_person_name)-1);
|
||||
snprintf(ctx->status_text, sizeof(ctx->status_text), "Init %s proxy %s (random) person %s", IMMICH_BASE_URL, RESIZE_PROXY_URL, ctx->current_person_name);
|
||||
ctx->thumb_buf = heap_caps_malloc(THUMB_BUF_SIZE, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
|
||||
if (!ctx->thumb_buf) ctx->thumb_buf = heap_caps_malloc(THUMB_BUF_SIZE, MALLOC_CAP_DEFAULT | MALLOC_CAP_8BIT);
|
||||
}
|
||||
@@ -742,9 +792,73 @@ static void destroy_data(void* data) {
|
||||
if (ctx->thumb_buf) { heap_caps_free(ctx->thumb_buf); ctx->thumb_buf=NULL; }
|
||||
free(ctx);
|
||||
}
|
||||
static void on_create(AppHandle app, void* data) { AppCtx* ctx=(AppCtx*)data; if(ctx) ctx->app=app; ESP_LOGI(TAG,"onCreate proxy=%s random %dx%d", RESIZE_PROXY_URL, ctx?ctx->img_w:0, ctx?ctx->img_h:0); }
|
||||
static void on_create(AppHandle app, void* data) { AppCtx* ctx=(AppCtx*)data; if(ctx) ctx->app=app; ESP_LOGI(TAG,"onCreate proxy=%s random %dx%d person %s", RESIZE_PROXY_URL, ctx?ctx->img_w:0, ctx?ctx->img_h:0, ctx?ctx->current_person_name:"?"); }
|
||||
static void on_destroy(AppHandle app, void* data) { (void)app;(void)data; }
|
||||
|
||||
static void person_selected_cb(lv_event_t* e) {
|
||||
AppCtx* ctx = (AppCtx*)lv_event_get_user_data(e);
|
||||
if (!ctx) return;
|
||||
int idx = (int)(intptr_t)lv_obj_get_user_data(lv_event_get_target(e));
|
||||
if (idx < 0 || idx >= PERSONS_COUNT) return;
|
||||
const PersonInfo* p = &PERSONS[idx];
|
||||
strncpy(ctx->current_person_id, p->id, sizeof(ctx->current_person_id)-1);
|
||||
strncpy(ctx->current_person_name, p->name, sizeof(ctx->current_person_name)-1);
|
||||
ESP_LOGI(TAG, "Person selected %s (%s)", p->name, p->id);
|
||||
photo_count = 0;
|
||||
ctx->current_idx = 0;
|
||||
ctx->thumb_len = 0;
|
||||
ctx->has_image = false;
|
||||
show_player(ctx);
|
||||
start_fetch_task(ctx);
|
||||
}
|
||||
|
||||
static void build_person_picker(AppCtx* ctx, lv_obj_t* parent) {
|
||||
ctx->picker_wrapper = lv_obj_create(parent);
|
||||
lv_obj_set_size(ctx->picker_wrapper, LV_PCT(100), LV_PCT(100));
|
||||
lv_obj_set_style_border_width(ctx->picker_wrapper, 0, 0);
|
||||
lv_obj_set_style_pad_all(ctx->picker_wrapper, 0, 0);
|
||||
lv_obj_set_style_bg_color(ctx->picker_wrapper, lv_color_hex(0x121212), LV_PART_MAIN);
|
||||
lv_obj_set_style_bg_opa(ctx->picker_wrapper, LV_OPA_COVER, LV_PART_MAIN);
|
||||
|
||||
lv_obj_t* toolbar = tt_lvgl_toolbar_create_for_app(ctx->picker_wrapper, ctx->app);
|
||||
|
||||
ctx->lst_persons = lv_list_create(ctx->picker_wrapper);
|
||||
lv_obj_set_width(ctx->lst_persons, LV_PCT(100));
|
||||
lv_obj_align_to(ctx->lst_persons, toolbar, LV_ALIGN_OUT_BOTTOM_MID, 0, 0);
|
||||
// Calculate height
|
||||
int32_t th = 44;
|
||||
// Use parent height minus toolbar
|
||||
lv_obj_set_height(ctx->lst_persons, LV_PCT(90));
|
||||
lv_obj_set_style_bg_color(ctx->lst_persons, lv_color_hex(0x121212), LV_PART_MAIN);
|
||||
|
||||
for (int i=0;i<PERSONS_COUNT;i++) {
|
||||
const PersonInfo* person = &PERSONS[i];
|
||||
char label[64];
|
||||
if (person->count_str[0]) snprintf(label, sizeof(label), "%s (%s)", person->name, person->count_str);
|
||||
else snprintf(label, sizeof(label), "%s", person->name);
|
||||
lv_obj_t* btn = lv_list_add_button(ctx->lst_persons, LV_SYMBOL_DIRECTORY, label);
|
||||
lv_obj_set_user_data(btn, (void*)(intptr_t)i);
|
||||
lv_obj_add_event_cb(btn, person_selected_cb, LV_EVENT_CLICKED, ctx);
|
||||
}
|
||||
}
|
||||
|
||||
static void show_picker(AppCtx* ctx) {
|
||||
if (!ctx) return;
|
||||
if (!tt_lvgl_lock(pdMS_TO_TICKS(500))) return;
|
||||
if (ctx->player_wrapper) lv_obj_add_flag(ctx->player_wrapper, LV_OBJ_FLAG_HIDDEN);
|
||||
if (ctx->picker_wrapper) lv_obj_remove_flag(ctx->picker_wrapper, LV_OBJ_FLAG_HIDDEN);
|
||||
tt_lvgl_unlock();
|
||||
}
|
||||
|
||||
static void show_player(AppCtx* ctx) {
|
||||
if (!ctx) return;
|
||||
if (!tt_lvgl_lock(pdMS_TO_TICKS(500))) return;
|
||||
if (ctx->picker_wrapper) lv_obj_add_flag(ctx->picker_wrapper, LV_OBJ_FLAG_HIDDEN);
|
||||
if (ctx->player_wrapper) lv_obj_remove_flag(ctx->player_wrapper, LV_OBJ_FLAG_HIDDEN);
|
||||
tt_lvgl_unlock();
|
||||
show_chrome(ctx);
|
||||
}
|
||||
|
||||
static void on_show(AppHandle app, void* data, lv_obj_t* parent) {
|
||||
AppCtx* ctx=(AppCtx*)data; if(!ctx) return;
|
||||
ctx->visible=true; ctx->stop_requested=false; ctx->app=app;
|
||||
@@ -756,6 +870,9 @@ static void on_show(AppHandle app, void* data, lv_obj_t* parent) {
|
||||
lv_obj_set_style_bg_color(parent, lv_color_hex(0x121212), LV_PART_MAIN);
|
||||
lv_obj_set_style_bg_opa(parent, LV_OPA_COVER, LV_PART_MAIN);
|
||||
|
||||
// Picker for persons (like Bible book selector)
|
||||
build_person_picker(ctx, parent);
|
||||
|
||||
// Fullscreen player like Bible app - image background, only bottom controls
|
||||
ctx->player_wrapper = lv_obj_create(parent);
|
||||
lv_obj_set_size(ctx->player_wrapper, LV_PCT(100), LV_PCT(100));
|
||||
@@ -765,6 +882,7 @@ static void on_show(AppHandle app, void* data, lv_obj_t* parent) {
|
||||
lv_obj_set_style_bg_color(ctx->player_wrapper, lv_color_hex(0x121212), LV_PART_MAIN);
|
||||
lv_obj_set_style_bg_opa(ctx->player_wrapper, LV_OPA_COVER, LV_PART_MAIN);
|
||||
lv_obj_remove_flag(ctx->player_wrapper, LV_OBJ_FLAG_SCROLLABLE);
|
||||
lv_obj_add_flag(ctx->player_wrapper, LV_OBJ_FLAG_HIDDEN);
|
||||
|
||||
// Image covers entire background
|
||||
ctx->img_page = lv_image_create(ctx->player_wrapper);
|
||||
@@ -777,7 +895,6 @@ static void on_show(AppHandle app, void* data, lv_obj_t* parent) {
|
||||
lv_obj_add_flag(ctx->img_page, LV_OBJ_FLAG_CLICKABLE);
|
||||
lv_obj_add_event_cb(ctx->img_page, on_image_click, LV_EVENT_CLICKED, ctx);
|
||||
|
||||
// Bottom-only controls like Bible app - no top header, no info/status text
|
||||
ctx->header_bar = NULL;
|
||||
ctx->lbl_book_title = NULL;
|
||||
ctx->lbl_indicator = NULL;
|
||||
@@ -798,12 +915,11 @@ static void on_show(AppHandle app, void* data, lv_obj_t* parent) {
|
||||
lv_obj_set_flex_align(ctx->ctrl_bar, LV_FLEX_ALIGN_SPACE_BETWEEN, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
|
||||
lv_obj_remove_flag(ctx->ctrl_bar, LV_OBJ_FLAG_SCROLLABLE);
|
||||
|
||||
// Left close button (like Bible app X)
|
||||
ctx->btn_back = lv_btn_create(ctx->ctrl_bar);
|
||||
lv_obj_set_size(ctx->btn_back, 44, 32);
|
||||
lv_obj_set_style_radius(ctx->btn_back, 8, 0);
|
||||
lv_obj_set_style_bg_color(ctx->btn_back, lv_color_hex(0x252538), LV_PART_MAIN);
|
||||
lv_obj_t* lbl_back = lv_label_create(ctx->btn_back); lv_label_set_text(lbl_back, LV_SYMBOL_CLOSE); lv_obj_center(lbl_back);
|
||||
lv_obj_t* lbl_back = lv_label_create(ctx->btn_back); lv_label_set_text(lbl_back, LV_SYMBOL_LEFT); lv_obj_center(lbl_back);
|
||||
lv_obj_add_event_cb(ctx->btn_back, back_event_cb, LV_EVENT_CLICKED, ctx);
|
||||
|
||||
ctx->btn_prev = lv_btn_create(ctx->ctrl_bar);
|
||||
@@ -828,24 +944,25 @@ static void on_show(AppHandle app, void* data, lv_obj_t* parent) {
|
||||
lv_obj_t* ln = lv_label_create(ctx->btn_next); lv_label_set_text(ln, LV_SYMBOL_NEXT); lv_obj_center(ln);
|
||||
lv_obj_add_event_cb(ctx->btn_next, next_event_cb, LV_EVENT_CLICKED, ctx);
|
||||
|
||||
// Timers: slideshow every 60s, autohide chrome after 3s
|
||||
// Only slideshow timer (60s), no autohide for stability - chrome toggles on tap like BookPlayer
|
||||
if (tt_lvgl_lock(pdMS_TO_TICKS(500))) {
|
||||
if (ctx->slideshow_timer) { lv_timer_delete(ctx->slideshow_timer); ctx->slideshow_timer=NULL; }
|
||||
ctx->slideshow_timer = lv_timer_create(slideshow_timer_cb, 60000, ctx);
|
||||
if (ctx->autohide_timer) { lv_timer_delete(ctx->autohide_timer); ctx->autohide_timer=NULL; }
|
||||
// Autohide disabled - user taps to hide/show like BookPlayer
|
||||
ctx->autohide_timer = NULL;
|
||||
tt_lvgl_unlock();
|
||||
}
|
||||
|
||||
if (photo_count>0) {
|
||||
if(ctx->current_idx>=photo_count) ctx->current_idx=0;
|
||||
snprintf(ctx->status_text,sizeof(ctx->status_text),"Cached %d random %dx%d",photo_count, ctx->img_w, ctx->img_h);
|
||||
ui_update_all(ctx); show_chrome(ctx); start_thumb_task(ctx);
|
||||
// Always show person picker first like Bible book selector
|
||||
show_picker(ctx);
|
||||
// If we already have photos for current person, user can still see them after selecting
|
||||
// Don't auto-start fetch here; wait for person selection to fetch fresh random page
|
||||
// But if we have cached photos from previous session for current person, we could show player directly
|
||||
// For now, just show picker and let user choose person
|
||||
if (photo_count>0 && ctx->current_person_id[0]!='\0') {
|
||||
// Optionally prefetch in background
|
||||
snprintf(ctx->status_text,sizeof(ctx->status_text),"Cached %d for %s - select person", photo_count, ctx->current_person_name);
|
||||
} else {
|
||||
snprintf(ctx->status_text,sizeof(ctx->status_text),"Fetching %s random %dx%d via %s disp %dx%d",PERSON_NAME, ctx->img_w, ctx->img_h, RESIZE_PROXY_URL, ctx->disp_w, ctx->disp_h);
|
||||
ui_update_all(ctx); show_chrome(ctx); start_fetch_task(ctx);
|
||||
snprintf(ctx->status_text,sizeof(ctx->status_text),"Select a person");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user