diff --git a/Apps/ImmichElias/main/Source/main.c b/Apps/ImmichElias/main/Source/main.c index 42582b4..76a23e0 100644 --- a/Apps/ImmichElias/main/Source/main.c +++ b/Apps/ImmichElias/main/Source/main.c @@ -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= 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;icount_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"); } }