diff --git a/Apps/BibleVerse/CMakeLists.txt b/Apps/BibleVerse/CMakeLists.txt new file mode 100644 index 0000000..3147b54 --- /dev/null +++ b/Apps/BibleVerse/CMakeLists.txt @@ -0,0 +1,16 @@ +cmake_minimum_required(VERSION 3.20) + +include($ENV{IDF_PATH}/tools/cmake/project.cmake) + +if (DEFINED ENV{TACTILITY_SDK_PATH}) + set(TACTILITY_SDK_PATH $ENV{TACTILITY_SDK_PATH}) +else() + set(TACTILITY_SDK_PATH "../../release/TactilitySDK") + message(WARNING "TACTILITY_SDK_PATH environment variable is not set, defaulting to ${TACTILITY_SDK_PATH}") +endif() + +include("${TACTILITY_SDK_PATH}/TactilitySDK.cmake") +set(EXTRA_COMPONENT_DIRS ${TACTILITY_SDK_PATH}) + +project(BibleVerse) +tactility_project(BibleVerse) diff --git a/Apps/BibleVerse/README.md b/Apps/BibleVerse/README.md new file mode 100644 index 0000000..561a42b --- /dev/null +++ b/Apps/BibleVerse/README.md @@ -0,0 +1,84 @@ +# BibleVerse for Tactility + +Single-verse bible reader. Shows one verse fullscreen, auto-advances every minute. Tap to toggle controls. + +## UX + +- **Default view (no chrome)**: immersive context — just the verse text + reference, centered, large font. Like BookPlayer image-only view. +- **Tap**: shows header (current ref + position) and footer controls: + - Close X, Prev, Pause/Play (auto-advance), Next, Highlight (star), Audio memo placeholder. +- Highlighted verses saved to `favorites.txt` in app user data. Long-term: record voice memo, read aloud TTS. + +## Bible data format (split per book) + +We fetch public domain bibles from https://github.com/sajeevavahini/bibles (Zefania XML). + +Offline conversion: + +``` +python3 tools/convert_bible.py --src "World English Bible.xml" --dst assets/bible +``` + +Result structure: + +``` +assets/bible/ + books.json # {"books":[{"bnumber":1,"bname":"Genesis","verses":1533,...}], total_verses, translation, version} + 01.bin / 01.idx # Genesis + 02.bin / 02.idx # Exodus + ... + 66.bin / 66.idx +``` + +- `.bin`: concatenated NUL-terminated UTF8 verse texts. No parsing needed, low RAM. +- `.idx`: header 10 bytes: magic 'BIBK', uint16 version, uint32 count. Then per entry 8 bytes: uint32 offset, uint16 cnum, uint16 vnum. + +This split makes memory footprint tiny for ESP32: +- Only one book loaded at a time (max ~215KB for Psalms, typical 12-100KB). +- Index ~19KB max for Psalms, typically <5KB. + +## SD card fallback + +App also scans: + +- `/sdcard/bibles/` and `/sdcard/bible/` + +If `books.json` not found in assets, it will try to use SD folder. This lets users drop extra translations offline. + +## Settings persistence + +Same pattern as EpubReader / BookPlayer (but those two didn't persist in BookPlayer, EpubReader does): + +Using Tactility app user data paths: + +- `tt_app_get_user_data_path()` -> e.g. `/sdcard/user/app/one.tactility.bibleverse` +- `progress.txt`: stores global verse index + b/c/v for debug + ``` + 1234 + 1 1 1 + 0 0 + ``` +- `favorites.txt`: list of global indices, one per line. + +This lives on SD card (or internal data partition), survives app reinstalls. + +## Build (color screen device, esp32s3) + +Same as BookPlayer — external app, compiled for esp32s3 target (covers CYD-2432s028r, m5stack-cores3, etc). + +```bash +# source IDF (example) +source ~/esp/esp-idf/export.sh +export TACTILITY_SDK_PATH=/path/to/TactilitySDK # or use --local-sdk + +python3 tactility.py Apps/BibleVerse build esp32s3 --local-sdk +``` + +Output: `build/BibleVerse.app` installable via Tactility file manager or `tactility.py ... install ` + +## Future extensions + +- Voice memo: record to `/sdcard/bible_memos/.wav` using same I2S code as BookPlayer +- TTS read aloud: use esp tts or pre-generated mp3 like BookPlayer pages +- Book picker UI (like BookPlayer picker but for bible books) +- Highlight color, search, jump to reference dialog diff --git a/Apps/BibleVerse/assets/bible/01.bin b/Apps/BibleVerse/assets/bible/01.bin new file mode 100644 index 0000000..db60001 Binary files /dev/null and b/Apps/BibleVerse/assets/bible/01.bin differ diff --git a/Apps/BibleVerse/assets/bible/01.idx b/Apps/BibleVerse/assets/bible/01.idx new file mode 100644 index 0000000..2427d34 Binary files /dev/null and b/Apps/BibleVerse/assets/bible/01.idx differ diff --git a/Apps/BibleVerse/assets/bible/02.bin b/Apps/BibleVerse/assets/bible/02.bin new file mode 100644 index 0000000..e519fe6 Binary files /dev/null and b/Apps/BibleVerse/assets/bible/02.bin differ diff --git a/Apps/BibleVerse/assets/bible/02.idx b/Apps/BibleVerse/assets/bible/02.idx new file mode 100644 index 0000000..217d31d Binary files /dev/null and b/Apps/BibleVerse/assets/bible/02.idx differ diff --git a/Apps/BibleVerse/assets/bible/03.bin b/Apps/BibleVerse/assets/bible/03.bin new file mode 100644 index 0000000..0ffda89 Binary files /dev/null and b/Apps/BibleVerse/assets/bible/03.bin differ diff --git a/Apps/BibleVerse/assets/bible/03.idx b/Apps/BibleVerse/assets/bible/03.idx new file mode 100644 index 0000000..737a67f Binary files /dev/null and b/Apps/BibleVerse/assets/bible/03.idx differ diff --git a/Apps/BibleVerse/assets/bible/04.bin b/Apps/BibleVerse/assets/bible/04.bin new file mode 100644 index 0000000..9d9a3cf Binary files /dev/null and b/Apps/BibleVerse/assets/bible/04.bin differ diff --git a/Apps/BibleVerse/assets/bible/04.idx b/Apps/BibleVerse/assets/bible/04.idx new file mode 100644 index 0000000..dff147d Binary files /dev/null and b/Apps/BibleVerse/assets/bible/04.idx differ diff --git a/Apps/BibleVerse/assets/bible/05.bin b/Apps/BibleVerse/assets/bible/05.bin new file mode 100644 index 0000000..a0b86bf Binary files /dev/null and b/Apps/BibleVerse/assets/bible/05.bin differ diff --git a/Apps/BibleVerse/assets/bible/05.idx b/Apps/BibleVerse/assets/bible/05.idx new file mode 100644 index 0000000..6badb98 Binary files /dev/null and b/Apps/BibleVerse/assets/bible/05.idx differ diff --git a/Apps/BibleVerse/assets/bible/06.bin b/Apps/BibleVerse/assets/bible/06.bin new file mode 100644 index 0000000..c09e567 Binary files /dev/null and b/Apps/BibleVerse/assets/bible/06.bin differ diff --git a/Apps/BibleVerse/assets/bible/06.idx b/Apps/BibleVerse/assets/bible/06.idx new file mode 100644 index 0000000..e4d038f Binary files /dev/null and b/Apps/BibleVerse/assets/bible/06.idx differ diff --git a/Apps/BibleVerse/assets/bible/07.bin b/Apps/BibleVerse/assets/bible/07.bin new file mode 100644 index 0000000..95be337 Binary files /dev/null and b/Apps/BibleVerse/assets/bible/07.bin differ diff --git a/Apps/BibleVerse/assets/bible/07.idx b/Apps/BibleVerse/assets/bible/07.idx new file mode 100644 index 0000000..be7f075 Binary files /dev/null and b/Apps/BibleVerse/assets/bible/07.idx differ diff --git a/Apps/BibleVerse/assets/bible/08.bin b/Apps/BibleVerse/assets/bible/08.bin new file mode 100644 index 0000000..f1f6e50 Binary files /dev/null and b/Apps/BibleVerse/assets/bible/08.bin differ diff --git a/Apps/BibleVerse/assets/bible/08.idx b/Apps/BibleVerse/assets/bible/08.idx new file mode 100644 index 0000000..f802182 Binary files /dev/null and b/Apps/BibleVerse/assets/bible/08.idx differ diff --git a/Apps/BibleVerse/assets/bible/09.bin b/Apps/BibleVerse/assets/bible/09.bin new file mode 100644 index 0000000..b2c2851 Binary files /dev/null and b/Apps/BibleVerse/assets/bible/09.bin differ diff --git a/Apps/BibleVerse/assets/bible/09.idx b/Apps/BibleVerse/assets/bible/09.idx new file mode 100644 index 0000000..23034e1 Binary files /dev/null and b/Apps/BibleVerse/assets/bible/09.idx differ diff --git a/Apps/BibleVerse/assets/bible/10.bin b/Apps/BibleVerse/assets/bible/10.bin new file mode 100644 index 0000000..18f2efa Binary files /dev/null and b/Apps/BibleVerse/assets/bible/10.bin differ diff --git a/Apps/BibleVerse/assets/bible/10.idx b/Apps/BibleVerse/assets/bible/10.idx new file mode 100644 index 0000000..e4452a2 Binary files /dev/null and b/Apps/BibleVerse/assets/bible/10.idx differ diff --git a/Apps/BibleVerse/assets/bible/11.bin b/Apps/BibleVerse/assets/bible/11.bin new file mode 100644 index 0000000..dabc761 Binary files /dev/null and b/Apps/BibleVerse/assets/bible/11.bin differ diff --git a/Apps/BibleVerse/assets/bible/11.idx b/Apps/BibleVerse/assets/bible/11.idx new file mode 100644 index 0000000..99618b2 Binary files /dev/null and b/Apps/BibleVerse/assets/bible/11.idx differ diff --git a/Apps/BibleVerse/assets/bible/12.bin b/Apps/BibleVerse/assets/bible/12.bin new file mode 100644 index 0000000..64ad86a Binary files /dev/null and b/Apps/BibleVerse/assets/bible/12.bin differ diff --git a/Apps/BibleVerse/assets/bible/12.idx b/Apps/BibleVerse/assets/bible/12.idx new file mode 100644 index 0000000..bfb0d05 Binary files /dev/null and b/Apps/BibleVerse/assets/bible/12.idx differ diff --git a/Apps/BibleVerse/assets/bible/13.bin b/Apps/BibleVerse/assets/bible/13.bin new file mode 100644 index 0000000..9258ce7 Binary files /dev/null and b/Apps/BibleVerse/assets/bible/13.bin differ diff --git a/Apps/BibleVerse/assets/bible/13.idx b/Apps/BibleVerse/assets/bible/13.idx new file mode 100644 index 0000000..22c6b3b Binary files /dev/null and b/Apps/BibleVerse/assets/bible/13.idx differ diff --git a/Apps/BibleVerse/assets/bible/14.bin b/Apps/BibleVerse/assets/bible/14.bin new file mode 100644 index 0000000..bc1601e Binary files /dev/null and b/Apps/BibleVerse/assets/bible/14.bin differ diff --git a/Apps/BibleVerse/assets/bible/14.idx b/Apps/BibleVerse/assets/bible/14.idx new file mode 100644 index 0000000..c545880 Binary files /dev/null and b/Apps/BibleVerse/assets/bible/14.idx differ diff --git a/Apps/BibleVerse/assets/bible/15.bin b/Apps/BibleVerse/assets/bible/15.bin new file mode 100644 index 0000000..d4eee77 Binary files /dev/null and b/Apps/BibleVerse/assets/bible/15.bin differ diff --git a/Apps/BibleVerse/assets/bible/15.idx b/Apps/BibleVerse/assets/bible/15.idx new file mode 100644 index 0000000..01b2bb5 Binary files /dev/null and b/Apps/BibleVerse/assets/bible/15.idx differ diff --git a/Apps/BibleVerse/assets/bible/16.bin b/Apps/BibleVerse/assets/bible/16.bin new file mode 100644 index 0000000..e6569ea Binary files /dev/null and b/Apps/BibleVerse/assets/bible/16.bin differ diff --git a/Apps/BibleVerse/assets/bible/16.idx b/Apps/BibleVerse/assets/bible/16.idx new file mode 100644 index 0000000..d0b2b02 Binary files /dev/null and b/Apps/BibleVerse/assets/bible/16.idx differ diff --git a/Apps/BibleVerse/assets/bible/17.bin b/Apps/BibleVerse/assets/bible/17.bin new file mode 100644 index 0000000..292eed3 Binary files /dev/null and b/Apps/BibleVerse/assets/bible/17.bin differ diff --git a/Apps/BibleVerse/assets/bible/17.idx b/Apps/BibleVerse/assets/bible/17.idx new file mode 100644 index 0000000..3ed82b4 Binary files /dev/null and b/Apps/BibleVerse/assets/bible/17.idx differ diff --git a/Apps/BibleVerse/assets/bible/18.bin b/Apps/BibleVerse/assets/bible/18.bin new file mode 100644 index 0000000..af96fe9 Binary files /dev/null and b/Apps/BibleVerse/assets/bible/18.bin differ diff --git a/Apps/BibleVerse/assets/bible/18.idx b/Apps/BibleVerse/assets/bible/18.idx new file mode 100644 index 0000000..f47798e Binary files /dev/null and b/Apps/BibleVerse/assets/bible/18.idx differ diff --git a/Apps/BibleVerse/assets/bible/19.bin b/Apps/BibleVerse/assets/bible/19.bin new file mode 100644 index 0000000..6ee5654 Binary files /dev/null and b/Apps/BibleVerse/assets/bible/19.bin differ diff --git a/Apps/BibleVerse/assets/bible/19.idx b/Apps/BibleVerse/assets/bible/19.idx new file mode 100644 index 0000000..a00da04 Binary files /dev/null and b/Apps/BibleVerse/assets/bible/19.idx differ diff --git a/Apps/BibleVerse/assets/bible/20.bin b/Apps/BibleVerse/assets/bible/20.bin new file mode 100644 index 0000000..32724fc Binary files /dev/null and b/Apps/BibleVerse/assets/bible/20.bin differ diff --git a/Apps/BibleVerse/assets/bible/20.idx b/Apps/BibleVerse/assets/bible/20.idx new file mode 100644 index 0000000..2b20256 Binary files /dev/null and b/Apps/BibleVerse/assets/bible/20.idx differ diff --git a/Apps/BibleVerse/assets/bible/21.bin b/Apps/BibleVerse/assets/bible/21.bin new file mode 100644 index 0000000..a8502c2 Binary files /dev/null and b/Apps/BibleVerse/assets/bible/21.bin differ diff --git a/Apps/BibleVerse/assets/bible/21.idx b/Apps/BibleVerse/assets/bible/21.idx new file mode 100644 index 0000000..12fae69 Binary files /dev/null and b/Apps/BibleVerse/assets/bible/21.idx differ diff --git a/Apps/BibleVerse/assets/bible/22.bin b/Apps/BibleVerse/assets/bible/22.bin new file mode 100644 index 0000000..0932f93 Binary files /dev/null and b/Apps/BibleVerse/assets/bible/22.bin differ diff --git a/Apps/BibleVerse/assets/bible/22.idx b/Apps/BibleVerse/assets/bible/22.idx new file mode 100644 index 0000000..3563bb6 Binary files /dev/null and b/Apps/BibleVerse/assets/bible/22.idx differ diff --git a/Apps/BibleVerse/assets/bible/23.bin b/Apps/BibleVerse/assets/bible/23.bin new file mode 100644 index 0000000..ae3ded4 Binary files /dev/null and b/Apps/BibleVerse/assets/bible/23.bin differ diff --git a/Apps/BibleVerse/assets/bible/23.idx b/Apps/BibleVerse/assets/bible/23.idx new file mode 100644 index 0000000..9c5f629 Binary files /dev/null and b/Apps/BibleVerse/assets/bible/23.idx differ diff --git a/Apps/BibleVerse/assets/bible/24.bin b/Apps/BibleVerse/assets/bible/24.bin new file mode 100644 index 0000000..9d5531b Binary files /dev/null and b/Apps/BibleVerse/assets/bible/24.bin differ diff --git a/Apps/BibleVerse/assets/bible/24.idx b/Apps/BibleVerse/assets/bible/24.idx new file mode 100644 index 0000000..7a850f8 Binary files /dev/null and b/Apps/BibleVerse/assets/bible/24.idx differ diff --git a/Apps/BibleVerse/assets/bible/25.bin b/Apps/BibleVerse/assets/bible/25.bin new file mode 100644 index 0000000..b059665 Binary files /dev/null and b/Apps/BibleVerse/assets/bible/25.bin differ diff --git a/Apps/BibleVerse/assets/bible/25.idx b/Apps/BibleVerse/assets/bible/25.idx new file mode 100644 index 0000000..4b05485 Binary files /dev/null and b/Apps/BibleVerse/assets/bible/25.idx differ diff --git a/Apps/BibleVerse/assets/bible/26.bin b/Apps/BibleVerse/assets/bible/26.bin new file mode 100644 index 0000000..f50ba7d Binary files /dev/null and b/Apps/BibleVerse/assets/bible/26.bin differ diff --git a/Apps/BibleVerse/assets/bible/26.idx b/Apps/BibleVerse/assets/bible/26.idx new file mode 100644 index 0000000..9cbfdd1 Binary files /dev/null and b/Apps/BibleVerse/assets/bible/26.idx differ diff --git a/Apps/BibleVerse/assets/bible/27.bin b/Apps/BibleVerse/assets/bible/27.bin new file mode 100644 index 0000000..e10909e Binary files /dev/null and b/Apps/BibleVerse/assets/bible/27.bin differ diff --git a/Apps/BibleVerse/assets/bible/27.idx b/Apps/BibleVerse/assets/bible/27.idx new file mode 100644 index 0000000..4148f1e Binary files /dev/null and b/Apps/BibleVerse/assets/bible/27.idx differ diff --git a/Apps/BibleVerse/assets/bible/28.bin b/Apps/BibleVerse/assets/bible/28.bin new file mode 100644 index 0000000..b33fec1 Binary files /dev/null and b/Apps/BibleVerse/assets/bible/28.bin differ diff --git a/Apps/BibleVerse/assets/bible/28.idx b/Apps/BibleVerse/assets/bible/28.idx new file mode 100644 index 0000000..8402ca3 Binary files /dev/null and b/Apps/BibleVerse/assets/bible/28.idx differ diff --git a/Apps/BibleVerse/assets/bible/29.bin b/Apps/BibleVerse/assets/bible/29.bin new file mode 100644 index 0000000..612660f Binary files /dev/null and b/Apps/BibleVerse/assets/bible/29.bin differ diff --git a/Apps/BibleVerse/assets/bible/29.idx b/Apps/BibleVerse/assets/bible/29.idx new file mode 100644 index 0000000..3d60ece Binary files /dev/null and b/Apps/BibleVerse/assets/bible/29.idx differ diff --git a/Apps/BibleVerse/assets/bible/30.bin b/Apps/BibleVerse/assets/bible/30.bin new file mode 100644 index 0000000..605ab2e Binary files /dev/null and b/Apps/BibleVerse/assets/bible/30.bin differ diff --git a/Apps/BibleVerse/assets/bible/30.idx b/Apps/BibleVerse/assets/bible/30.idx new file mode 100644 index 0000000..09afd50 Binary files /dev/null and b/Apps/BibleVerse/assets/bible/30.idx differ diff --git a/Apps/BibleVerse/assets/bible/31.bin b/Apps/BibleVerse/assets/bible/31.bin new file mode 100644 index 0000000..dff6511 Binary files /dev/null and b/Apps/BibleVerse/assets/bible/31.bin differ diff --git a/Apps/BibleVerse/assets/bible/31.idx b/Apps/BibleVerse/assets/bible/31.idx new file mode 100644 index 0000000..8c41d26 Binary files /dev/null and b/Apps/BibleVerse/assets/bible/31.idx differ diff --git a/Apps/BibleVerse/assets/bible/32.bin b/Apps/BibleVerse/assets/bible/32.bin new file mode 100644 index 0000000..7f2a6e5 Binary files /dev/null and b/Apps/BibleVerse/assets/bible/32.bin differ diff --git a/Apps/BibleVerse/assets/bible/32.idx b/Apps/BibleVerse/assets/bible/32.idx new file mode 100644 index 0000000..15c1e60 Binary files /dev/null and b/Apps/BibleVerse/assets/bible/32.idx differ diff --git a/Apps/BibleVerse/assets/bible/33.bin b/Apps/BibleVerse/assets/bible/33.bin new file mode 100644 index 0000000..582e9e3 Binary files /dev/null and b/Apps/BibleVerse/assets/bible/33.bin differ diff --git a/Apps/BibleVerse/assets/bible/33.idx b/Apps/BibleVerse/assets/bible/33.idx new file mode 100644 index 0000000..ff30cf2 Binary files /dev/null and b/Apps/BibleVerse/assets/bible/33.idx differ diff --git a/Apps/BibleVerse/assets/bible/34.bin b/Apps/BibleVerse/assets/bible/34.bin new file mode 100644 index 0000000..5006024 Binary files /dev/null and b/Apps/BibleVerse/assets/bible/34.bin differ diff --git a/Apps/BibleVerse/assets/bible/34.idx b/Apps/BibleVerse/assets/bible/34.idx new file mode 100644 index 0000000..7d5a0d9 Binary files /dev/null and b/Apps/BibleVerse/assets/bible/34.idx differ diff --git a/Apps/BibleVerse/assets/bible/35.bin b/Apps/BibleVerse/assets/bible/35.bin new file mode 100644 index 0000000..73170d2 Binary files /dev/null and b/Apps/BibleVerse/assets/bible/35.bin differ diff --git a/Apps/BibleVerse/assets/bible/35.idx b/Apps/BibleVerse/assets/bible/35.idx new file mode 100644 index 0000000..28619b6 Binary files /dev/null and b/Apps/BibleVerse/assets/bible/35.idx differ diff --git a/Apps/BibleVerse/assets/bible/36.bin b/Apps/BibleVerse/assets/bible/36.bin new file mode 100644 index 0000000..ae65d16 Binary files /dev/null and b/Apps/BibleVerse/assets/bible/36.bin differ diff --git a/Apps/BibleVerse/assets/bible/36.idx b/Apps/BibleVerse/assets/bible/36.idx new file mode 100644 index 0000000..2d985a0 Binary files /dev/null and b/Apps/BibleVerse/assets/bible/36.idx differ diff --git a/Apps/BibleVerse/assets/bible/37.bin b/Apps/BibleVerse/assets/bible/37.bin new file mode 100644 index 0000000..f3ef828 Binary files /dev/null and b/Apps/BibleVerse/assets/bible/37.bin differ diff --git a/Apps/BibleVerse/assets/bible/37.idx b/Apps/BibleVerse/assets/bible/37.idx new file mode 100644 index 0000000..7c6fad9 Binary files /dev/null and b/Apps/BibleVerse/assets/bible/37.idx differ diff --git a/Apps/BibleVerse/assets/bible/38.bin b/Apps/BibleVerse/assets/bible/38.bin new file mode 100644 index 0000000..543cf2d Binary files /dev/null and b/Apps/BibleVerse/assets/bible/38.bin differ diff --git a/Apps/BibleVerse/assets/bible/38.idx b/Apps/BibleVerse/assets/bible/38.idx new file mode 100644 index 0000000..15ba74b Binary files /dev/null and b/Apps/BibleVerse/assets/bible/38.idx differ diff --git a/Apps/BibleVerse/assets/bible/39.bin b/Apps/BibleVerse/assets/bible/39.bin new file mode 100644 index 0000000..c01bb63 Binary files /dev/null and b/Apps/BibleVerse/assets/bible/39.bin differ diff --git a/Apps/BibleVerse/assets/bible/39.idx b/Apps/BibleVerse/assets/bible/39.idx new file mode 100644 index 0000000..98db05d Binary files /dev/null and b/Apps/BibleVerse/assets/bible/39.idx differ diff --git a/Apps/BibleVerse/assets/bible/40.bin b/Apps/BibleVerse/assets/bible/40.bin new file mode 100644 index 0000000..1f7537d Binary files /dev/null and b/Apps/BibleVerse/assets/bible/40.bin differ diff --git a/Apps/BibleVerse/assets/bible/40.idx b/Apps/BibleVerse/assets/bible/40.idx new file mode 100644 index 0000000..06aece0 Binary files /dev/null and b/Apps/BibleVerse/assets/bible/40.idx differ diff --git a/Apps/BibleVerse/assets/bible/41.bin b/Apps/BibleVerse/assets/bible/41.bin new file mode 100644 index 0000000..4312f60 Binary files /dev/null and b/Apps/BibleVerse/assets/bible/41.bin differ diff --git a/Apps/BibleVerse/assets/bible/41.idx b/Apps/BibleVerse/assets/bible/41.idx new file mode 100644 index 0000000..95a2981 Binary files /dev/null and b/Apps/BibleVerse/assets/bible/41.idx differ diff --git a/Apps/BibleVerse/assets/bible/42.bin b/Apps/BibleVerse/assets/bible/42.bin new file mode 100644 index 0000000..46e112e Binary files /dev/null and b/Apps/BibleVerse/assets/bible/42.bin differ diff --git a/Apps/BibleVerse/assets/bible/42.idx b/Apps/BibleVerse/assets/bible/42.idx new file mode 100644 index 0000000..144b6ec Binary files /dev/null and b/Apps/BibleVerse/assets/bible/42.idx differ diff --git a/Apps/BibleVerse/assets/bible/43.bin b/Apps/BibleVerse/assets/bible/43.bin new file mode 100644 index 0000000..cc02d2d Binary files /dev/null and b/Apps/BibleVerse/assets/bible/43.bin differ diff --git a/Apps/BibleVerse/assets/bible/43.idx b/Apps/BibleVerse/assets/bible/43.idx new file mode 100644 index 0000000..5e173d4 Binary files /dev/null and b/Apps/BibleVerse/assets/bible/43.idx differ diff --git a/Apps/BibleVerse/assets/bible/44.bin b/Apps/BibleVerse/assets/bible/44.bin new file mode 100644 index 0000000..9bf2e88 Binary files /dev/null and b/Apps/BibleVerse/assets/bible/44.bin differ diff --git a/Apps/BibleVerse/assets/bible/44.idx b/Apps/BibleVerse/assets/bible/44.idx new file mode 100644 index 0000000..4f01d45 Binary files /dev/null and b/Apps/BibleVerse/assets/bible/44.idx differ diff --git a/Apps/BibleVerse/assets/bible/45.bin b/Apps/BibleVerse/assets/bible/45.bin new file mode 100644 index 0000000..b9642c2 Binary files /dev/null and b/Apps/BibleVerse/assets/bible/45.bin differ diff --git a/Apps/BibleVerse/assets/bible/45.idx b/Apps/BibleVerse/assets/bible/45.idx new file mode 100644 index 0000000..cccc6c3 Binary files /dev/null and b/Apps/BibleVerse/assets/bible/45.idx differ diff --git a/Apps/BibleVerse/assets/bible/46.bin b/Apps/BibleVerse/assets/bible/46.bin new file mode 100644 index 0000000..a5717cf Binary files /dev/null and b/Apps/BibleVerse/assets/bible/46.bin differ diff --git a/Apps/BibleVerse/assets/bible/46.idx b/Apps/BibleVerse/assets/bible/46.idx new file mode 100644 index 0000000..6cf708a Binary files /dev/null and b/Apps/BibleVerse/assets/bible/46.idx differ diff --git a/Apps/BibleVerse/assets/bible/47.bin b/Apps/BibleVerse/assets/bible/47.bin new file mode 100644 index 0000000..3a64ac3 Binary files /dev/null and b/Apps/BibleVerse/assets/bible/47.bin differ diff --git a/Apps/BibleVerse/assets/bible/47.idx b/Apps/BibleVerse/assets/bible/47.idx new file mode 100644 index 0000000..d6a56af Binary files /dev/null and b/Apps/BibleVerse/assets/bible/47.idx differ diff --git a/Apps/BibleVerse/assets/bible/48.bin b/Apps/BibleVerse/assets/bible/48.bin new file mode 100644 index 0000000..e114600 Binary files /dev/null and b/Apps/BibleVerse/assets/bible/48.bin differ diff --git a/Apps/BibleVerse/assets/bible/48.idx b/Apps/BibleVerse/assets/bible/48.idx new file mode 100644 index 0000000..c0e96b9 Binary files /dev/null and b/Apps/BibleVerse/assets/bible/48.idx differ diff --git a/Apps/BibleVerse/assets/bible/49.bin b/Apps/BibleVerse/assets/bible/49.bin new file mode 100644 index 0000000..5ed71c3 Binary files /dev/null and b/Apps/BibleVerse/assets/bible/49.bin differ diff --git a/Apps/BibleVerse/assets/bible/49.idx b/Apps/BibleVerse/assets/bible/49.idx new file mode 100644 index 0000000..c392aab Binary files /dev/null and b/Apps/BibleVerse/assets/bible/49.idx differ diff --git a/Apps/BibleVerse/assets/bible/50.bin b/Apps/BibleVerse/assets/bible/50.bin new file mode 100644 index 0000000..6c53866 Binary files /dev/null and b/Apps/BibleVerse/assets/bible/50.bin differ diff --git a/Apps/BibleVerse/assets/bible/50.idx b/Apps/BibleVerse/assets/bible/50.idx new file mode 100644 index 0000000..3ea4ada Binary files /dev/null and b/Apps/BibleVerse/assets/bible/50.idx differ diff --git a/Apps/BibleVerse/assets/bible/51.bin b/Apps/BibleVerse/assets/bible/51.bin new file mode 100644 index 0000000..5a92e53 Binary files /dev/null and b/Apps/BibleVerse/assets/bible/51.bin differ diff --git a/Apps/BibleVerse/assets/bible/51.idx b/Apps/BibleVerse/assets/bible/51.idx new file mode 100644 index 0000000..98a77af Binary files /dev/null and b/Apps/BibleVerse/assets/bible/51.idx differ diff --git a/Apps/BibleVerse/assets/bible/52.bin b/Apps/BibleVerse/assets/bible/52.bin new file mode 100644 index 0000000..2a51a68 Binary files /dev/null and b/Apps/BibleVerse/assets/bible/52.bin differ diff --git a/Apps/BibleVerse/assets/bible/52.idx b/Apps/BibleVerse/assets/bible/52.idx new file mode 100644 index 0000000..0e93187 Binary files /dev/null and b/Apps/BibleVerse/assets/bible/52.idx differ diff --git a/Apps/BibleVerse/assets/bible/53.bin b/Apps/BibleVerse/assets/bible/53.bin new file mode 100644 index 0000000..103182d Binary files /dev/null and b/Apps/BibleVerse/assets/bible/53.bin differ diff --git a/Apps/BibleVerse/assets/bible/53.idx b/Apps/BibleVerse/assets/bible/53.idx new file mode 100644 index 0000000..c5409a7 Binary files /dev/null and b/Apps/BibleVerse/assets/bible/53.idx differ diff --git a/Apps/BibleVerse/assets/bible/54.bin b/Apps/BibleVerse/assets/bible/54.bin new file mode 100644 index 0000000..0a5ab8e Binary files /dev/null and b/Apps/BibleVerse/assets/bible/54.bin differ diff --git a/Apps/BibleVerse/assets/bible/54.idx b/Apps/BibleVerse/assets/bible/54.idx new file mode 100644 index 0000000..824bc50 Binary files /dev/null and b/Apps/BibleVerse/assets/bible/54.idx differ diff --git a/Apps/BibleVerse/assets/bible/55.bin b/Apps/BibleVerse/assets/bible/55.bin new file mode 100644 index 0000000..3d824ff Binary files /dev/null and b/Apps/BibleVerse/assets/bible/55.bin differ diff --git a/Apps/BibleVerse/assets/bible/55.idx b/Apps/BibleVerse/assets/bible/55.idx new file mode 100644 index 0000000..242d506 Binary files /dev/null and b/Apps/BibleVerse/assets/bible/55.idx differ diff --git a/Apps/BibleVerse/assets/bible/56.bin b/Apps/BibleVerse/assets/bible/56.bin new file mode 100644 index 0000000..056948d Binary files /dev/null and b/Apps/BibleVerse/assets/bible/56.bin differ diff --git a/Apps/BibleVerse/assets/bible/56.idx b/Apps/BibleVerse/assets/bible/56.idx new file mode 100644 index 0000000..4081e9c Binary files /dev/null and b/Apps/BibleVerse/assets/bible/56.idx differ diff --git a/Apps/BibleVerse/assets/bible/57.bin b/Apps/BibleVerse/assets/bible/57.bin new file mode 100644 index 0000000..253b757 Binary files /dev/null and b/Apps/BibleVerse/assets/bible/57.bin differ diff --git a/Apps/BibleVerse/assets/bible/57.idx b/Apps/BibleVerse/assets/bible/57.idx new file mode 100644 index 0000000..cefc853 Binary files /dev/null and b/Apps/BibleVerse/assets/bible/57.idx differ diff --git a/Apps/BibleVerse/assets/bible/58.bin b/Apps/BibleVerse/assets/bible/58.bin new file mode 100644 index 0000000..9c68add Binary files /dev/null and b/Apps/BibleVerse/assets/bible/58.bin differ diff --git a/Apps/BibleVerse/assets/bible/58.idx b/Apps/BibleVerse/assets/bible/58.idx new file mode 100644 index 0000000..eabf9e8 Binary files /dev/null and b/Apps/BibleVerse/assets/bible/58.idx differ diff --git a/Apps/BibleVerse/assets/bible/59.bin b/Apps/BibleVerse/assets/bible/59.bin new file mode 100644 index 0000000..3d3cdde Binary files /dev/null and b/Apps/BibleVerse/assets/bible/59.bin differ diff --git a/Apps/BibleVerse/assets/bible/59.idx b/Apps/BibleVerse/assets/bible/59.idx new file mode 100644 index 0000000..a69b1eb Binary files /dev/null and b/Apps/BibleVerse/assets/bible/59.idx differ diff --git a/Apps/BibleVerse/assets/bible/60.bin b/Apps/BibleVerse/assets/bible/60.bin new file mode 100644 index 0000000..e6c6e4b Binary files /dev/null and b/Apps/BibleVerse/assets/bible/60.bin differ diff --git a/Apps/BibleVerse/assets/bible/60.idx b/Apps/BibleVerse/assets/bible/60.idx new file mode 100644 index 0000000..50a350d Binary files /dev/null and b/Apps/BibleVerse/assets/bible/60.idx differ diff --git a/Apps/BibleVerse/assets/bible/61.bin b/Apps/BibleVerse/assets/bible/61.bin new file mode 100644 index 0000000..f577066 Binary files /dev/null and b/Apps/BibleVerse/assets/bible/61.bin differ diff --git a/Apps/BibleVerse/assets/bible/61.idx b/Apps/BibleVerse/assets/bible/61.idx new file mode 100644 index 0000000..a37f8cd Binary files /dev/null and b/Apps/BibleVerse/assets/bible/61.idx differ diff --git a/Apps/BibleVerse/assets/bible/62.bin b/Apps/BibleVerse/assets/bible/62.bin new file mode 100644 index 0000000..561130b Binary files /dev/null and b/Apps/BibleVerse/assets/bible/62.bin differ diff --git a/Apps/BibleVerse/assets/bible/62.idx b/Apps/BibleVerse/assets/bible/62.idx new file mode 100644 index 0000000..f09b69f Binary files /dev/null and b/Apps/BibleVerse/assets/bible/62.idx differ diff --git a/Apps/BibleVerse/assets/bible/63.bin b/Apps/BibleVerse/assets/bible/63.bin new file mode 100644 index 0000000..093a7b5 Binary files /dev/null and b/Apps/BibleVerse/assets/bible/63.bin differ diff --git a/Apps/BibleVerse/assets/bible/63.idx b/Apps/BibleVerse/assets/bible/63.idx new file mode 100644 index 0000000..b8920fd Binary files /dev/null and b/Apps/BibleVerse/assets/bible/63.idx differ diff --git a/Apps/BibleVerse/assets/bible/64.bin b/Apps/BibleVerse/assets/bible/64.bin new file mode 100644 index 0000000..43f5600 Binary files /dev/null and b/Apps/BibleVerse/assets/bible/64.bin differ diff --git a/Apps/BibleVerse/assets/bible/64.idx b/Apps/BibleVerse/assets/bible/64.idx new file mode 100644 index 0000000..6d292cc Binary files /dev/null and b/Apps/BibleVerse/assets/bible/64.idx differ diff --git a/Apps/BibleVerse/assets/bible/65.bin b/Apps/BibleVerse/assets/bible/65.bin new file mode 100644 index 0000000..1fe608c Binary files /dev/null and b/Apps/BibleVerse/assets/bible/65.bin differ diff --git a/Apps/BibleVerse/assets/bible/65.idx b/Apps/BibleVerse/assets/bible/65.idx new file mode 100644 index 0000000..e450e2b Binary files /dev/null and b/Apps/BibleVerse/assets/bible/65.idx differ diff --git a/Apps/BibleVerse/assets/bible/66.bin b/Apps/BibleVerse/assets/bible/66.bin new file mode 100644 index 0000000..ef33eea Binary files /dev/null and b/Apps/BibleVerse/assets/bible/66.bin differ diff --git a/Apps/BibleVerse/assets/bible/66.idx b/Apps/BibleVerse/assets/bible/66.idx new file mode 100644 index 0000000..7ce0b20 Binary files /dev/null and b/Apps/BibleVerse/assets/bible/66.idx differ diff --git a/Apps/BibleVerse/assets/bible/books.json b/Apps/BibleVerse/assets/bible/books.json new file mode 100644 index 0000000..63f6713 --- /dev/null +++ b/Apps/BibleVerse/assets/bible/books.json @@ -0,0 +1,403 @@ +{ + "books": [ + { + "bnumber": 1, + "bname": "Genesis", + "verses": 1533, + "chapters": 50 + }, + { + "bnumber": 2, + "bname": "Exodus", + "verses": 1213, + "chapters": 40 + }, + { + "bnumber": 3, + "bname": "Leviticus", + "verses": 859, + "chapters": 27 + }, + { + "bnumber": 4, + "bname": "Numbers", + "verses": 1288, + "chapters": 36 + }, + { + "bnumber": 5, + "bname": "Deuteronomy", + "verses": 959, + "chapters": 34 + }, + { + "bnumber": 6, + "bname": "Joshua", + "verses": 658, + "chapters": 24 + }, + { + "bnumber": 7, + "bname": "Judges", + "verses": 618, + "chapters": 21 + }, + { + "bnumber": 8, + "bname": "Ruth", + "verses": 85, + "chapters": 4 + }, + { + "bnumber": 9, + "bname": "1 Samuel", + "verses": 810, + "chapters": 31 + }, + { + "bnumber": 10, + "bname": "2 Samuel", + "verses": 695, + "chapters": 24 + }, + { + "bnumber": 11, + "bname": "1 Kings", + "verses": 816, + "chapters": 22 + }, + { + "bnumber": 12, + "bname": "2 Kings", + "verses": 719, + "chapters": 25 + }, + { + "bnumber": 13, + "bname": "1 Chronicles", + "verses": 942, + "chapters": 29 + }, + { + "bnumber": 14, + "bname": "2 Chronicles", + "verses": 822, + "chapters": 36 + }, + { + "bnumber": 15, + "bname": "Ezra", + "verses": 280, + "chapters": 10 + }, + { + "bnumber": 16, + "bname": "Nehemiah", + "verses": 406, + "chapters": 13 + }, + { + "bnumber": 17, + "bname": "Esther", + "verses": 167, + "chapters": 10 + }, + { + "bnumber": 18, + "bname": "Job", + "verses": 1070, + "chapters": 42 + }, + { + "bnumber": 19, + "bname": "Psalm", + "verses": 2461, + "chapters": 150 + }, + { + "bnumber": 20, + "bname": "Proverbs", + "verses": 915, + "chapters": 31 + }, + { + "bnumber": 21, + "bname": "Ecclesiastes", + "verses": 222, + "chapters": 12 + }, + { + "bnumber": 22, + "bname": "Song of Solomon", + "verses": 117, + "chapters": 8 + }, + { + "bnumber": 23, + "bname": "Isaiah", + "verses": 1292, + "chapters": 66 + }, + { + "bnumber": 24, + "bname": "Jeremiah", + "verses": 1363, + "chapters": 52 + }, + { + "bnumber": 25, + "bname": "Lamentations", + "verses": 154, + "chapters": 5 + }, + { + "bnumber": 26, + "bname": "Ezekiel", + "verses": 1273, + "chapters": 48 + }, + { + "bnumber": 27, + "bname": "Daniel", + "verses": 357, + "chapters": 12 + }, + { + "bnumber": 28, + "bname": "Hosea", + "verses": 197, + "chapters": 14 + }, + { + "bnumber": 29, + "bname": "Joel", + "verses": 73, + "chapters": 3 + }, + { + "bnumber": 30, + "bname": "Amos", + "verses": 146, + "chapters": 9 + }, + { + "bnumber": 31, + "bname": "Obadiah", + "verses": 21, + "chapters": 1 + }, + { + "bnumber": 32, + "bname": "Jonah", + "verses": 48, + "chapters": 4 + }, + { + "bnumber": 33, + "bname": "Micah", + "verses": 105, + "chapters": 7 + }, + { + "bnumber": 34, + "bname": "Nahum", + "verses": 47, + "chapters": 3 + }, + { + "bnumber": 35, + "bname": "Habakkuk", + "verses": 56, + "chapters": 3 + }, + { + "bnumber": 36, + "bname": "Zephaniah", + "verses": 53, + "chapters": 3 + }, + { + "bnumber": 37, + "bname": "Haggai", + "verses": 38, + "chapters": 2 + }, + { + "bnumber": 38, + "bname": "Zechariah", + "verses": 211, + "chapters": 14 + }, + { + "bnumber": 39, + "bname": "Malachi", + "verses": 55, + "chapters": 4 + }, + { + "bnumber": 40, + "bname": "Matthew", + "verses": 1071, + "chapters": 28 + }, + { + "bnumber": 41, + "bname": "Mark", + "verses": 678, + "chapters": 16 + }, + { + "bnumber": 42, + "bname": "Luke", + "verses": 1150, + "chapters": 24 + }, + { + "bnumber": 43, + "bname": "John", + "verses": 879, + "chapters": 21 + }, + { + "bnumber": 44, + "bname": "Acts", + "verses": 1004, + "chapters": 28 + }, + { + "bnumber": 45, + "bname": "Romans", + "verses": 430, + "chapters": 16 + }, + { + "bnumber": 46, + "bname": "1 Corinthians", + "verses": 437, + "chapters": 16 + }, + { + "bnumber": 47, + "bname": "2 Corinthians", + "verses": 257, + "chapters": 13 + }, + { + "bnumber": 48, + "bname": "Galatians", + "verses": 149, + "chapters": 6 + }, + { + "bnumber": 49, + "bname": "Ephesians", + "verses": 155, + "chapters": 6 + }, + { + "bnumber": 50, + "bname": "Philippians", + "verses": 104, + "chapters": 4 + }, + { + "bnumber": 51, + "bname": "Colossians", + "verses": 95, + "chapters": 4 + }, + { + "bnumber": 52, + "bname": "1 Thessalonians", + "verses": 89, + "chapters": 5 + }, + { + "bnumber": 53, + "bname": "2 Thessalonians", + "verses": 47, + "chapters": 3 + }, + { + "bnumber": 54, + "bname": "1 Timothy", + "verses": 113, + "chapters": 6 + }, + { + "bnumber": 55, + "bname": "2 Timothy", + "verses": 83, + "chapters": 4 + }, + { + "bnumber": 56, + "bname": "Titus", + "verses": 46, + "chapters": 3 + }, + { + "bnumber": 57, + "bname": "Philemon", + "verses": 25, + "chapters": 1 + }, + { + "bnumber": 58, + "bname": "Hebrews", + "verses": 303, + "chapters": 13 + }, + { + "bnumber": 59, + "bname": "James", + "verses": 108, + "chapters": 5 + }, + { + "bnumber": 60, + "bname": "1 Peter", + "verses": 105, + "chapters": 5 + }, + { + "bnumber": 61, + "bname": "2 Peter", + "verses": 61, + "chapters": 3 + }, + { + "bnumber": 62, + "bname": "1 John", + "verses": 105, + "chapters": 5 + }, + { + "bnumber": 63, + "bname": "2 John", + "verses": 13, + "chapters": 1 + }, + { + "bnumber": 64, + "bname": "3 John", + "verses": 14, + "chapters": 1 + }, + { + "bnumber": 65, + "bname": "Jude", + "verses": 25, + "chapters": 1 + }, + { + "bnumber": 66, + "bname": "Revelation", + "verses": 404, + "chapters": 22 + } + ], + "total_verses": 31094, + "version": 1, + "translation": "World English Bible" +} \ No newline at end of file diff --git a/Apps/BibleVerse/main/CMakeLists.txt b/Apps/BibleVerse/main/CMakeLists.txt new file mode 100644 index 0000000..1e48077 --- /dev/null +++ b/Apps/BibleVerse/main/CMakeLists.txt @@ -0,0 +1,13 @@ +file(GLOB_RECURSE SOURCE_FILES Source/*.c) + +idf_component_register( + SRCS ${SOURCE_FILES} + REQUIRES TactilitySDK +) + +# Relax format-truncation: buffers sized generously, warnings are noise here +# and other apps (BookPlayer etc.) use same pattern. +target_compile_options(${COMPONENT_LIB} PRIVATE + -Wno-format-truncation + -Wno-unused-but-set-variable +) diff --git a/Apps/BibleVerse/main/Source/main.c b/Apps/BibleVerse/main/Source/main.c new file mode 100644 index 0000000..19bd47f --- /dev/null +++ b/Apps/BibleVerse/main/Source/main.c @@ -0,0 +1,930 @@ +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "freertos/FreeRTOS.h" +#include "esp_log.h" +#include "tactility/lvgl_fonts.h" + +#define TAG "BibleVerse" +#define AUTO_ADV_MS 60000 // 1 minute + +// Per-book binary format (produced by split_bible.py): +// .bin : concatenated NUL-terminated UTF8 verse strings +// .idx : header 8 bytes: magic 'BIBK' 4s, version u16, count u32 +// then per entry: offset u32, cnum u16, vnum u16 (8 bytes) +// books.json : array of {bnumber, bname, verses, chapters} + +#define MAX_BIBLE_BOOKS 66 +#define MAX_BOOK_NAME 40 +#define MAX_VERSE_TEXT 2048 +#define MAX_PATH 320 + +typedef struct { + uint32_t offset; + uint16_t cnum; + uint16_t vnum; +} VerseIndex; + +typedef struct { + int bnumber; + char bname[MAX_BOOK_NAME]; + int verse_count; + int chapter_count; +} BookInfo; + +typedef struct { + // App + AppHandle app; + + // Bible metadata + BookInfo books[MAX_BIBLE_BOOKS]; + int book_count; + int total_verses; + + // Current position (global index 0..total-1 AND book/chap/verse breakdown) + int cur_global; // 0-based global verse index + int cur_book_idx; // 0..book_count-1 index into books[] + int cur_verse_in_book; // 0..book.verse_count-1 + + // Loaded book data (only one book at a time) + uint8_t* book_bin; // malloc'd content of XX.bin + size_t book_bin_size; + VerseIndex* book_idx; // malloc'd parsed index + int book_idx_count; + int loaded_bnumber; // which bnumber is loaded, -1 = none + + // Settings / favorites + char progress_path[MAX_PATH]; + char favorites_path[MAX_PATH]; + char books_json_path[MAX_PATH]; + char bible_dir[MAX_PATH]; // assets/bible/ path prefix + // SD fallback paths + char sd_bible_dir[MAX_PATH]; + bool use_sd_bible; + + // Highlight + bool is_highlighted; + + // UI + lv_obj_t* root; + lv_obj_t* verse_wrapper; // full screen, clickable to toggle chrome + lv_obj_t* lbl_verse; + lv_obj_t* lbl_ref; // small ref line below verse + lv_obj_t* header_bar; + lv_obj_t* ctrl_bar; + lv_obj_t* lbl_position; + lv_obj_t* lbl_book_title; + lv_obj_t* btn_prev; + lv_obj_t* btn_next; + lv_obj_t* btn_play_pause; + lv_obj_t* btn_highlight; + lv_obj_t* btn_close; + + // Timer + lv_timer_t* auto_timer; + bool auto_paused; + bool ui_visible; // chrome visible? +} AppCtx; + +static AppCtx g_ctx; + +/* ── Forward ── */ +static bool load_books_manifest(AppCtx* ctx); +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 save_progress(AppCtx* ctx); +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 toggle_favorite(AppCtx* ctx); +static void ensure_user_dir(AppCtx* ctx); + +/* ── Utilities ── */ +static void ensure_user_dir(AppCtx* ctx) { + // mkdir -p for progress dir: parent of progress_path + char dir[MAX_PATH]; + strncpy(dir, ctx->progress_path, sizeof(dir)-1); + dir[sizeof(dir)-1]='\0'; + char* last = strrchr(dir, '/'); + if (last) { + *last='\0'; + // create intermediate + char tmp[MAX_PATH]; + size_t len = strlen(dir); + for (size_t i=1;i<=len;i++) { + if (dir[i]=='/' || dir[i]=='\0') { + char c=dir[i]; + dir[i]='\0'; + strncpy(tmp, dir, sizeof(tmp)-1); + tmp[sizeof(tmp)-1]='\0'; + mkdir(tmp, 0755); + dir[i]=c; + } + } + } +} + +static bool file_exists(const char* path) { + struct stat st; + return stat(path, &st)==0; +} + +/* ── Bible asset discovery: try assets/bible then SD fallback ── */ +static void resolve_bible_paths(AppCtx* ctx) { + // First try app assets: tt_app_get_assets_child_path("bible") + char assets_path[MAX_PATH]; size_t sz=sizeof(assets_path); + tt_app_get_assets_path(ctx->app, assets_path, &sz); + // assets_path already is the assets folder itself + snprintf(ctx->bible_dir, sizeof(ctx->bible_dir), "%s/bible", assets_path); + snprintf(ctx->books_json_path, sizeof(ctx->books_json_path), "%s/books.json", ctx->bible_dir); + + // SD fallback: /sdcard/bibles or /sdcard/bible + ctx->use_sd_bible = false; + const char* candidates[] = {"/sdcard/bibles", "/sdcard/bible", "/data/bibles", NULL}; + for (int i=0;candidates[i];i++) { + char cand_books[MAX_PATH]; + snprintf(cand_books, sizeof(cand_books), "%s/books.json", candidates[i]); + if (file_exists(cand_books)) { + strncpy(ctx->sd_bible_dir, candidates[i], sizeof(ctx->sd_bible_dir)-1); + // prefer SD if assets not found + if (!file_exists(ctx->books_json_path)) { + strncpy(ctx->bible_dir, candidates[i], sizeof(ctx->bible_dir)-1); + strncpy(ctx->books_json_path, cand_books, sizeof(ctx->books_json_path)-1); + ctx->use_sd_bible = true; + } + ESP_LOGI(TAG, "Found SD bible at %s (assets bible exists=%d)", candidates[i], file_exists(assets_path)); + break; + } + // also check if XX.bin exists without manifest - if so accept + if (!ctx->use_sd_bible) { + char sample[MAX_PATH]; + snprintf(sample, sizeof(sample), "%s/01.bin", candidates[i]); + if (file_exists(sample)) { + // synthesize if no json - will still attempt scan + strncpy(ctx->sd_bible_dir, candidates[i], sizeof(ctx->sd_bible_dir)-1); + if (!file_exists(ctx->books_json_path)) { + strncpy(ctx->bible_dir, candidates[i], sizeof(ctx->bible_dir)-1); + snprintf(ctx->books_json_path, sizeof(ctx->books_json_path), "%s/books.json", ctx->bible_dir); + ctx->use_sd_bible = true; + } + break; + } + } + } + ESP_LOGI(TAG, "Bible dir resolved: %s use_sd=%d", ctx->bible_dir, ctx->use_sd_bible); +} + +/* ── Simple JSON parse for books.json (no cJSON to keep deps low) ── */ +static bool parse_books_json(AppCtx* ctx, const char* json_str) { + // Extremely simple: look for "bnumber": N and "bname": "...", "verses": N, "chapters": N + // We'll do a state scan: search for { ... } objects inside "books": [ + const char* p = strstr(json_str, "\"books\""); + if (!p) return false; + p = strchr(p, '['); + if (!p) return false; + p++; + ctx->book_count = 0; + while (ctx->book_count < MAX_BIBLE_BOOKS) { + const char* obj_start = strchr(p, '{'); + if (!obj_start) break; + const char* obj_end = strchr(obj_start, '}'); + if (!obj_end) break; + // parse fields within obj_start..obj_end + char temp[512]; + size_t len = (size_t)(obj_end - obj_start + 1); + if (len >= sizeof(temp)) len = sizeof(temp)-1; + memcpy(temp, obj_start, len); + temp[len]='\0'; + + int bnum=0, verses=0, chapters=0; + char bname[MAX_BOOK_NAME]={0}; + + const char* pn = strstr(temp, "\"bnumber\""); + if (pn) sscanf(pn, "\"bnumber\"%*[: ]%d", &bnum); + const char* pb = strstr(temp, "\"bname\""); + if (pb) { + const char* colon = strchr(pb, ':'); + if (colon) { + const char* q1 = strchr(colon, '\"'); + if (q1) { + q1++; + const char* q2 = strchr(q1, '\"'); + if (q2 && (q2-q1)< (int)sizeof(bname)-1) { + memcpy(bname, q1, q2-q1); + bname[q2-q1]='\0'; + } + } + } + } + const char* pv = strstr(temp, "\"verses\""); + if (pv) sscanf(pv, "\"verses\"%*[: ]%d", &verses); + const char* pc = strstr(temp, "\"chapters\""); + if (pc) sscanf(pc, "\"chapters\"%*[: ]%d", &chapters); + + if (bnum>0 && bname[0]) { + ctx->books[ctx->book_count].bnumber = bnum; + strncpy(ctx->books[ctx->book_count].bname, bname, MAX_BOOK_NAME-1); + ctx->books[ctx->book_count].verse_count = verses; + ctx->books[ctx->book_count].chapter_count = chapters; + ctx->book_count++; + } + p = obj_end+1; + // stop if ] seen before next { + const char* next_brace = strchr(p, '{'); + const char* next_bracket = strchr(p, ']'); + if (next_bracket && (!next_brace || next_bracket < next_brace)) break; + } + + // total_verses + const char* pt = strstr(json_str, "\"total_verses\""); + if (pt) { + sscanf(pt, "\"total_verses\"%*[: ]%d", &ctx->total_verses); + } else { + ctx->total_verses=0; + for (int i=0;ibook_count;i++) ctx->total_verses+=ctx->books[i].verse_count; + } + return ctx->book_count>0; +} + +static char* read_file_to_str(const char* path, size_t* out_len) { + FILE* f = fopen(path, "rb"); + if (!f) return NULL; + fseek(f,0,SEEK_END); + long sz = ftell(f); + if (sz<=0 || sz>500000) { fclose(f); return NULL; } + fseek(f,0,SEEK_SET); + char* buf = malloc(sz+1); + if (!buf) { fclose(f); return NULL; } + size_t n = fread(buf,1,sz,f); + fclose(f); + buf[n]='\0'; + if (out_len) *out_len=n; + return buf; +} + +static bool load_books_manifest(AppCtx* ctx) { + char* json = read_file_to_str(ctx->books_json_path, NULL); + if (!json) { + // fallback: scan directory for *.bin and create synthetic list if books.json missing + ESP_LOGW(TAG, "books.json not found at %s, scanning dir", ctx->books_json_path); + DIR* d = opendir(ctx->bible_dir); + if (!d) { + ESP_LOGE(TAG, "Cannot open bible dir %s", ctx->bible_dir); + return false; + } + ctx->book_count=0; + ctx->total_verses=0; + struct dirent* ent; + while ((ent=readdir(d))!=NULL && ctx->book_countd_name[0]=='.') continue; + size_t l=strlen(ent->d_name); + if (l<5) continue; + if (strcmp(ent->d_name+l-4, ".bin")!=0) continue; + int bnum=0; + if (sscanf(ent->d_name, "%d", &bnum)!=1) continue; + // try read idx to get count + char idx_path[MAX_PATH]; + snprintf(idx_path, sizeof(idx_path), "%s/%02d.idx", ctx->bible_dir, bnum); + FILE* idxf = fopen(idx_path, "rb"); + int vcount=0; + if (idxf) { + uint8_t hdr[10]={0}; + if (fread(hdr,1,10,idxf)==10) { + if (memcmp(hdr, "BIBK",4)==0) { + // format <4sHI + uint16_t ver; uint32_t cnt; + memcpy(&ver, hdr+4,2); + memcpy(&cnt, hdr+6,4); + vcount = (int)cnt; + } else { + // maybe old header <4sHI as earlier: try + // Actually we wrote <4sHI -> magic 4, H 2, I 4 =10 bytes + // So above. + } + } + fclose(idxf); + } + // generic name from bnumber if can't find + ctx->books[ctx->book_count].bnumber = bnum; + snprintf(ctx->books[ctx->book_count].bname, MAX_BOOK_NAME, "Book %02d", bnum); + ctx->books[ctx->book_count].verse_count = vcount; + ctx->books[ctx->book_count].chapter_count = 0; + ctx->book_count++; + ctx->total_verses+=vcount; + } + closedir(d); + // sort by bnumber + for (int i=0;ibook_count-1;i++) for (int j=0;jbook_count-i-1;j++) if (ctx->books[j].bnumber > ctx->books[j+1].bnumber) { + BookInfo tmp = ctx->books[j]; + ctx->books[j]=ctx->books[j+1]; + ctx->books[j+1]=tmp; + } + return ctx->book_count>0; + } + bool ok = parse_books_json(ctx, json); + free(json); + ESP_LOGI(TAG, "Loaded %d books, total verses %d from %s", ctx->book_count, ctx->total_verses, ctx->books_json_path); + return ok; +} + +/* ── Book loading ── */ +static bool load_book(AppCtx* ctx, int bnumber) { + if (ctx->loaded_bnumber == bnumber && ctx->book_bin && ctx->book_idx) return true; + unload_book(ctx); + + char bin_path[MAX_PATH]; + char idx_path[MAX_PATH]; + snprintf(bin_path, sizeof(bin_path), "%s/%02d.bin", ctx->bible_dir, bnumber); + snprintf(idx_path, sizeof(idx_path), "%s/%02d.idx", ctx->bible_dir, bnumber); + + if (!file_exists(bin_path) || !file_exists(idx_path)) { + ESP_LOGE(TAG, "Book files missing bnum=%d bin=%s idx=%s", bnumber, bin_path, idx_path); + return false; + } + + // load idx first + FILE* f = fopen(idx_path, "rb"); + if (!f) return false; + uint8_t hdr[10]; + if (fread(hdr,1,10,f)!=10) { fclose(f); return false; } + if (memcmp(hdr, "BIBK",4)!=0) { fclose(f); return false; } + uint16_t ver=0; uint32_t count=0; + memcpy(&ver, hdr+4, 2); + memcpy(&count, hdr+6, 4); + if (count==0 || count>10000) { fclose(f); return false; } + + ctx->book_idx = malloc(sizeof(VerseIndex)*count); + if (!ctx->book_idx) { fclose(f); return false; } + for (uint32_t i=0;ibook_idx); ctx->book_idx=NULL; fclose(f); return false; } + uint32_t off; uint16_t cnum, vnum; + memcpy(&off, entry,4); + memcpy(&cnum, entry+4,2); + memcpy(&vnum, entry+6,2); + ctx->book_idx[i].offset=off; + ctx->book_idx[i].cnum=cnum; + ctx->book_idx[i].vnum=vnum; + } + fclose(f); + ctx->book_idx_count = (int)count; + + // load bin + f = fopen(bin_path, "rb"); + if (!f) { free(ctx->book_idx); ctx->book_idx=NULL; return false; } + fseek(f,0,SEEK_END); + long sz = ftell(f); + fseek(f,0,SEEK_SET); + if (sz<=0 || sz>1024*1024) { fclose(f); free(ctx->book_idx); ctx->book_idx=NULL; return false; } + ctx->book_bin = malloc(sz+1); + if (!ctx->book_bin) { fclose(f); free(ctx->book_idx); ctx->book_idx=NULL; return false; } + size_t n = fread(ctx->book_bin,1,sz,f); + fclose(f); + ctx->book_bin[sz]='\0'; + ctx->book_bin_size = n; + ctx->loaded_bnumber = bnumber; + ESP_LOGI(TAG, "Loaded book %02d (%d verses, %ld bytes)", bnumber, ctx->book_idx_count, sz); + return true; +} + +static void unload_book(AppCtx* ctx) { + if (ctx->book_bin) { free(ctx->book_bin); ctx->book_bin=NULL; } + if (ctx->book_idx) { free(ctx->book_idx); ctx->book_idx=NULL; } + ctx->book_bin_size=0; + ctx->book_idx_count=0; + ctx->loaded_bnumber=-1; +} + +/* ── Progress persistence ── */ +static void save_progress(AppCtx* ctx) { + ensure_user_dir(ctx); + FILE* f = fopen(ctx->progress_path, "w"); + if (!f) { ESP_LOGE(TAG,"save_progress fail %s", ctx->progress_path); return; } + // Store global index and also b/c/v for human debug + int bnum = 0; int cnum=1; int vnum=1; + if (ctx->cur_book_idx>=0 && ctx->cur_book_idx < ctx->book_count) { + bnum = ctx->books[ctx->cur_book_idx].bnumber; + if (ctx->book_idx && ctx->cur_verse_in_book < ctx->book_idx_count) { + cnum = ctx->book_idx[ctx->cur_verse_in_book].cnum; + vnum = ctx->book_idx[ctx->cur_verse_in_book].vnum; + } + } + fprintf(f, "%d\n%d %d %d\n%d %d\n", ctx->cur_global, bnum, cnum, vnum, ctx->cur_book_idx, ctx->cur_verse_in_book); + fclose(f); + ESP_LOGI(TAG,"Saved progress global=%d b=%d c=%d v=%d", ctx->cur_global, bnum, cnum, vnum); +} + +static bool load_progress(AppCtx* ctx) { + FILE* f = fopen(ctx->progress_path, "r"); + if (!f) return false; + int global_=0; + int bnum=0,cnum=0,vnum=0; + int book_idx_=0, verse_in_book_=0; + int r = fscanf(f, "%d", &global_); + if (r==1) { + // try second line + fscanf(f, "%d %d %d", &bnum, &cnum, &vnum); + fscanf(f, "%d %d", &book_idx_, &verse_in_book_); + } + fclose(f); + if (global_<0) global_=0; + if (global_>=ctx->total_verses) global_=0; + ctx->cur_global = global_; + // We will resolve book_idx / verse_in_book from global later in global_to_book() + (void)bnum;(void)cnum;(void)vnum;(void)book_idx_;(void)verse_in_book_; + return true; +} + +// Convert global idx -> book_idx and verse_in_book +static void global_to_book(AppCtx* ctx, int global) { + if (ctx->book_count==0) return; + int remaining = global; + for (int i=0;ibook_count;i++) { + int vc = ctx->books[i].verse_count; + if (vc<=0) continue; + if (remaining < vc) { + ctx->cur_book_idx = i; + ctx->cur_verse_in_book = remaining; + return; + } + remaining -= vc; + } + // clamp to last + ctx->cur_book_idx = ctx->book_count-1; + ctx->cur_verse_in_book = ctx->books[ctx->book_count-1].verse_count-1; + if (ctx->cur_verse_in_book<0) ctx->cur_verse_in_book=0; +} + +static void book_to_global(AppCtx* ctx) { + int g=0; + for (int i=0;icur_book_idx;i++) g+=ctx->books[i].verse_count; + g+=ctx->cur_verse_in_book; + ctx->cur_global=g; +} + +/* ── Favorites ── */ +static bool is_favorite(AppCtx* ctx, int global_idx) { + if (global_idx<0) return false; + FILE* f = fopen(ctx->favorites_path, "r"); + if (!f) return false; + int v; + while (fscanf(f, "%d", &v)==1) { + if (v==global_idx) { fclose(f); return true; } + } + fclose(f); + return false; +} + +static void toggle_favorite(AppCtx* ctx) { + // load all + int vals[512]; + int cnt=0; + FILE* f = fopen(ctx->favorites_path, "r"); + if (f) { + while (cnt<512 && fscanf(f, "%d", &vals[cnt])==1) cnt++; + fclose(f); + } + // check existence + int idx=-1; + for (int i=0;icur_global) idx=i; + if (idx>=0) { + // remove + for (int i=idx;iis_highlighted=false; + } else { + if (cnt<512) { + vals[cnt++]=ctx->cur_global; + ctx->is_highlighted=true; + } + } + ensure_user_dir(ctx); + f = fopen(ctx->favorites_path, "w"); + if (!f) return; + for (int i=0;ilbl_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; + int line_space; + int pad_v; + int pad_h; + int letter_space = 0; + + if (len < 40) { + font_sz = FONT_SIZE_LARGE; + line_space = 14; + pad_v = 32; + pad_h = 36; + letter_space = 1; + } else if (len < 100) { + font_sz = FONT_SIZE_LARGE; + line_space = 11; + pad_v = 24; + pad_h = 28; + } else if (len < 200) { + font_sz = FONT_SIZE_DEFAULT; + line_space = 9; + pad_v = 18; + pad_h = 22; + } else if (len < 350) { + font_sz = FONT_SIZE_SMALL; + line_space = 6; + pad_v = 12; + pad_h = 18; + } else { + font_sz = FONT_SIZE_SMALL; + line_space = 4; + pad_v = 8; + pad_h = 14; + } + + lv_obj_set_style_text_font(ctx->lbl_verse, lvgl_get_text_font(font_sz), 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); + lv_obj_set_style_pad_bottom(ctx->lbl_verse, pad_v, 0); + lv_obj_set_style_pad_left(ctx->lbl_verse, pad_h, 0); + lv_obj_set_style_pad_right(ctx->lbl_verse, pad_h, 0); +} + +static void show_current_verse(AppCtx* ctx) { + if (ctx->book_count==0) return; + global_to_book(ctx, ctx->cur_global); + book_to_global(ctx); + int bnum = ctx->books[ctx->cur_book_idx].bnumber; + + if (!load_book(ctx, bnum)) { + lv_label_set_text(ctx->lbl_verse, "Failed to load book"); + lv_label_set_text(ctx->lbl_ref, ""); + return; + } + + if (ctx->cur_verse_in_book<0) ctx->cur_verse_in_book=0; + if (ctx->cur_verse_in_book>=ctx->book_idx_count) ctx->cur_verse_in_book=ctx->book_idx_count-1; + + VerseIndex* vi = &ctx->book_idx[ctx->cur_verse_in_book]; + uint32_t off = vi->offset; + const char* text_ptr = "(empty)"; + if (off < ctx->book_bin_size) text_ptr = (const char*)(ctx->book_bin + off); + + char verse_buf[MAX_VERSE_TEXT]; + strncpy(verse_buf, text_ptr, sizeof(verse_buf)-1); + verse_buf[sizeof(verse_buf)-1]='\0'; + + 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); + + if (ctx->lbl_verse) { + 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_book_title) lv_label_set_text(ctx->lbl_book_title, ref_buf); + if (ctx->lbl_position) { + char pos_buf[40]; + snprintf(pos_buf, sizeof(pos_buf), "%d / %d", ctx->cur_global+1, ctx->total_verses); + lv_label_set_text(ctx->lbl_position, pos_buf); + } + + ctx->is_highlighted = is_favorite(ctx, ctx->cur_global); + 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 (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); + } else { + lv_obj_set_style_bg_color(ctx->btn_highlight, lv_color_hex(0x252538), 0); + lv_obj_set_style_text_color(ctx->btn_highlight, lv_color_hex(0x8C8CB0), 0); + } + if (ctx->lbl_verse) { + if (ctx->is_highlighted) { + lv_obj_set_style_bg_color(ctx->lbl_verse, lv_color_hex(0x2E2818), 0); + lv_obj_set_style_bg_opa(ctx->lbl_verse, LV_OPA_COVER, 0); + lv_obj_set_style_border_color(ctx->lbl_verse, lv_color_hex(0xF5C16C), 0); + lv_obj_set_style_border_width(ctx->lbl_verse, 1, 0); + lv_obj_set_style_border_opa(ctx->lbl_verse, LV_OPA_40, 0); + } else { + lv_obj_set_style_bg_opa(ctx->lbl_verse, LV_OPA_TRANSP, 0); + lv_obj_set_style_border_width(ctx->lbl_verse, 0, 0); + } + } + } + + if (ctx->btn_prev) { + if (ctx->cur_global<=0) 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); + else lv_obj_clear_state(ctx->btn_next, LV_STATE_DISABLED); + } + save_progress(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->cur_global) { + ctx->cur_global = ng; + show_current_verse(ctx); + } +} + +/* ── Timer ── */ +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) { + advance_verse(ctx, 1); + } else { + ctx->auto_paused = true; + if (ctx->btn_play_pause) { + lv_obj_t* lbl = lv_obj_get_child(ctx->btn_play_pause, 0); + if (lbl) lv_label_set_text(lbl, LV_SYMBOL_PLAY); + } + } +} + +/* ── 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 toggle_ui(AppCtx* ctx) { + set_chrome_visible(ctx, !ctx->ui_visible); +} + +static void set_chrome_visible(AppCtx* ctx, bool visible) { + ctx->ui_visible = visible; + if (visible) { + if (ctx->header_bar) lv_obj_remove_flag(ctx->header_bar, LV_OBJ_FLAG_HIDDEN); + if (ctx->ctrl_bar) lv_obj_remove_flag(ctx->ctrl_bar, LV_OBJ_FLAG_HIDDEN); + } else { + if (ctx->header_bar) lv_obj_add_flag(ctx->header_bar, LV_OBJ_FLAG_HIDDEN); + if (ctx->ctrl_bar) lv_obj_add_flag(ctx->ctrl_bar, LV_OBJ_FLAG_HIDDEN); + } +} + +static void on_prev_click(lv_event_t* e) { + AppCtx* ctx = (AppCtx*)lv_event_get_user_data(e); + 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); +} +static void on_play_pause_click(lv_event_t* e) { + AppCtx* ctx = (AppCtx*)lv_event_get_user_data(e); + ctx->auto_paused = !ctx->auto_paused; + if (ctx->btn_play_pause) { + lv_obj_t* lbl = lv_obj_get_child(ctx->btn_play_pause, 0); + if (lbl) lv_label_set_text(lbl, ctx->auto_paused ? LV_SYMBOL_PLAY : LV_SYMBOL_PAUSE); + } +} +static void on_highlight_click(lv_event_t* e) { + AppCtx* ctx = (AppCtx*)lv_event_get_user_data(e); + toggle_favorite(ctx); + // refresh highlight visuals without reloading + show_current_verse(ctx); +} +static void on_close_click(lv_event_t* e) { + AppCtx* ctx = (AppCtx*)lv_event_get_user_data(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/.wav + 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); +} + +/* ── App lifecycle ── */ +static void onShowApp(AppHandle app, void* data, lv_obj_t* parent) { + memset(&g_ctx, 0, sizeof(g_ctx)); + g_ctx.app = app; + g_ctx.loaded_bnumber = -1; + g_ctx.auto_paused = false; + g_ctx.ui_visible = false; + g_ctx.root = parent; + + char ud_path[MAX_PATH]; size_t sz=sizeof(ud_path); + tt_app_get_user_data_path(app, ud_path, &sz); + 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); + + resolve_bible_paths(&g_ctx); + + if (!load_books_manifest(&g_ctx)) { + lv_obj_t* lbl = lv_label_create(parent); + lv_label_set_text(lbl, "Bible data not found.\n\nExpected:\n assets/bible/books.json\n or /sdcard/bibles/\n\nRun tools/convert_bible.py"); + lv_obj_align(lbl, LV_ALIGN_CENTER, 0, 0); + lv_label_set_long_mode(lbl, LV_LABEL_LONG_MODE_WRAP); + lv_obj_set_width(lbl, LV_PCT(80)); + return; + } + + if (!load_progress(&g_ctx)) g_ctx.cur_global = 0; + if (g_ctx.cur_global <0 || g_ctx.cur_global >= g_ctx.total_verses) g_ctx.cur_global=0; + + // Deep paper dark - eye friendly for meditation + lv_obj_set_style_bg_color(parent, lv_color_hex(0x0E0E14), 0); + lv_obj_set_style_bg_opa(parent, LV_OPA_COVER, 0); + lv_obj_remove_flag(parent, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_style_pad_all(parent, 0, 0); + + // Full screen tap area + g_ctx.verse_wrapper = lv_obj_create(parent); + lv_obj_set_size(g_ctx.verse_wrapper, LV_PCT(100), LV_PCT(100)); + lv_obj_align(g_ctx.verse_wrapper, LV_ALIGN_CENTER, 0, 0); + lv_obj_set_style_bg_opa(g_ctx.verse_wrapper, LV_OPA_TRANSP, 0); + lv_obj_set_style_border_width(g_ctx.verse_wrapper, 0, 0); + 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_tap, LV_EVENT_CLICKED, &g_ctx); + + // Center reading column with generous breathing room + 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); + 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_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_tap, LV_EVENT_CLICKED, &g_ctx); + + 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_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_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_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_label_set_text(g_ctx.lbl_ref, ""); + + // ── Header bar (frosted dark) ── + 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_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_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_remove_flag(hb, LV_OBJ_FLAG_SCROLLABLE); + + g_ctx.lbl_book_title = lv_label_create(hb); + lv_obj_align(g_ctx.lbl_book_title, LV_ALIGN_LEFT_MID, 0, 0); + 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"); + + g_ctx.lbl_position = lv_label_create(hb); + lv_obj_align(g_ctx.lbl_position, LV_ALIGN_RIGHT_MID, 0, 0); + lv_obj_set_style_text_color(g_ctx.lbl_position, lv_color_hex(0x6A6A8A), 0); + lv_obj_set_style_text_font(g_ctx.lbl_position, lvgl_get_text_font(FONT_SIZE_SMALL), 0); + lv_label_set_text(g_ctx.lbl_position, "1 / 1"); + + // ── Bottom controls (minimal) ── + g_ctx.ctrl_bar = lv_obj_create(parent); + lv_obj_t* cb = g_ctx.ctrl_bar; + 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_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); + lv_obj_set_size(g_ctx.btn_close, 38, 38); + lv_obj_set_style_radius(g_ctx.btn_close, 10, 0); + lv_obj_set_style_bg_color(g_ctx.btn_close, lv_color_hex(0x1E1E2E), 0); + lv_obj_set_style_text_color(g_ctx.btn_close, lv_color_hex(0x8C8CB0), 0); + 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_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); + lv_obj_set_style_bg_color(g_ctx.btn_prev, lv_color_hex(0x252538), 0); + lv_obj_set_style_text_color(g_ctx.btn_prev, lv_color_hex(0xC4C4D4), 0); + lbl = lv_label_create(g_ctx.btn_prev); lv_label_set_text(lbl, LV_SYMBOL_PREV); lv_obj_center(lbl); + 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_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); + lbl = lv_label_create(g_ctx.btn_play_pause); lv_label_set_text(lbl, LV_SYMBOL_PAUSE); lv_obj_center(lbl); + lv_obj_add_event_cb(g_ctx.btn_play_pause, on_play_pause_click, LV_EVENT_CLICKED, &g_ctx); + + g_ctx.btn_next = lv_button_create(cb); + lv_obj_set_size(g_ctx.btn_next, 44, 38); + lv_obj_set_style_radius(g_ctx.btn_next, 10, 0); + lv_obj_set_style_bg_color(g_ctx.btn_next, lv_color_hex(0x252538), 0); + lv_obj_set_style_text_color(g_ctx.btn_next, lv_color_hex(0xC4C4D4), 0); + lbl = lv_label_create(g_ctx.btn_next); lv_label_set_text(lbl, LV_SYMBOL_NEXT); lv_obj_center(lbl); + 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_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); + show_current_verse(&g_ctx); + g_ctx.auto_timer = lv_timer_create(on_auto_timer, AUTO_ADV_MS, &g_ctx); +} + +static void onHideApp(AppHandle app, void* data) { + (void)app; (void)data; + if (g_ctx.auto_timer) { + lv_timer_delete(g_ctx.auto_timer); + g_ctx.auto_timer = NULL; + } + save_progress(&g_ctx); + unload_book(&g_ctx); + // UI objects are deleted by LVGL parent cleanup + memset(&g_ctx, 0, sizeof(g_ctx)); + g_ctx.loaded_bnumber = -1; +} + +int main(int argc, char* argv[]) { + tt_app_register((AppRegistration){ + .onShow = onShowApp, + .onHide = onHideApp + }); + return 0; +} diff --git a/Apps/BibleVerse/manifest.properties b/Apps/BibleVerse/manifest.properties new file mode 100644 index 0000000..c9a3245 --- /dev/null +++ b/Apps/BibleVerse/manifest.properties @@ -0,0 +1,11 @@ +[manifest] +version=0.1 +[target] +sdk=0.8.0-dev +platforms=esp32s3,esp32p4 +[app] +id=one.tactility.bibleverse +versionName=0.1.0 +versionCode=1 +name=Bible Verse +description=Single verse at a time, advances each minute. Tap to show controls. diff --git a/Apps/BibleVerse/tools/convert_bible.py b/Apps/BibleVerse/tools/convert_bible.py new file mode 100755 index 0000000..185385d --- /dev/null +++ b/Apps/BibleVerse/tools/convert_bible.py @@ -0,0 +1,109 @@ +#!/usr/bin/env python3 +""" +Convert sajeevavahini Zefania XML bible -> per-book .bin/.idx format for BibleVerse app. + +Usage: + python tools/convert_bible.py --src "World English Bible.xml" --dst assets/bible + # or auto-download: + python tools/convert_bible.py --download WEB --dst assets/bible +""" +import argparse, os, sys, json, struct, pathlib, urllib.request, xml.etree.ElementTree as ET + +# mapping for --download shortcut to raw urls +DL_MAP = { + "WEB": "https://raw.githubusercontent.com/sajeevavahini/bibles/main/World%20English%20Bible.xml", + "KJV": "https://raw.githubusercontent.com/sajeevavahini/bibles/main/King%20James%20Version.xml", + "ASV": "https://raw.githubusercontent.com/sajeevavahini/bibles/main/American%20Standard%20Version%20(1901).xml", +} + +def download(url, dst_path): + print(f"Downloading {url} -> {dst_path}") + urllib.request.urlretrieve(url, dst_path) + print(f" done {os.path.getsize(dst_path)} bytes") + +def parse_and_write(src_xml, dst_dir): + os.makedirs(dst_dir, exist_ok=True) + print(f"Parsing {src_xml} ...") + books_data = {} # bnum -> {bname, chapters: {cnum: [(vnum,text)]}} + + cur_bnum = 0; cur_bname=""; cur_cnum=0 + # iterparse for low memory + ctx = ET.iterparse(src_xml, events=("start","end")) + for event, elem in ctx: + if event=="start": + if elem.tag=="BIBLEBOOK": + cur_bnum=int(elem.attrib.get("bnumber","0")) + cur_bname=elem.attrib.get("bname","").strip() + if cur_bnum not in books_data: + books_data[cur_bnum]={"bname":cur_bname, "chapters":{}} + elif elem.tag=="CHAPTER": + cur_cnum=int(elem.attrib.get("cnumber","0")) + if cur_bnum in books_data: + books_data[cur_bnum]["chapters"].setdefault(cur_cnum, []) + else: + if elem.tag=="VERS": + vnum=int(elem.attrib.get("vnumber","0")) + text=(elem.text or "").strip() + if text and cur_bnum in books_data: + books_data[cur_bnum]["chapters"].setdefault(cur_cnum, []).append((vnum,text)) + if elem.tag in ("VERS","CHAPTER","BIBLEBOOK"): + elem.clear() + + print(f"Found {len(books_data)} books") + book_list=[] + total=0 + for bnum in sorted(books_data.keys()): + binfo=books_data[bnum] + bname=binfo["bname"] + chapters=binfo["chapters"] + entries=[] + for cnum in sorted(chapters.keys()): + for vnum, txt in sorted(chapters[cnum], key=lambda x: x[0]): + entries.append((cnum,vnum,txt)) + total+=len(entries) + bin_path=os.path.join(dst_dir, f"{bnum:02d}.bin") + idx_path=os.path.join(dst_dir, f"{bnum:02d}.idx") + with open(bin_path,"wb") as bf, open(idx_path,"wb") as ix: + ix.write(struct.pack("<4sHI", b'BIBK', 1, len(entries))) + off=0 + for cnum,vnum,txt in entries: + data=txt.encode('utf-8') + bf.write(data+b'\x00') + ix.write(struct.pack("