latest change on laptop Bible and SDDownload

This commit is contained in:
Adolfo
2026-09-11 10:51:52 -04:00
parent 74fe10f130
commit e0c5cbc1a0
2 changed files with 338 additions and 48 deletions
+266 -19
View File
@@ -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 (cnt<MAX_FAVORITES && fscanf(f, "%d", &vals[cnt])==1) cnt++;
fclose(f);
}
// check existence
@@ -542,7 +577,7 @@ static void toggle_favorite(AppCtx* ctx) {
cnt--;
ctx->is_highlighted=false;
} else {
if (cnt<512) {
if (cnt<MAX_FAVORITES) {
vals[cnt++]=ctx->cur_global;
ctx->is_highlighted=true;
}
@@ -552,6 +587,7 @@ static void toggle_favorite(AppCtx* ctx) {
if (!f) return;
for (int i=0;i<cnt;i++) fprintf(f, "%d\n", vals[i]);
fclose(f);
load_favorites(ctx);
}
/* ── UI logic ── */
@@ -709,9 +745,24 @@ static void show_current_verse(AppCtx* ctx) {
}
ctx->is_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;i<book_idx;i++) global += ctx->books[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
+72 -29
View File
@@ -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[]) {