diff --git a/Apps/BibleVerse/main/Source/main.c b/Apps/BibleVerse/main/Source/main.c index b534fae..377020d 100644 --- a/Apps/BibleVerse/main/Source/main.c +++ b/Apps/BibleVerse/main/Source/main.c @@ -31,6 +31,7 @@ LV_FONT_DECLARE(georgia_regular_24) // books.json : array of {bnumber, bname, verses, chapters} #define MAX_BIBLE_BOOKS 66 +#define MAX_FAVORITES 512 #define MAX_BOOK_NAME 40 #define MAX_VERSE_TEXT 2048 #define MAX_PATH 128 @@ -115,11 +116,23 @@ typedef struct { lv_obj_t* book_center_num; lv_obj_t* book_center_verses; lv_obj_t* book_slider; // fast scrub + lv_obj_t* btn_favorites; int book_dial_idx; // 0..book_count-1 currently selected + int favorite_indices[MAX_FAVORITES]; + int favorite_count; + int favorite_dial_idx; + bool browsing_favorites; + bool favorite_navigation_active; int book_drag_start_x; bool book_dragging; bool book_browser_visible; + // Reading-view swipe tracking + int verse_drag_start_x; + int verse_drag_start_y; + bool verse_dragging; + bool verse_swipe_consumed; + // Timer lv_timer_t* auto_timer; lv_timer_t* chrome_auto_hide_timer; // menu auto-hide after 10s on start @@ -135,6 +148,7 @@ static bool load_book(AppCtx* ctx, int bnumber); static void unload_book(AppCtx* ctx); static void show_current_verse(AppCtx* ctx); static void advance_verse(AppCtx* ctx, int delta); +static void manually_advance_verse(AppCtx* ctx, int delta); static void jump_to_book(AppCtx* ctx, int book_idx); static void jump_to_global(AppCtx* ctx, int global_idx); static void save_progress(AppCtx* ctx); @@ -142,6 +156,7 @@ static bool load_progress(AppCtx* ctx); static void toggle_ui(AppCtx* ctx); static void set_chrome_visible(AppCtx* ctx, bool visible); static bool is_favorite(AppCtx* ctx, int global_idx); +static void load_favorites(AppCtx* ctx); static void toggle_favorite(AppCtx* ctx); static void ensure_user_dir(AppCtx* ctx); static void set_book_browser_visible(AppCtx* ctx, bool visible); @@ -524,13 +539,33 @@ static bool is_favorite(AppCtx* ctx, int global_idx) { return false; } +static void load_favorites(AppCtx* ctx) { + ctx->favorite_count = 0; + FILE* f = fopen(ctx->favorites_path, "r"); + if (!f) return; + int global_idx; + while (ctx->favorite_count < MAX_FAVORITES && fscanf(f, "%d", &global_idx) == 1) { + if (global_idx >= 0 && global_idx < ctx->total_verses) { + ctx->favorite_indices[ctx->favorite_count++] = global_idx; + } + } + fclose(f); +} + +static int find_favorite_position(AppCtx* ctx, int global_idx) { + for (int i = 0; i < ctx->favorite_count; i++) { + if (ctx->favorite_indices[i] == global_idx) return i; + } + return -1; +} + static void toggle_favorite(AppCtx* ctx) { // load all - int vals[512]; + int vals[MAX_FAVORITES]; int cnt=0; FILE* f = fopen(ctx->favorites_path, "r"); if (f) { - while (cnt<512 && fscanf(f, "%d", &vals[cnt])==1) cnt++; + while (cntis_highlighted=false; } else { - if (cnt<512) { + if (cntcur_global; ctx->is_highlighted=true; } @@ -552,6 +587,7 @@ static void toggle_favorite(AppCtx* ctx) { if (!f) return; for (int i=0;iis_highlighted = is_favorite(ctx, ctx->cur_global); + if (ctx->favorite_navigation_active) { + load_favorites(ctx); + int favorite_pos = find_favorite_position(ctx, ctx->cur_global); + if (favorite_pos >= 0) { + ctx->favorite_dial_idx = favorite_pos; + if (ctx->lbl_position) { + char favorite_pos_buf[40]; + snprintf(favorite_pos_buf, sizeof(favorite_pos_buf), "Fav %d / %d", + favorite_pos + 1, ctx->favorite_count); + lv_label_set_text(ctx->lbl_position, favorite_pos_buf); + } + } else if (ctx->favorite_count == 0) { + ctx->favorite_navigation_active = false; + } + } if (ctx->btn_highlight) { lv_obj_t* lbl = lv_obj_get_child(ctx->btn_highlight, 0); - if (lbl) lv_label_set_text(lbl, ctx->is_highlighted ? LV_SYMBOL_OK : LV_SYMBOL_DUMMY); + if (lbl) lv_label_set_text(lbl, ctx->is_highlighted ? LV_SYMBOL_OK : LV_SYMBOL_PLUS); if (ctx->is_highlighted) { lv_obj_set_style_bg_color(ctx->btn_highlight, lv_color_hex(0xF5C16C), 0); lv_obj_set_style_text_color(ctx->btn_highlight, lv_color_hex(0x1E1E2E), 0); @@ -734,11 +785,17 @@ static void show_current_verse(AppCtx* ctx) { } if (ctx->btn_prev) { - if (ctx->cur_global<=0) lv_obj_add_state(ctx->btn_prev, LV_STATE_DISABLED); + bool at_start = ctx->favorite_navigation_active + ? ctx->favorite_dial_idx <= 0 + : ctx->cur_global <= 0; + if (at_start) lv_obj_add_state(ctx->btn_prev, LV_STATE_DISABLED); else lv_obj_clear_state(ctx->btn_prev, LV_STATE_DISABLED); } if (ctx->btn_next) { - if (ctx->cur_global >= ctx->total_verses-1) lv_obj_add_state(ctx->btn_next, LV_STATE_DISABLED); + bool at_end = ctx->favorite_navigation_active + ? ctx->favorite_dial_idx >= ctx->favorite_count - 1 + : ctx->cur_global >= ctx->total_verses - 1; + if (at_end) lv_obj_add_state(ctx->btn_next, LV_STATE_DISABLED); else lv_obj_clear_state(ctx->btn_next, LV_STATE_DISABLED); } free(verse_buf); @@ -747,6 +804,28 @@ static void show_current_verse(AppCtx* ctx) { } static void advance_verse(AppCtx* ctx, int delta) { + if (ctx->favorite_navigation_active) { + load_favorites(ctx); + if (ctx->favorite_count <= 0) { + ctx->favorite_navigation_active = false; + return; + } + int current_pos = find_favorite_position(ctx, ctx->cur_global); + if (current_pos < 0) current_pos = ctx->favorite_dial_idx; + if (current_pos < 0) current_pos = 0; + if (current_pos >= ctx->favorite_count) current_pos = ctx->favorite_count - 1; + int next_pos = current_pos + delta; + if (next_pos < 0) next_pos = 0; + if (next_pos >= ctx->favorite_count) next_pos = ctx->favorite_count - 1; + ctx->favorite_dial_idx = next_pos; + int next_global = ctx->favorite_indices[next_pos]; + if (next_global != ctx->cur_global) { + ctx->cur_global = next_global; + show_current_verse(ctx); + } + return; + } + int ng = ctx->cur_global + delta; if (ng < 0) ng = 0; if (ng >= ctx->total_verses) ng = ctx->total_verses-1; @@ -756,6 +835,11 @@ static void advance_verse(AppCtx* ctx, int delta) { } } +static void manually_advance_verse(AppCtx* ctx, int delta) { + advance_verse(ctx, delta); + if (ctx->auto_timer) lv_timer_reset(ctx->auto_timer); +} + static void jump_to_global(AppCtx* ctx, int global_idx) { if (global_idx < 0) global_idx = 0; if (global_idx >= ctx->total_verses) global_idx = ctx->total_verses-1; @@ -769,6 +853,7 @@ static void jump_to_global(AppCtx* ctx, int global_idx) { static void jump_to_book(AppCtx* ctx, int book_idx) { if (book_idx < 0 || book_idx >= ctx->book_count) return; + ctx->favorite_navigation_active = false; // compute global start of book int global = 0; for (int i=0;ibooks[i].verse_count; @@ -779,7 +864,10 @@ static void jump_to_book(AppCtx* ctx, int book_idx) { static void on_auto_timer(lv_timer_t* t) { AppCtx* ctx = (AppCtx*)lv_timer_get_user_data(t); if (ctx->auto_paused) return; - if (ctx->cur_global < ctx->total_verses-1) { + bool has_next = ctx->favorite_navigation_active + ? ctx->favorite_dial_idx < ctx->favorite_count - 1 + : ctx->cur_global < ctx->total_verses - 1; + if (has_next) { advance_verse(ctx, 1); } else { ctx->auto_paused = true; @@ -803,6 +891,51 @@ static void book_fade_anim_cb(void* var, int32_t v) { static void book_dial_update_center(AppCtx* ctx) { if (!ctx->book_center_name) return; + if (ctx->browsing_favorites) { + if (ctx->favorite_count <= 0) { + lv_label_set_text(ctx->book_center_name, "No favorites yet"); + lv_label_set_text(ctx->book_center_verses, "Mark a verse with the check button, then return here."); + lv_label_set_text(ctx->book_center_num, "0 / 0"); + lv_slider_set_range(ctx->book_slider, 0, 0); + lv_slider_set_value(ctx->book_slider, 0, LV_ANIM_OFF); + lv_obj_set_style_text_color(ctx->book_center_name, lv_color_hex(0xF5C16C), 0); + return; + } + + if (ctx->favorite_dial_idx < 0) ctx->favorite_dial_idx = 0; + if (ctx->favorite_dial_idx >= ctx->favorite_count) ctx->favorite_dial_idx = ctx->favorite_count - 1; + int global_idx = ctx->favorite_indices[ctx->favorite_dial_idx]; + int remaining = global_idx; + int book_idx = 0; + while (book_idx < ctx->book_count - 1 && + remaining >= ctx->books[book_idx].verse_count) { + remaining -= ctx->books[book_idx].verse_count; + book_idx++; + } + + const char* verse_text = "Unable to load verse"; + char ref[64]; + snprintf(ref, sizeof(ref), "%s", ctx->books[book_idx].bname); + if (load_book(ctx, ctx->books[book_idx].bnumber) && + remaining >= 0 && remaining < ctx->book_idx_count) { + VerseIndex* vi = &ctx->book_idx[remaining]; + snprintf(ref, sizeof(ref), "%s %d:%d", ctx->books[book_idx].bname, vi->cnum, vi->vnum); + if (vi->offset < ctx->book_bin_size) { + verse_text = (const char*)(ctx->book_bin + vi->offset); + } + } + + lv_label_set_text(ctx->book_center_name, ref); + lv_label_set_text(ctx->book_center_verses, verse_text); + char count[24]; + snprintf(count, sizeof(count), "%d / %d", ctx->favorite_dial_idx + 1, ctx->favorite_count); + lv_label_set_text(ctx->book_center_num, count); + lv_slider_set_range(ctx->book_slider, 0, ctx->favorite_count - 1); + lv_slider_set_value(ctx->book_slider, ctx->favorite_dial_idx, LV_ANIM_OFF); + lv_obj_set_style_text_color(ctx->book_center_name, lv_color_hex(0xF5C16C), 0); + return; + } + if (ctx->book_dial_idx <0) ctx->book_dial_idx=0; if (ctx->book_dial_idx >= ctx->book_count) ctx->book_dial_idx = ctx->book_count-1; BookInfo* b = &ctx->books[ctx->book_dial_idx]; @@ -835,6 +968,7 @@ static void book_dial_update_center(AppCtx* ctx) { lv_obj_set_style_text_color(ctx->book_center_name, lv_color_hex(col), 0); if (ctx->book_slider) { + lv_slider_set_range(ctx->book_slider, 0, ctx->book_count>0 ? ctx->book_count-1 : 0); // avoid recursive event loop - set without anim, but we add subtle anim for drag lv_slider_set_value(ctx->book_slider, ctx->book_dial_idx, LV_ANIM_OFF); } @@ -843,7 +977,13 @@ static void book_dial_update_center(AppCtx* ctx) { static void set_book_browser_visible(AppCtx* ctx, bool visible) { ctx->book_browser_visible = visible; if (visible) { + ctx->browsing_favorites = false; ctx->book_dial_idx = ctx->cur_book_idx; + load_favorites(ctx); + if (ctx->btn_favorites) { + lv_obj_set_style_bg_color(ctx->btn_favorites, lv_color_hex(0x252538), 0); + lv_obj_set_style_text_color(ctx->btn_favorites, lv_color_hex(0xC4C4D4), 0); + } book_dial_update_center(ctx); if (ctx->book_browser) { lv_obj_remove_flag(ctx->book_browser, LV_OBJ_FLAG_HIDDEN); @@ -890,20 +1030,65 @@ static void on_books_close_click(lv_event_t* e) { } static void on_book_center_click(lv_event_t* e) { AppCtx* ctx = (AppCtx*)lv_event_get_user_data(e); - jump_to_book(ctx, ctx->book_dial_idx); + if (ctx->browsing_favorites) { + if (ctx->favorite_count > 0) { + jump_to_global(ctx, ctx->favorite_indices[ctx->favorite_dial_idx]); + } + } else { + jump_to_book(ctx, ctx->book_dial_idx); + } } static void on_dial_prev_click(lv_event_t* e) { AppCtx* ctx = (AppCtx*)lv_event_get_user_data(e); - if (ctx->book_dial_idx>0) { ctx->book_dial_idx--; book_dial_update_center(ctx); } + if (ctx->browsing_favorites) { + if (ctx->favorite_dial_idx > 0) { + ctx->favorite_dial_idx--; + book_dial_update_center(ctx); + } + } else if (ctx->book_dial_idx>0) { + ctx->book_dial_idx--; + book_dial_update_center(ctx); + } } static void on_dial_next_click(lv_event_t* e) { AppCtx* ctx = (AppCtx*)lv_event_get_user_data(e); - if (ctx->book_dial_idx < ctx->book_count-1) { ctx->book_dial_idx++; book_dial_update_center(ctx); } + if (ctx->browsing_favorites) { + if (ctx->favorite_dial_idx < ctx->favorite_count - 1) { + ctx->favorite_dial_idx++; + book_dial_update_center(ctx); + } + } else if (ctx->book_dial_idx < ctx->book_count-1) { + ctx->book_dial_idx++; + book_dial_update_center(ctx); + } } static void on_slider_changed(lv_event_t* e) { AppCtx* ctx = (AppCtx*)lv_event_get_user_data(e); int v = lv_slider_get_value(ctx->book_slider); - if (v!=ctx->book_dial_idx) { ctx->book_dial_idx=v; book_dial_update_center(ctx); } + if (ctx->browsing_favorites) { + if (v != ctx->favorite_dial_idx) { + ctx->favorite_dial_idx = v; + book_dial_update_center(ctx); + } + } else if (v!=ctx->book_dial_idx) { + ctx->book_dial_idx=v; + book_dial_update_center(ctx); + } +} +static void on_favorites_click(lv_event_t* e) { + AppCtx* ctx = (AppCtx*)lv_event_get_user_data(e); + load_favorites(ctx); + if (ctx->favorite_count <= 0) { + ctx->browsing_favorites = true; + book_dial_update_center(ctx); + return; + } + + int current_pos = find_favorite_position(ctx, ctx->cur_global); + ctx->favorite_dial_idx = current_pos >= 0 ? current_pos : 0; + ctx->favorite_navigation_active = true; + ctx->browsing_favorites = false; + jump_to_global(ctx, ctx->favorite_indices[ctx->favorite_dial_idx]); } static void on_swipe_area_event(lv_event_t* e) { AppCtx* ctx = (AppCtx*)lv_event_get_user_data(e); @@ -919,11 +1104,15 @@ static void on_swipe_area_event(lv_event_t* e) { int thresh = 28; // px per book if (abs(dx) >= thresh) { int steps = dx / thresh; - int new_idx = ctx->book_dial_idx - steps; // swipe left -> next + int old_idx = ctx->browsing_favorites ? ctx->favorite_dial_idx : ctx->book_dial_idx; + int max_idx = ctx->browsing_favorites ? ctx->favorite_count - 1 : ctx->book_count - 1; + if (max_idx < 0) max_idx = 0; + int new_idx = old_idx - steps; // swipe left -> next if (new_idx<0) new_idx=0; - if (new_idx>=ctx->book_count) new_idx=ctx->book_count-1; - if (new_idx!=ctx->book_dial_idx) { - ctx->book_dial_idx = new_idx; + if (new_idx>max_idx) new_idx=max_idx; + if (new_idx!=old_idx) { + if (ctx->browsing_favorites) ctx->favorite_dial_idx = new_idx; + else ctx->book_dial_idx = new_idx; book_dial_update_center(ctx); } ctx->book_drag_start_x = p.x; // reset for next step @@ -992,6 +1181,9 @@ static void build_book_browser(AppCtx* ctx, lv_obj_t* parent) { ctx->book_center_verses = lv_label_create(mid_wrap); lv_label_set_text(ctx->book_center_verses, "—"); + lv_obj_set_width(ctx->book_center_verses, LV_PCT(100)); + lv_obj_set_height(ctx->book_center_verses, 72); + lv_label_set_long_mode(ctx->book_center_verses, LV_LABEL_LONG_MODE_DOTS); lv_obj_set_style_text_color(ctx->book_center_verses, lv_color_hex(0x5A5A78), 0); lv_obj_set_style_text_font(ctx->book_center_verses, lvgl_get_text_font(FONT_SIZE_SMALL), 0); lv_obj_set_style_text_align(ctx->book_center_verses, LV_TEXT_ALIGN_CENTER, 0); @@ -1005,7 +1197,7 @@ static void build_book_browser(AppCtx* ctx, lv_obj_t* parent) { lv_obj_t* l_r = lv_label_create(btn_r); lv_label_set_text(l_r, LV_SYMBOL_NEXT); lv_obj_center(l_r); lv_obj_add_event_cb(btn_r, on_dial_next_click, LV_EVENT_CLICKED, ctx); - // Bottom bar: [X cancel] [slider] [N / 66] + // Bottom bar: [X cancel] [Fav] [slider] [N / 66] lv_obj_t* bottom = lv_obj_create(ctx->book_browser); lv_obj_set_size(bottom, LV_PCT(100), 56); lv_obj_align(bottom, LV_ALIGN_BOTTOM_MID, 0, 0); @@ -1029,6 +1221,17 @@ static void build_book_browser(AppCtx* ctx, lv_obj_t* parent) { lv_obj_t* l_cancel = lv_label_create(btn_cancel); lv_label_set_text(l_cancel, LV_SYMBOL_CLOSE); lv_obj_center(l_cancel); lv_obj_add_event_cb(btn_cancel, on_books_close_click, LV_EVENT_CLICKED, ctx); + ctx->btn_favorites = lv_button_create(bottom); + lv_obj_set_size(ctx->btn_favorites, 44, 32); + lv_obj_set_style_radius(ctx->btn_favorites, 8, 0); + lv_obj_set_style_bg_color(ctx->btn_favorites, lv_color_hex(0x252538), 0); + lv_obj_set_style_text_color(ctx->btn_favorites, lv_color_hex(0xC4C4D4), 0); + lv_obj_t* l_favorites = lv_label_create(ctx->btn_favorites); + lv_label_set_text(l_favorites, "Fav"); + lv_obj_set_style_text_font(l_favorites, lvgl_get_text_font(FONT_SIZE_SMALL), 0); + lv_obj_center(l_favorites); + lv_obj_add_event_cb(ctx->btn_favorites, on_favorites_click, LV_EVENT_CLICKED, ctx); + ctx->book_slider = lv_slider_create(bottom); lv_obj_set_flex_grow(ctx->book_slider, 1); lv_obj_set_height(ctx->book_slider, 8); @@ -1052,8 +1255,35 @@ static void build_book_browser(AppCtx* ctx, lv_obj_t* parent) { } /* ── UI callbacks ── */ +static void on_verse_pointer_event(lv_event_t* e) { + AppCtx* ctx = (AppCtx*)lv_event_get_user_data(e); + lv_event_code_t code = lv_event_get_code(e); + lv_indev_t* indev = lv_indev_active(); + if (!indev) return; + lv_point_t p; + lv_indev_get_point(indev, &p); + if (code == LV_EVENT_PRESSED) { + ctx->verse_drag_start_x = p.x; + ctx->verse_drag_start_y = p.y; + ctx->verse_dragging = true; + ctx->verse_swipe_consumed = false; + } else if ((code == LV_EVENT_RELEASED || code == LV_EVENT_PRESS_LOST) && ctx->verse_dragging) { + int dx = p.x - ctx->verse_drag_start_x; + int dy = p.y - ctx->verse_drag_start_y; + ctx->verse_dragging = false; + if (abs(dx) >= 40 && abs(dx) > abs(dy)) { + ctx->verse_swipe_consumed = true; + manually_advance_verse(ctx, dx < 0 ? 1 : -1); + } + } +} + static void on_verse_tap(lv_event_t* e) { AppCtx* ctx = (AppCtx*)lv_event_get_user_data(e); + if (ctx->verse_swipe_consumed) { + ctx->verse_swipe_consumed = false; + return; + } toggle_ui(ctx); } @@ -1103,11 +1333,11 @@ static void set_chrome_visible(AppCtx* ctx, bool visible) { static void on_prev_click(lv_event_t* e) { AppCtx* ctx = (AppCtx*)lv_event_get_user_data(e); - advance_verse(ctx, -1); + manually_advance_verse(ctx, -1); } static void on_next_click(lv_event_t* e) { AppCtx* ctx = (AppCtx*)lv_event_get_user_data(e); - advance_verse(ctx, 1); + manually_advance_verse(ctx, 1); } static void on_play_pause_click(lv_event_t* e) { AppCtx* ctx = (AppCtx*)lv_event_get_user_data(e); @@ -1120,6 +1350,17 @@ static void on_play_pause_click(lv_event_t* e) { static void on_highlight_click(lv_event_t* e) { AppCtx* ctx = (AppCtx*)lv_event_get_user_data(e); toggle_favorite(ctx); + if (ctx->favorite_navigation_active && !ctx->is_highlighted) { + if (ctx->favorite_count <= 0) { + ctx->favorite_navigation_active = false; + } else { + if (ctx->favorite_dial_idx >= ctx->favorite_count) { + ctx->favorite_dial_idx = ctx->favorite_count - 1; + } + ctx->cur_global = ctx->favorite_indices[ctx->favorite_dial_idx]; + if (ctx->auto_timer) lv_timer_reset(ctx->auto_timer); + } + } // refresh highlight visuals without reloading show_current_verse(ctx); } @@ -1198,6 +1439,9 @@ static void onShowApp(AppHandle app, void* data, lv_obj_t* parent) { lv_obj_set_style_pad_all(g_ctx.verse_wrapper, 0, 0); lv_obj_remove_flag(g_ctx.verse_wrapper, LV_OBJ_FLAG_SCROLLABLE); lv_obj_add_flag(g_ctx.verse_wrapper, LV_OBJ_FLAG_CLICKABLE); + lv_obj_add_event_cb(g_ctx.verse_wrapper, on_verse_pointer_event, LV_EVENT_PRESSED, &g_ctx); + lv_obj_add_event_cb(g_ctx.verse_wrapper, on_verse_pointer_event, LV_EVENT_RELEASED, &g_ctx); + lv_obj_add_event_cb(g_ctx.verse_wrapper, on_verse_pointer_event, LV_EVENT_PRESS_LOST, &g_ctx); lv_obj_add_event_cb(g_ctx.verse_wrapper, on_verse_tap, LV_EVENT_CLICKED, &g_ctx); // Center reading column with generous breathing room - avoid top chrome crop @@ -1213,6 +1457,9 @@ static void onShowApp(AppHandle app, void* data, lv_obj_t* parent) { lv_obj_set_style_pad_all(center_col, 0, 0); lv_obj_remove_flag(center_col, LV_OBJ_FLAG_SCROLLABLE); lv_obj_add_flag(center_col, LV_OBJ_FLAG_CLICKABLE); + lv_obj_add_event_cb(center_col, on_verse_pointer_event, LV_EVENT_PRESSED, &g_ctx); + lv_obj_add_event_cb(center_col, on_verse_pointer_event, LV_EVENT_RELEASED, &g_ctx); + lv_obj_add_event_cb(center_col, on_verse_pointer_event, LV_EVENT_PRESS_LOST, &g_ctx); lv_obj_add_event_cb(center_col, on_verse_tap, LV_EVENT_CLICKED, &g_ctx); g_ctx.lbl_verse = lv_label_create(center_col); @@ -1324,7 +1571,7 @@ static void onShowApp(AppHandle app, void* data, lv_obj_t* parent) { lv_obj_set_style_radius(g_ctx.btn_highlight, 10, 0); lv_obj_set_style_bg_color(g_ctx.btn_highlight, lv_color_hex(0x252538), 0); lv_obj_set_style_text_color(g_ctx.btn_highlight, lv_color_hex(0x8C8CB0), 0); - lbl = lv_label_create(g_ctx.btn_highlight); lv_label_set_text(lbl, LV_SYMBOL_DUMMY); lv_obj_center(lbl); + lbl = lv_label_create(g_ctx.btn_highlight); lv_label_set_text(lbl, LV_SYMBOL_PLUS); lv_obj_center(lbl); lv_obj_add_event_cb(g_ctx.btn_highlight, on_highlight_click, LV_EVENT_CLICKED, &g_ctx); set_chrome_visible(&g_ctx, true); // start with menu showing per user request diff --git a/Apps/SdDownloader/main/Source/main.c b/Apps/SdDownloader/main/Source/main.c index 8db5bc5..b72dd65 100644 --- a/Apps/SdDownloader/main/Source/main.c +++ b/Apps/SdDownloader/main/Source/main.c @@ -21,6 +21,7 @@ #define DEFAULT_PATH "/sdcard/download/test2.mp3" typedef struct { + lv_obj_t *root_obj; lv_obj_t *bar; lv_obj_t *lbl_url; lv_obj_t *lbl_path; @@ -37,6 +38,7 @@ typedef struct { TaskHandle_t dl_handle; volatile bool downloading; volatile bool cancel_req; + volatile bool task_exited; volatile int total; volatile int expected; int last_pct; @@ -82,13 +84,18 @@ static void ensure_parent_dir(const char *filepath) { } } +#define DL_DOWNLOADED 0 +#define DL_EXISTED 1 +#define DL_FAILED -1 + static void update_ui_status(const char *txt) { tt_lvgl_lock(portMAX_DELAY); if (G.lbl_status) lv_label_set_text(G.lbl_status, txt); tt_lvgl_unlock(); } -static void set_result_and_close(bool success) { +static void set_result_and_close(int dl_res) { + bool success = (dl_res != DL_FAILED); G.result_success = success; size_t free_internal = heap_caps_get_free_size(MALLOC_CAP_INTERNAL); BundleHandle result_bundle = NULL; @@ -97,6 +104,13 @@ static void set_result_and_close(bool success) { } if (result_bundle) { tt_bundle_put_bool(result_bundle, "success", success); + if (dl_res == DL_DOWNLOADED) { + tt_bundle_put_string(result_bundle, "status", "downloaded"); + } else if (dl_res == DL_EXISTED) { + tt_bundle_put_string(result_bundle, "status", "existed"); + } else { + tt_bundle_put_string(result_bundle, "status", "failure"); + } // Keep bundle minimal to avoid low heap crash (previous put_string path 512B caused OOM) tt_app_set_result(G.app_handle, success ? APP_RESULT_OK : APP_RESULT_ERROR, result_bundle); } else { @@ -106,16 +120,9 @@ static void set_result_and_close(bool success) { } tt_app_set_result(G.app_handle, success ? APP_RESULT_OK : APP_RESULT_ERROR, NULL); } - // Change Cancel button to Close once done (manual close, auto-close unreliable) - tt_lvgl_lock(portMAX_DELAY); - if (G.btn_cancel) { - lv_obj_t* lbl = lv_obj_get_child(G.btn_cancel, 0); - if (lbl) lv_label_set_text(lbl, "Close"); - } - tt_lvgl_unlock(); } -static bool download_one_file(const char *url, const char *path) { +static int download_one_file(const char *url, const char *path) { ESP_LOGI(TAG, "DL one file url=%s path=%s override=%d", url, path, G.override_existing); struct stat st; @@ -124,7 +131,7 @@ static bool download_one_file(const char *url, const char *path) { ESP_LOGW(TAG, "File exists and override=false: %s (returning true as cached)", path); G.total = st.st_size; update_ui_status("Already on SD"); - return true; + return DL_EXISTED; } else { ESP_LOGI(TAG, "File exists but override=true, overwriting %s", path); unlink(path); @@ -141,7 +148,7 @@ static bool download_one_file(const char *url, const char *path) { if (strlen(url) < 8 || strncmp(url, "http://", 7)!=0) { update_ui_status("Invalid URL"); - return false; + return DL_FAILED; } char tmp_path[520]; @@ -151,7 +158,7 @@ static bool download_one_file(const char *url, const char *path) { if (!f) { ESP_LOGE(TAG, "fopen fail %s errno=%d", tmp_path, errno); update_ui_status("Failed to open file"); - return false; + return DL_FAILED; } esp_http_client_config_t cfg = { @@ -166,7 +173,7 @@ static bool download_one_file(const char *url, const char *path) { fclose(f); unlink(tmp_path); update_ui_status("HTTP init failed"); - return false; + return DL_FAILED; } esp_err_t err = esp_http_client_open(client, 0); @@ -175,7 +182,7 @@ static bool download_one_file(const char *url, const char *path) { fclose(f); unlink(tmp_path); update_ui_status("HTTP open failed"); - return false; + return DL_FAILED; } int content_len = esp_http_client_fetch_headers(client); @@ -189,7 +196,7 @@ static bool download_one_file(const char *url, const char *path) { esp_http_client_cleanup(client); fclose(f); unlink(tmp_path); - return false; + return DL_FAILED; } G.expected = content_len; @@ -203,7 +210,7 @@ static bool download_one_file(const char *url, const char *path) { fclose(f); unlink(tmp_path); update_ui_status("No memory"); - return false; + return DL_FAILED; } } @@ -264,18 +271,19 @@ static bool download_one_file(const char *url, const char *path) { esp_http_client_cleanup(client); fclose(f); - if (G.cancel_req) { unlink(tmp_path); update_ui_status("Canceled"); return false; } - if (!ok || total < 512) { unlink(tmp_path); update_ui_status("Failed – error"); return false; } - if (content_len >=0 && total < content_len * 95 / 100) { unlink(tmp_path); update_ui_status("Failed – incomplete"); return false; } - if (rename(tmp_path, path) != 0) { unlink(tmp_path); update_ui_status("Rename failed"); return false; } + if (G.cancel_req) { unlink(tmp_path); update_ui_status("Canceled"); return DL_FAILED; } + if (!ok || total < 512) { unlink(tmp_path); update_ui_status("Failed – error"); return DL_FAILED; } + if (content_len >=0 && total < content_len * 95 / 100) { unlink(tmp_path); update_ui_status("Failed – incomplete"); return DL_FAILED; } + if (rename(tmp_path, path) != 0) { unlink(tmp_path); update_ui_status("Rename failed"); return DL_FAILED; } - return true; + return DL_DOWNLOADED; } static void download_task(void *arg) { (void)arg; G.downloading = true; G.cancel_req = false; + G.task_exited = false; G.last_pct = -1; char url[512]; @@ -285,35 +293,48 @@ static void download_task(void *arg) { url[sizeof(url)-1]=0; path[sizeof(path)-1]=0; - bool ok = download_one_file(url, path); - if (ok) { + int dl_res = download_one_file(url, path); + if (dl_res == DL_DOWNLOADED || dl_res == DL_EXISTED) { char msg[64]; - snprintf(msg, sizeof(msg), "Done! %d KB", G.total/1024); + if (dl_res == DL_EXISTED) snprintf(msg, sizeof(msg), "Already on SD (%d KB)", G.total/1024); + else snprintf(msg, sizeof(msg), "Done! %d KB", G.total/1024); update_ui_status(msg); } + tt_lvgl_lock(portMAX_DELAY); + if (G.btn_cancel) { + lv_obj_t* lbl = lv_obj_get_child(G.btn_cancel, 0); + if (lbl) lv_label_set_text(lbl, "Close"); + } + tt_lvgl_unlock(); + G.downloading = false; + set_result_and_close(dl_res); + G.task_exited = true; G.dl_handle = NULL; - set_result_and_close(ok); vTaskDelete(NULL); } static void btn_cancel_cb(lv_event_t *e) { (void)e; + ESP_LOGI(TAG, "btn_cancel_cb clicked! downloading=%d", G.downloading); + if (G.btn_cancel) { + lv_obj_remove_flag(G.btn_cancel, LV_OBJ_FLAG_CLICKABLE); + } + if (!G.downloading) { tt_app_stop(); return; } G.cancel_req = true; - tt_lvgl_lock(portMAX_DELAY); if (G.lbl_status) lv_label_set_text(G.lbl_status, "Canceling..."); - tt_lvgl_unlock(); } static void build_ui(void) { tt_lvgl_lock(portMAX_DELAY); lv_obj_t *root = lv_obj_create(lv_scr_act()); + G.root_obj = root; lv_obj_set_size(root, LV_PCT(100), LV_PCT(100)); lv_obj_set_style_pad_all(root, 8, 0); lv_obj_set_style_bg_color(root, lv_color_hex(0x111111), 0); @@ -448,9 +469,31 @@ static void onHide(AppHandle app, void* data) { (void)app; (void)data; if (G.dl_handle) { G.cancel_req = true; - for (int i=0;i<20 && G.dl_handle!=NULL; i++) vTaskDelay(pdMS_TO_TICKS(100)); - if (G.dl_handle) { vTaskDelete(G.dl_handle); G.dl_handle = NULL; G.downloading = false; } + for (int i = 0; i < 30 && !G.task_exited; i++) { + vTaskDelay(pdMS_TO_TICKS(100)); + } + if (!G.task_exited && G.dl_handle) { + ESP_LOGW(TAG, "onHide: force deleting dl task after timeout"); + vTaskDelete(G.dl_handle); + G.dl_handle = NULL; + G.downloading = false; + } + vTaskDelay(pdMS_TO_TICKS(50)); } + + tt_lvgl_lock(portMAX_DELAY); + if (G.root_obj) { + lv_obj_delete(G.root_obj); + G.root_obj = NULL; + G.lbl_url = NULL; + G.lbl_path = NULL; + G.lbl_pct = NULL; + G.lbl_detail = NULL; + G.lbl_status = NULL; + G.bar = NULL; + G.btn_cancel = NULL; + } + tt_lvgl_unlock(); } int main(int argc, char* argv[]) {