feat(bibleverse): editorial v6 - Georgia Italic serif pretty font, 15px lift, menu auto-show 10s, animated book picker, no black bar
Main / Build (BookPlayer) (push) Has been cancelled
Main / Build (Brainfuck) (push) Has been cancelled
Main / Build (Breakout) (push) Has been cancelled
Main / Build (Calculator) (push) Has been cancelled
Main / Build (Diceware) (push) Has been cancelled
Main / Build (EpubReader) (push) Has been cancelled
Main / Build (GPIO) (push) Has been cancelled
Main / Build (GraphicsDemo) (push) Has been cancelled
Main / Build (HelloWorld) (push) Has been cancelled
Main / Build (M5UnitTest) (push) Has been cancelled
Main / Build (Magic8Ball) (push) Has been cancelled
Main / Build (MediaKeys) (push) Has been cancelled
Main / Build (MystifyDemo) (push) Has been cancelled
Main / Build (SerialConsole) (push) Has been cancelled
Main / Build (Snake) (push) Has been cancelled
Main / Build (TamaTac) (push) Has been cancelled
Main / Build (TodoList) (push) Has been cancelled
Main / Build (TwoEleven) (push) Has been cancelled
Main / Bundle (push) Has been cancelled

- Pretty font: embedded LVGL C Georgia Italic 26/22/20/18/16 + regular 24 (lv_font_conv --lv-include lvgl.h) ~368KB src ~315KB PSRAM via CONFIG_ELF_LOADER_LOAD_PSRAM=y, heap 47KB stable on 192.168.68.129 0.8.0-dev
- Downtier for readability: LARGE->22 (was 26), DEFAULT->18, SMALL->16, ultra <40c still 26, pads tighter 6/4/3/2/1
- Black bar fix: header 36px bg 0x0E0E14 TRANSP not 15151F COVER, border 0, ctrl TRANSP, vision 'no extra black bar overlapping'
- Lift 15px up: center ALIGN_CENTER 0,-7 (was -22 30px, user corrected), balanced optical center, reference gap 10
- Menu auto-show: start set_chrome_visible(true) + chrome_auto_hide_timer 10000ms via lv_timer_get_user_data + toggle_ui re-arms, onHide cleanup
- Book picker animation: book_y_anim_cb y 20->0 260ms ease_out + fade 20->COVER 220ms pop, name fade 60->255 on dial change, swipe 28px + slider fast scrub, [X left][slider flex][39/66 right], no card/no Go/no top bar, LARGE pretty name 50% bigger tappable
- Fade transition verse 220ms ref 300ms + uppercase ref tracking 2 #9A9AA8 + warmer #F2F0E8
- Compat: no lv_arc_create (0.7 fleet), 0 missing LVGL syms undef=0, build 4.3M
- Skill docs: SKILL.md v6 black-bar+downtier+auto-hide+anim, pretty-font.md v6, immersive-ui.md v6

Screenshots: Zephaniah 1:3 8683 bytes menu visible (top ref + bottom pills), after 6882 immersive, Habakkuk 2:9 no crop vision PASS
Co-Authored-By: internal-model
This commit is contained in:
Adolfo Reyna
2026-07-12 12:07:04 -04:00
parent 9773a78f6b
commit 214287193e
13 changed files with 9321 additions and 69 deletions
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+502 -69
View File
@@ -5,6 +5,7 @@
#include <string.h>
#include <stdlib.h>
#include <stdio.h>
#include <ctype.h>
#include <sys/stat.h>
#include <dirent.h>
@@ -12,6 +13,14 @@
#include "esp_log.h"
#include "tactility/lvgl_fonts.h"
// Embedded pretty serif fonts - Georgia Italic like reference white card
LV_FONT_DECLARE(georgia_italic_26)
LV_FONT_DECLARE(georgia_italic_22)
LV_FONT_DECLARE(georgia_italic_20)
LV_FONT_DECLARE(georgia_italic_18)
LV_FONT_DECLARE(georgia_italic_16)
LV_FONT_DECLARE(georgia_regular_24)
#define TAG "BibleVerse"
#define AUTO_ADV_MS 60000 // 1 minute
@@ -75,6 +84,7 @@ typedef struct {
// UI
lv_obj_t* root;
lv_obj_t* verse_wrapper; // full screen, clickable to toggle chrome
lv_obj_t* center_col; // editorial column for fade
lv_obj_t* lbl_verse;
lv_obj_t* lbl_ref; // small ref line below verse
lv_obj_t* header_bar;
@@ -86,9 +96,33 @@ typedef struct {
lv_obj_t* btn_play_pause;
lv_obj_t* btn_highlight;
lv_obj_t* btn_close;
lv_obj_t* btn_books;
// Pretty serif fonts (Georgia Italic .fnt bin)
// Will be linked as C arrays from assets/fonts via binfont -> replaced with embedded C now
// Keep pointers for compatibility
lv_font_t* font_serif_26;
lv_font_t* font_serif_22;
lv_font_t* font_serif_20;
lv_font_t* font_serif_18;
lv_font_t* font_serif_16;
lv_font_t* font_book_24;
bool fonts_loaded;
// Book browser - horizontal slide (0.7.0-dev compat, low RAM)
lv_obj_t* book_browser;
lv_obj_t* book_center_name;
lv_obj_t* book_center_num;
lv_obj_t* book_center_verses;
lv_obj_t* book_slider; // fast scrub
int book_dial_idx; // 0..book_count-1 currently selected
int book_drag_start_x;
bool book_dragging;
bool book_browser_visible;
// Timer
lv_timer_t* auto_timer;
lv_timer_t* chrome_auto_hide_timer; // menu auto-hide after 10s on start
bool auto_paused;
bool ui_visible; // chrome visible?
} AppCtx;
@@ -101,6 +135,8 @@ 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 jump_to_book(AppCtx* ctx, int book_idx);
static void jump_to_global(AppCtx* ctx, int global_idx);
static void save_progress(AppCtx* ctx);
static bool load_progress(AppCtx* ctx);
static void toggle_ui(AppCtx* ctx);
@@ -108,6 +144,9 @@ static void set_chrome_visible(AppCtx* ctx, bool visible);
static bool is_favorite(AppCtx* ctx, int global_idx);
static void toggle_favorite(AppCtx* ctx);
static void ensure_user_dir(AppCtx* ctx);
static void set_book_browser_visible(AppCtx* ctx, bool visible);
static void toggle_book_browser(AppCtx* ctx);
static void build_book_browser(AppCtx* ctx, lv_obj_t* parent);
/* ── Utilities ── */
static void ensure_user_dir(AppCtx* ctx) {
@@ -516,16 +555,31 @@ static void toggle_favorite(AppCtx* ctx) {
}
/* ── UI logic ── */
// Font resolution: pretty serif if loaded, else fallback to system
// Georgia Italic is wider/taller than Montserrat system -> map down one tier to avoid crop
static const lv_font_t* resolve_verse_font(AppCtx* ctx, enum LvglFontSize fallback_sz) {
if (!ctx->fonts_loaded) return lvgl_get_text_font(fallback_sz);
// Georgia 26 is visually larger than system LARGE, so use 22 for LARGE tier, 20 for DEFAULT, 16 for SMALL
if (fallback_sz == FONT_SIZE_LARGE && ctx->font_serif_22) return ctx->font_serif_22; // was 26, too big
if (fallback_sz == FONT_SIZE_DEFAULT && ctx->font_serif_18) return ctx->font_serif_18; // was 20
if (fallback_sz == FONT_SIZE_SMALL && ctx->font_serif_16) return ctx->font_serif_16;
// ultra-short special: still allow 26 for very tiny verses <40 chars
return lvgl_get_text_font(fallback_sz);
}
static const lv_font_t* resolve_verse_font_ultra(AppCtx* ctx) {
if (ctx->fonts_loaded && ctx->font_serif_26) return ctx->font_serif_26;
return lvgl_get_text_font(FONT_SIZE_LARGE);
}
static const lv_font_t* resolve_book_font(AppCtx* ctx) {
if (ctx->font_book_24) return ctx->font_book_24;
if (ctx->font_serif_22) return ctx->font_serif_22;
return lvgl_get_text_font(FONT_SIZE_LARGE);
}
static void apply_verse_scaling(AppCtx* ctx, const char* verse) {
if (!ctx->lbl_verse) return;
size_t len = strlen(verse);
// Tiers tuned from real device screenshots (240x320 style)
// < 40 : ultra short e.g. "Jesus wept." -> giant, centered, lots of breathing
// 40-100 : short -> large
// 100-200: medium -> default/large borderline
// 200-350: long -> small/default
// 350+ : very long (Esther 8:9 etc) -> extra compact
enum LvglFontSize font_sz;
bool is_ultra = false;
int line_space;
int pad_v;
int pad_h;
@@ -533,33 +587,35 @@ static void apply_verse_scaling(AppCtx* ctx, const char* verse) {
if (len < 40) {
font_sz = FONT_SIZE_LARGE;
line_space = 14;
pad_v = 32;
pad_h = 36;
letter_space = 1;
is_ultra = true;
line_space = 12;
pad_v = 6;
pad_h = 20;
letter_space = 0;
} else if (len < 100) {
font_sz = FONT_SIZE_LARGE;
line_space = 11;
pad_v = 24;
pad_h = 28;
line_space = 8;
pad_v = 4;
pad_h = 16;
} else if (len < 200) {
font_sz = FONT_SIZE_DEFAULT;
line_space = 9;
pad_v = 18;
pad_h = 22;
line_space = 6;
pad_v = 3;
pad_h = 14;
} else if (len < 350) {
font_sz = FONT_SIZE_SMALL;
line_space = 6;
pad_v = 12;
pad_h = 18;
line_space = 5;
pad_v = 2;
pad_h = 12;
} else {
font_sz = FONT_SIZE_SMALL;
line_space = 4;
pad_v = 8;
pad_h = 14;
pad_v = 1;
pad_h = 8;
}
lv_obj_set_style_text_font(ctx->lbl_verse, lvgl_get_text_font(font_sz), 0);
const lv_font_t* f = is_ultra ? resolve_verse_font_ultra(ctx) : resolve_verse_font(ctx, font_sz);
lv_obj_set_style_text_font(ctx->lbl_verse, f, 0);
lv_obj_set_style_text_line_space(ctx->lbl_verse, line_space, 0);
lv_obj_set_style_text_letter_space(ctx->lbl_verse, letter_space, 0);
lv_obj_set_style_pad_top(ctx->lbl_verse, pad_v, 0);
@@ -568,6 +624,34 @@ static void apply_verse_scaling(AppCtx* ctx, const char* verse) {
lv_obj_set_style_pad_right(ctx->lbl_verse, pad_h, 0);
}
// Fade transition for verse change
static void verse_fade_anim_cb(void* var, int32_t v) {
lv_obj_t* obj = (lv_obj_t*)var;
lv_obj_set_style_text_opa(obj, v, 0);
}
static void verse_fade_in(AppCtx* ctx) {
if (!ctx->lbl_verse || !ctx->center_col) return;
lv_anim_t a;
lv_anim_init(&a);
lv_anim_set_var(&a, ctx->lbl_verse);
lv_anim_set_values(&a, 40, 255);
lv_anim_set_duration(&a, 220);
lv_anim_set_exec_cb(&a, verse_fade_anim_cb);
lv_anim_set_path_cb(&a, lv_anim_path_ease_out);
lv_anim_start(&a);
// Also fade ref slightly delayed
if (ctx->lbl_ref) {
lv_anim_t b;
lv_anim_init(&b);
lv_anim_set_var(&b, ctx->lbl_ref);
lv_anim_set_values(&b, 30, 200);
lv_anim_set_duration(&b, 300);
lv_anim_set_exec_cb(&b, verse_fade_anim_cb);
lv_anim_set_path_cb(&b, lv_anim_path_ease_out);
lv_anim_start(&b);
}
}
static void show_current_verse(AppCtx* ctx) {
if (ctx->book_count==0) return;
global_to_book(ctx, ctx->cur_global);
@@ -595,14 +679,28 @@ static void show_current_verse(AppCtx* ctx) {
char ref_buf[96];
snprintf(ref_buf, sizeof(ref_buf), "%s %d:%d", ctx->books[ctx->cur_book_idx].bname, vi->cnum, vi->vnum);
char ref_pretty[96];
snprintf(ref_pretty, sizeof(ref_pretty), "— %s", ref_buf);
// Editorial reference style like your image: "PSALMS 23:1" with tracking
char ref_pretty[108];
// Use uppercase for book? Keep title for now but uppercase style for editorial match
// We'll format as "— PSALMS 23:1 —" feel? For dark mode keep minimal like ref image
// Reference image shows lines flanking citation - we do with letterspacing + dim
// For now: "PSALMS 23:1" style - uppercase via runtime? We'll uppercase book name
char bname_upper[48];
strncpy(bname_upper, ctx->books[ctx->cur_book_idx].bname, sizeof(bname_upper)-1);
bname_upper[sizeof(bname_upper)-1]='\0';
for (char* p=bname_upper; *p; ++p) *p = toupper((unsigned char)*p);
snprintf(ref_pretty, sizeof(ref_pretty), "%s %d:%d", bname_upper, vi->cnum, vi->vnum);
if (ctx->lbl_verse) {
// start faded for transition
lv_obj_set_style_text_opa(ctx->lbl_verse, 60, 0);
lv_label_set_text(ctx->lbl_verse, verse_buf);
apply_verse_scaling(ctx, verse_buf);
}
if (ctx->lbl_ref) lv_label_set_text(ctx->lbl_ref, ref_pretty);
if (ctx->lbl_ref) {
lv_obj_set_style_text_opa(ctx->lbl_ref, 40, 0);
lv_label_set_text(ctx->lbl_ref, ref_pretty);
}
if (ctx->lbl_book_title) lv_label_set_text(ctx->lbl_book_title, ref_buf);
if (ctx->lbl_position) {
char pos_buf[40];
@@ -644,20 +742,38 @@ static void show_current_verse(AppCtx* ctx) {
else lv_obj_clear_state(ctx->btn_next, LV_STATE_DISABLED);
}
save_progress(ctx);
verse_fade_in(ctx);
}
static void advance_verse(AppCtx* ctx, int delta) {
int ng = ctx->cur_global + delta;
if (ng < 0) ng = 0;
if (ng >= ctx->total_verses) ng = ctx->total_verses-1; // stop at end, or wrap? we stop
// Optionally wrap to start after Revelation
// if (ng >= ctx->total_verses) ng = 0;
if (ng >= ctx->total_verses) ng = ctx->total_verses-1;
if (ng != ctx->cur_global) {
ctx->cur_global = ng;
show_current_verse(ctx);
}
}
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;
ctx->cur_global = global_idx;
set_book_browser_visible(ctx, false);
set_chrome_visible(ctx, false);
show_current_verse(ctx);
// restart auto timer
if (ctx->auto_timer) lv_timer_reset(ctx->auto_timer);
}
static void jump_to_book(AppCtx* ctx, int book_idx) {
if (book_idx < 0 || book_idx >= ctx->book_count) return;
// compute global start of book
int global = 0;
for (int i=0;i<book_idx;i++) global += ctx->books[i].verse_count;
jump_to_global(ctx, global);
}
/* ── Timer ── */
static void on_auto_timer(lv_timer_t* t) {
AppCtx* ctx = (AppCtx*)lv_timer_get_user_data(t);
@@ -673,14 +789,304 @@ static void on_auto_timer(lv_timer_t* t) {
}
}
/* ── Book browser: horizontal slide, center name updates (arc-free) ── */
static void book_y_anim_cb(void* var, int32_t v) {
lv_obj_t* obj = (lv_obj_t*)var;
// v is y offset
lv_obj_set_y(obj, v);
}
static void book_fade_anim_cb(void* var, int32_t v) {
lv_obj_t* obj = (lv_obj_t*)var;
lv_obj_set_style_text_opa(obj, v, 0);
}
static void book_dial_update_center(AppCtx* ctx) {
if (!ctx->book_center_name) 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];
// Animate name change: quick fade (40->255)
lv_obj_set_style_text_opa(ctx->book_center_name, 100, 0);
{
lv_anim_t an; lv_anim_init(&an);
lv_anim_set_var(&an, ctx->book_center_name);
lv_anim_set_values(&an, 60, 255);
lv_anim_set_duration(&an, 180);
lv_anim_set_exec_cb(&an, book_fade_anim_cb);
lv_anim_set_path_cb(&an, lv_anim_path_ease_out);
lv_anim_start(&an);
}
lv_label_set_text(ctx->book_center_name, b->bname);
// Use pretty book font if loaded (50% larger already via LARGE vs DEFAULT fallback)
lv_obj_set_style_text_font(ctx->book_center_name, resolve_book_font(ctx), 0);
if (ctx->book_center_num) {
char nb[20]; snprintf(nb, sizeof(nb), "%2d / %d", b->bnumber, ctx->book_count);
lv_label_set_text(ctx->book_center_num, nb);
}
if (ctx->book_center_verses) {
char vb[24]; snprintf(vb, sizeof(vb), "%d verses", b->verse_count);
lv_label_set_text(ctx->book_center_verses, vb);
}
uint32_t col = (b->bnumber < 40) ? 0xE8E6E0 : 0x89B4FA;
lv_obj_set_style_text_color(ctx->book_center_name, lv_color_hex(col), 0);
if (ctx->book_slider) {
// 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);
}
}
static void set_book_browser_visible(AppCtx* ctx, bool visible) {
ctx->book_browser_visible = visible;
if (visible) {
ctx->book_dial_idx = ctx->cur_book_idx;
book_dial_update_center(ctx);
if (ctx->book_browser) {
lv_obj_remove_flag(ctx->book_browser, LV_OBJ_FLAG_HIDDEN);
// Anim: pop center mid_wrap y 20 -> 0, ease_out 260ms
if (ctx->book_center_name) {
lv_obj_t* parent = lv_obj_get_parent(ctx->book_center_name); // mid_wrap
lv_obj_set_y(parent, 20);
lv_anim_t ay; lv_anim_init(&ay);
lv_anim_set_var(&ay, parent);
lv_anim_set_values(&ay, 20, 0);
lv_anim_set_duration(&ay, 260);
lv_anim_set_exec_cb(&ay, book_y_anim_cb);
lv_anim_set_path_cb(&ay, lv_anim_path_ease_out);
lv_anim_start(&ay);
}
// Fade name
if (ctx->book_center_name) {
lv_obj_set_style_text_opa(ctx->book_center_name, LV_OPA_20, 0);
lv_anim_t af; lv_anim_init(&af);
lv_anim_set_var(&af, ctx->book_center_name);
lv_anim_set_values(&af, LV_OPA_20, LV_OPA_COVER);
lv_anim_set_duration(&af, 220);
lv_anim_set_exec_cb(&af, book_fade_anim_cb);
lv_anim_set_path_cb(&af, lv_anim_path_ease_out);
lv_anim_start(&af);
}
}
} else {
if (ctx->book_browser) lv_obj_add_flag(ctx->book_browser, LV_OBJ_FLAG_HIDDEN);
}
}
static void toggle_book_browser(AppCtx* ctx) {
// reset chrome auto-hide when opening browser
if (ctx->chrome_auto_hide_timer) {
lv_timer_delete(ctx->chrome_auto_hide_timer);
ctx->chrome_auto_hide_timer = NULL;
}
set_book_browser_visible(ctx, !ctx->book_browser_visible);
}
static void on_books_close_click(lv_event_t* e) {
AppCtx* ctx = (AppCtx*)lv_event_get_user_data(e);
set_book_browser_visible(ctx, false);
}
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);
}
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); }
}
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); }
}
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); }
}
static void on_swipe_area_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->book_drag_start_x = p.x;
ctx->book_dragging = true;
} else if (code==LV_EVENT_PRESSING && ctx->book_dragging) {
int dx = p.x - ctx->book_drag_start_x;
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
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;
book_dial_update_center(ctx);
}
ctx->book_drag_start_x = p.x; // reset for next step
}
} else if (code==LV_EVENT_RELEASED || code==LV_EVENT_PRESS_LOST) {
ctx->book_dragging=false;
}
}
static void build_book_browser(AppCtx* ctx, lv_obj_t* parent) {
ctx->book_browser = lv_obj_create(parent);
lv_obj_set_size(ctx->book_browser, LV_PCT(100), LV_PCT(100));
lv_obj_align(ctx->book_browser, LV_ALIGN_CENTER, 0, 0);
lv_obj_set_style_bg_color(ctx->book_browser, lv_color_hex(0x0E0E14), 0);
lv_obj_set_style_bg_opa(ctx->book_browser, LV_OPA_COVER, 0);
lv_obj_set_style_border_width(ctx->book_browser, 0, 0);
lv_obj_set_style_pad_all(ctx->book_browser, 0, 0);
lv_obj_set_style_radius(ctx->book_browser, 0, 0);
// Swipeable full-center area - no top bar
lv_obj_t* center_wrap = lv_obj_create(ctx->book_browser);
lv_obj_set_size(center_wrap, LV_PCT(100), LV_PCT(100));
lv_obj_set_style_pad_top(center_wrap, 0, 0);
lv_obj_set_style_pad_bottom(center_wrap, 64, 0);
lv_obj_set_style_bg_opa(center_wrap, LV_OPA_TRANSP, 0);
lv_obj_set_style_border_width(center_wrap, 0, 0);
lv_obj_remove_flag(center_wrap, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_align(center_wrap, LV_ALIGN_CENTER, 0, 0);
lv_obj_add_flag(center_wrap, LV_OBJ_FLAG_CLICKABLE);
lv_obj_add_event_cb(center_wrap, on_swipe_area_event, LV_EVENT_PRESSED, ctx);
lv_obj_add_event_cb(center_wrap, on_swipe_area_event, LV_EVENT_PRESSING, ctx);
lv_obj_add_event_cb(center_wrap, on_swipe_area_event, LV_EVENT_RELEASED, ctx);
lv_obj_add_event_cb(center_wrap, on_swipe_area_event, LV_EVENT_PRESS_LOST, ctx);
// Left arrow
lv_obj_t* btn_l = lv_button_create(center_wrap);
lv_obj_set_size(btn_l, 44, 44);
lv_obj_align(btn_l, LV_ALIGN_LEFT_MID, 8, 0);
lv_obj_set_style_radius(btn_l, 22, 0);
lv_obj_set_style_bg_color(btn_l, lv_color_hex(0x1E1E2E), 0);
lv_obj_t* l_l = lv_label_create(btn_l); lv_label_set_text(l_l, LV_SYMBOL_PREV); lv_obj_center(l_l);
lv_obj_add_event_cb(btn_l, on_dial_prev_click, LV_EVENT_CLICKED, ctx);
// Center — book name directly, 50% larger, tappable (no card)
lv_obj_t* mid_wrap = lv_obj_create(center_wrap);
lv_obj_set_size(mid_wrap, LV_PCT(68), LV_SIZE_CONTENT);
lv_obj_align(mid_wrap, LV_ALIGN_CENTER, 0, 0);
lv_obj_set_style_bg_opa(mid_wrap, LV_OPA_TRANSP, 0);
lv_obj_set_style_border_width(mid_wrap, 0, 0);
lv_obj_set_style_pad_all(mid_wrap, 0, 0);
lv_obj_set_style_pad_gap(mid_wrap, 10, 0);
lv_obj_set_flex_flow(mid_wrap, LV_FLEX_FLOW_COLUMN);
lv_obj_set_flex_align(mid_wrap, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
lv_obj_remove_flag(mid_wrap, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_add_flag(mid_wrap, LV_OBJ_FLAG_CLICKABLE);
lv_obj_add_event_cb(mid_wrap, on_book_center_click, LV_EVENT_CLICKED, ctx);
ctx->book_center_name = lv_label_create(mid_wrap);
lv_label_set_text(ctx->book_center_name, "Genesis");
lv_obj_set_style_text_color(ctx->book_center_name, lv_color_hex(0xE8E6E0), 0);
lv_obj_set_style_text_font(ctx->book_center_name, lvgl_get_text_font(FONT_SIZE_LARGE), 0); // 50% bigger: LARGE vs DEFAULT
lv_obj_set_style_text_letter_space(ctx->book_center_name, 0, 0);
lv_label_set_long_mode(ctx->book_center_name, LV_LABEL_LONG_MODE_DOTS);
lv_obj_set_width(ctx->book_center_name, LV_PCT(100));
lv_obj_set_style_text_align(ctx->book_center_name, LV_TEXT_ALIGN_CENTER, 0);
ctx->book_center_verses = lv_label_create(mid_wrap);
lv_label_set_text(ctx->book_center_verses, "");
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);
// Right arrow
lv_obj_t* btn_r = lv_button_create(center_wrap);
lv_obj_set_size(btn_r, 44, 44);
lv_obj_align(btn_r, LV_ALIGN_RIGHT_MID, -8, 0);
lv_obj_set_style_radius(btn_r, 22, 0);
lv_obj_set_style_bg_color(btn_r, lv_color_hex(0x1E1E2E), 0);
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]
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);
lv_obj_set_style_bg_color(bottom, lv_color_hex(0x15151F), 0);
lv_obj_set_style_bg_opa(bottom, LV_OPA_COVER, 0);
lv_obj_set_style_border_width(bottom, 1, 0);
lv_obj_set_style_border_side(bottom, LV_BORDER_SIDE_TOP, 0);
lv_obj_set_style_border_color(bottom, lv_color_hex(0x232338), 0);
lv_obj_set_style_radius(bottom, 0, 0);
lv_obj_set_style_pad_hor(bottom, 10, 0);
lv_obj_set_style_pad_ver(bottom, 8, 0);
lv_obj_set_style_pad_gap(bottom, 10, 0);
lv_obj_set_flex_flow(bottom, LV_FLEX_FLOW_ROW);
lv_obj_set_flex_align(bottom, LV_FLEX_ALIGN_SPACE_BETWEEN, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
lv_obj_remove_flag(bottom, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_t* btn_cancel = lv_button_create(bottom);
lv_obj_set_size(btn_cancel, 36, 32);
lv_obj_set_style_radius(btn_cancel, 8, 0);
lv_obj_set_style_bg_color(btn_cancel, lv_color_hex(0x252538), 0);
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->book_slider = lv_slider_create(bottom);
lv_obj_set_flex_grow(ctx->book_slider, 1);
lv_obj_set_height(ctx->book_slider, 8);
lv_slider_set_range(ctx->book_slider, 0, ctx->book_count>0 ? ctx->book_count-1 : 65);
lv_obj_set_style_bg_color(ctx->book_slider, lv_color_hex(0x232338), LV_PART_MAIN);
lv_obj_set_style_bg_color(ctx->book_slider, lv_color_hex(0x4A4A6E), LV_PART_INDICATOR);
lv_obj_set_style_bg_color(ctx->book_slider, lv_color_hex(0xE8E6E0), LV_PART_KNOB);
lv_obj_add_event_cb(ctx->book_slider, on_slider_changed, LV_EVENT_VALUE_CHANGED, ctx);
ctx->book_center_num = lv_label_create(bottom);
lv_label_set_text(ctx->book_center_num, "1 / 66");
lv_obj_set_style_text_color(ctx->book_center_num, lv_color_hex(0x8A8AAA), 0);
lv_obj_set_style_text_font(ctx->book_center_num, lvgl_get_text_font(FONT_SIZE_SMALL), 0);
lv_obj_set_style_pad_left(ctx->book_center_num, 4, 0);
lv_obj_add_flag(ctx->book_browser, LV_OBJ_FLAG_HIDDEN);
ctx->book_browser_visible = false;
ctx->book_dial_idx = 0;
ctx->book_drag_start_x = 0;
ctx->book_dragging = false;
}
/* ── UI callbacks ── */
static void on_verse_tap(lv_event_t* e) {
AppCtx* ctx = (AppCtx*)lv_event_get_user_data(e);
toggle_ui(ctx);
}
static void chrome_auto_hide_cb(lv_timer_t* t) {
AppCtx* ctx = (AppCtx*)lv_timer_get_user_data(t);
if (!ctx) return;
// Don't hide if book browser open
if (ctx->book_browser_visible) return;
if (ctx->ui_visible) set_chrome_visible(ctx, false);
if (ctx->chrome_auto_hide_timer) {
lv_timer_delete(ctx->chrome_auto_hide_timer);
ctx->chrome_auto_hide_timer = NULL;
}
}
static void schedule_chrome_auto_hide(AppCtx* ctx, uint32_t delay_ms) {
if (ctx->chrome_auto_hide_timer) {
lv_timer_delete(ctx->chrome_auto_hide_timer);
ctx->chrome_auto_hide_timer = NULL;
}
ctx->chrome_auto_hide_timer = lv_timer_create(chrome_auto_hide_cb, delay_ms, ctx);
}
static void toggle_ui(AppCtx* ctx) {
set_chrome_visible(ctx, !ctx->ui_visible);
bool new_vis = !ctx->ui_visible;
set_chrome_visible(ctx, new_vis);
if (new_vis) {
schedule_chrome_auto_hide(ctx, 10000); // hide again after 10s if user shows via tap
} else {
if (ctx->chrome_auto_hide_timer) {
lv_timer_delete(ctx->chrome_auto_hide_timer);
ctx->chrome_auto_hide_timer = NULL;
}
}
}
static void set_chrome_visible(AppCtx* ctx, bool visible) {
@@ -721,12 +1127,16 @@ static void on_close_click(lv_event_t* e) {
(void)ctx;
tt_app_stop();
}
// Future hooks
static void on_memo_click(lv_event_t* e) {
// Placeholder: in future record voice memo to /sdcard/bible_memos/<global>.wav
// Future hooks merged into books click
static void on_books_click(lv_event_t* e) {
AppCtx* ctx = (AppCtx*)lv_event_get_user_data(e);
const char* btns[] = {"OK"};
tt_app_alertdialog_start("Voice Memo", "Voice memo recording will be added in a future version.\nFile will be saved under /sdcard/bible_memos/", btns, 1);
toggle_book_browser(ctx);
}
static void on_title_click(lv_event_t* e) {
AppCtx* ctx = (AppCtx*)lv_event_get_user_data(e);
// Tapping title when chrome visible opens books
if (ctx->ui_visible) toggle_book_browser(ctx);
}
/* ── App lifecycle ── */
@@ -743,6 +1153,21 @@ static void onShowApp(AppHandle app, void* data, lv_obj_t* parent) {
snprintf(g_ctx.progress_path, sizeof(g_ctx.progress_path), "%s/progress.txt", ud_path);
snprintf(g_ctx.favorites_path, sizeof(g_ctx.favorites_path), "%s/favorites.txt", ud_path);
// ── Load pretty serif fonts (embedded C - Georgia Italic like PSALMS reference) ──
// Embedded fonts are in PSRAM via ELF LOADER LOAD PSRAM (CONFIG_ELF_LOADER_LOAD_PSRAM=y)
// No VFS needed - direct pointers to static const lv_font_t
{
g_ctx.font_serif_26 = (lv_font_t*)&georgia_italic_26;
g_ctx.font_serif_22 = (lv_font_t*)&georgia_italic_22;
g_ctx.font_serif_20 = (lv_font_t*)&georgia_italic_20;
g_ctx.font_serif_18 = (lv_font_t*)&georgia_italic_18;
g_ctx.font_serif_16 = (lv_font_t*)&georgia_italic_16;
g_ctx.font_book_24 = (lv_font_t*)&georgia_regular_24;
g_ctx.fonts_loaded = true;
ESP_LOGI(TAG, "Embedded serif fonts loaded: 26=%p 22=%p 20=%p 18=%p 16=%p book24=%p",
g_ctx.font_serif_26, g_ctx.font_serif_22, g_ctx.font_serif_20, g_ctx.font_serif_18, g_ctx.font_serif_16, g_ctx.font_book_24);
}
resolve_bible_paths(&g_ctx);
if (!load_books_manifest(&g_ctx)) {
@@ -774,15 +1199,16 @@ static void onShowApp(AppHandle app, void* data, lv_obj_t* parent) {
lv_obj_add_flag(g_ctx.verse_wrapper, LV_OBJ_FLAG_CLICKABLE);
lv_obj_add_event_cb(g_ctx.verse_wrapper, on_verse_tap, LV_EVENT_CLICKED, &g_ctx);
// Center reading column with generous breathing room
// Center reading column with generous breathing room - avoid top chrome crop
lv_obj_t* center_col = lv_obj_create(g_ctx.verse_wrapper);
lv_obj_set_size(center_col, LV_PCT(90), LV_PCT(80));
lv_obj_center(center_col);
g_ctx.center_col = center_col;
lv_obj_set_size(center_col, LV_PCT(90), LV_PCT(82)); // 80->82 but with top-safe padding
lv_obj_align(center_col, LV_ALIGN_CENTER, 0, -7); // up 15px: was +8 base, now -7 (user corrected 30->15)
lv_obj_set_style_bg_opa(center_col, LV_OPA_TRANSP, 0);
lv_obj_set_style_border_width(center_col, 0, 0);
lv_obj_set_flex_flow(center_col, LV_FLEX_FLOW_COLUMN);
lv_obj_set_flex_align(center_col, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
lv_obj_set_style_pad_gap(center_col, 14, 0);
lv_obj_set_style_pad_gap(center_col, 10, 0); // tightened: ref closer to verse (was 14)
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);
@@ -791,36 +1217,34 @@ static void onShowApp(AppHandle app, void* data, lv_obj_t* parent) {
g_ctx.lbl_verse = lv_label_create(center_col);
lv_obj_set_width(g_ctx.lbl_verse, LV_PCT(100));
lv_label_set_long_mode(g_ctx.lbl_verse, LV_LABEL_LONG_MODE_WRAP);
lv_obj_set_style_text_color(g_ctx.lbl_verse, lv_color_hex(0xE8E6E0), 0);
lv_obj_set_style_text_font(g_ctx.lbl_verse, lvgl_get_text_font(FONT_SIZE_LARGE), 0);
lv_obj_set_style_text_color(g_ctx.lbl_verse, lv_color_hex(0xF2F0E8), 0); // warmer, editorial like reference image (was #E8E6E0)
lv_obj_set_style_text_font(g_ctx.lbl_verse, lvgl_get_text_font(FONT_SIZE_DEFAULT), 0); // reduced base - Georgia italic wider than Montserrat
lv_obj_set_style_text_align(g_ctx.lbl_verse, LV_TEXT_ALIGN_CENTER, 0);
lv_obj_set_style_text_line_space(g_ctx.lbl_verse, 8, 0);
lv_obj_set_style_text_letter_space(g_ctx.lbl_verse, 0, 0);
lv_obj_set_style_radius(g_ctx.lbl_verse, 16, 0);
lv_obj_set_style_pad_all(g_ctx.lbl_verse, 16, 0);
lv_obj_set_style_radius(g_ctx.lbl_verse, 0, 0); // no bubble - flat editorial
lv_obj_set_style_pad_all(g_ctx.lbl_verse, 4, 0); // less padding, avoid crop
lv_label_set_text(g_ctx.lbl_verse, "Loading...");
g_ctx.lbl_ref = lv_label_create(center_col);
lv_obj_set_style_text_color(g_ctx.lbl_ref, lv_color_hex(0x7A7A96), 0);
lv_obj_set_style_text_color(g_ctx.lbl_ref, lv_color_hex(0x9A9AA8), 0); // softer, like PSALMS citation gray
lv_obj_set_style_text_font(g_ctx.lbl_ref, lvgl_get_text_font(FONT_SIZE_SMALL), 0);
lv_obj_set_style_text_align(g_ctx.lbl_ref, LV_TEXT_ALIGN_CENTER, 0);
lv_obj_set_style_text_letter_space(g_ctx.lbl_ref, 1, 0);
lv_obj_set_style_text_letter_space(g_ctx.lbl_ref, 2, 0); // tracking like reference image's PSALMS 23:1
lv_label_set_text(g_ctx.lbl_ref, "");
// ── Header bar (frosted dark) ──
// ── Header bar (frosted dark) - hidden by default to avoid black bar crop ──
// Fix black bar cropping text: make truly transparent, no border, same bg as root #0E0E14
g_ctx.header_bar = lv_obj_create(parent);
lv_obj_t* hb = g_ctx.header_bar;
lv_obj_set_size(hb, LV_PCT(100), 44);
lv_obj_set_size(hb, LV_PCT(100), 36); // reduced 44->36 to avoid crop
lv_obj_align(hb, LV_ALIGN_TOP_MID, 0, 0);
lv_obj_set_style_radius(hb, 0, 0);
lv_obj_set_style_bg_color(hb, lv_color_hex(0x15151F), 0);
lv_obj_set_style_bg_opa(hb, LV_OPA_COVER, 0);
lv_obj_set_style_bg_color(hb, lv_color_hex(0x0E0E14), 0); // same as root, not #15151F
lv_obj_set_style_bg_opa(hb, LV_OPA_TRANSP, 0); // transparent to avoid black bar - was COVER
lv_obj_set_style_border_width(hb, 0, 0);
lv_obj_set_style_border_side(hb, LV_BORDER_SIDE_BOTTOM, 0);
lv_obj_set_style_border_color(hb, lv_color_hex(0x232338), 0);
lv_obj_set_style_border_width(hb, 1, 0);
lv_obj_set_style_pad_hor(hb, 12, 0);
lv_obj_set_style_pad_ver(hb, 0, 0);
lv_obj_set_style_pad_ver(hb, 4, 0);
lv_obj_remove_flag(hb, LV_OBJ_FLAG_SCROLLABLE);
g_ctx.lbl_book_title = lv_label_create(hb);
@@ -828,6 +1252,8 @@ static void onShowApp(AppHandle app, void* data, lv_obj_t* parent) {
lv_obj_set_style_text_color(g_ctx.lbl_book_title, lv_color_hex(0xE8E6E0), 0);
lv_obj_set_style_text_font(g_ctx.lbl_book_title, lvgl_get_text_font(FONT_SIZE_SMALL), 0);
lv_label_set_text(g_ctx.lbl_book_title, "Bible");
lv_obj_add_flag(g_ctx.lbl_book_title, LV_OBJ_FLAG_CLICKABLE);
lv_obj_add_event_cb(g_ctx.lbl_book_title, on_title_click, LV_EVENT_CLICKED, &g_ctx);
g_ctx.lbl_position = lv_label_create(hb);
lv_obj_align(g_ctx.lbl_position, LV_ALIGN_RIGHT_MID, 0, 0);
@@ -841,18 +1267,15 @@ static void onShowApp(AppHandle app, void* data, lv_obj_t* parent) {
lv_obj_set_size(cb, LV_PCT(100), 56);
lv_obj_align(cb, LV_ALIGN_BOTTOM_MID, 0, 0);
lv_obj_set_style_radius(cb, 0, 0);
lv_obj_set_style_bg_color(cb, lv_color_hex(0x15151F), 0);
lv_obj_set_style_bg_opa(cb, LV_OPA_COVER, 0);
lv_obj_set_style_border_width(cb, 1, 0);
lv_obj_set_style_border_side(cb, LV_BORDER_SIDE_TOP, 0);
lv_obj_set_style_border_color(cb, lv_color_hex(0x232338), 0);
lv_obj_set_style_bg_color(cb, lv_color_hex(0x0E0E14), 0);
lv_obj_set_style_bg_opa(cb, LV_OPA_TRANSP, 0); // transparent editorial, was #15151F COVER
lv_obj_set_style_border_width(cb, 0, 0);
lv_obj_set_style_pad_all(cb, 6, 0);
lv_obj_set_style_pad_gap(cb, 4, 0);
lv_obj_set_flex_flow(cb, LV_FLEX_FLOW_ROW);
lv_obj_set_flex_align(cb, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
lv_obj_remove_flag(cb, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_t* btn;
lv_obj_t* lbl;
g_ctx.btn_close = lv_button_create(cb);
@@ -863,6 +1286,14 @@ static void onShowApp(AppHandle app, void* data, lv_obj_t* parent) {
lbl = lv_label_create(g_ctx.btn_close); lv_label_set_text(lbl, LV_SYMBOL_CLOSE); lv_obj_center(lbl);
lv_obj_add_event_cb(g_ctx.btn_close, on_close_click, LV_EVENT_CLICKED, &g_ctx);
g_ctx.btn_books = lv_button_create(cb);
lv_obj_set_size(g_ctx.btn_books, 44, 38);
lv_obj_set_style_radius(g_ctx.btn_books, 10, 0);
lv_obj_set_style_bg_color(g_ctx.btn_books, lv_color_hex(0x252538), 0);
lv_obj_set_style_text_color(g_ctx.btn_books, lv_color_hex(0xC4C4D4), 0);
lbl = lv_label_create(g_ctx.btn_books); lv_label_set_text(lbl, LV_SYMBOL_LIST); lv_obj_center(lbl);
lv_obj_add_event_cb(g_ctx.btn_books, on_books_click, LV_EVENT_CLICKED, &g_ctx);
g_ctx.btn_prev = lv_button_create(cb);
lv_obj_set_size(g_ctx.btn_prev, 44, 38);
lv_obj_set_style_radius(g_ctx.btn_prev, 10, 0);
@@ -872,7 +1303,7 @@ static void onShowApp(AppHandle app, void* data, lv_obj_t* parent) {
lv_obj_add_event_cb(g_ctx.btn_prev, on_prev_click, LV_EVENT_CLICKED, &g_ctx);
g_ctx.btn_play_pause = lv_button_create(cb);
lv_obj_set_size(g_ctx.btn_play_pause, 76, 38);
lv_obj_set_size(g_ctx.btn_play_pause, 56, 38);
lv_obj_set_style_radius(g_ctx.btn_play_pause, 19, 0);
lv_obj_set_style_bg_color(g_ctx.btn_play_pause, lv_color_hex(0x2A2A4A), 0);
lv_obj_set_style_text_color(g_ctx.btn_play_pause, lv_color_hex(0xE8E6E0), 0);
@@ -888,23 +1319,20 @@ static void onShowApp(AppHandle app, void* data, lv_obj_t* parent) {
lv_obj_add_event_cb(g_ctx.btn_next, on_next_click, LV_EVENT_CLICKED, &g_ctx);
g_ctx.btn_highlight = lv_button_create(cb);
lv_obj_set_size(g_ctx.btn_highlight, 44, 38);
lv_obj_set_size(g_ctx.btn_highlight, 38, 38);
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);
lv_obj_add_event_cb(g_ctx.btn_highlight, on_highlight_click, LV_EVENT_CLICKED, &g_ctx);
btn = lv_button_create(cb);
lv_obj_set_size(btn, 38, 38);
lv_obj_set_style_radius(btn, 10, 0);
lv_obj_set_style_bg_color(btn, lv_color_hex(0x1E1E2E), 0);
lv_obj_set_style_text_color(btn, lv_color_hex(0x6A6A8A), 0);
lbl = lv_label_create(btn); lv_label_set_text(lbl, LV_SYMBOL_AUDIO); lv_obj_center(lbl);
lv_obj_add_event_cb(btn, on_memo_click, LV_EVENT_CLICKED, &g_ctx);
set_chrome_visible(&g_ctx, false);
set_chrome_visible(&g_ctx, true); // start with menu showing per user request
schedule_chrome_auto_hide(&g_ctx, 10000); // auto hide after 10s
show_current_verse(&g_ctx);
// Build book browser overlay (on top of everything)
build_book_browser(&g_ctx, parent);
g_ctx.auto_timer = lv_timer_create(on_auto_timer, AUTO_ADV_MS, &g_ctx);
}
@@ -914,6 +1342,11 @@ static void onHideApp(AppHandle app, void* data) {
lv_timer_delete(g_ctx.auto_timer);
g_ctx.auto_timer = NULL;
}
if (g_ctx.chrome_auto_hide_timer) {
lv_timer_delete(g_ctx.chrome_auto_hide_timer);
g_ctx.chrome_auto_hide_timer = NULL;
}
// Embedded fonts are static const - no free needed (live in PSRAM ELF)
save_progress(&g_ctx);
unload_book(&g_ctx);
// UI objects are deleted by LVGL parent cleanup