From b94e76931a9243b5f8611a950f31d5f3f34cae33 Mon Sep 17 00:00:00 2001 From: ml Date: Tue, 14 Jul 2026 19:03:25 +0200 Subject: [PATCH] Replace the watch app's menu UI with a single scrollable report page, add the config page Report screen (ui_report_window.c, new): replaces the MenuLayer + per-item drill-down windows (ui_menu_window.c/h, ui_card_window.c/h, removed) with one continuously scrollable page mirroring the interpreter's own text/HTML report - day significance, each transit's narrative, the full Celtic Cross spread, and guidance last. Bigger/ bolder fonts throughout. Card images are a fixed 72px wide: a full- display-width version was tried first but reliably crashes the real PNG decoder ("PNG memory allocation failed") on actual hardware, independent of the app's own heap or lazy-loading - 72px is the hardware-verified ceiling. Config page (pkjs/index.js): a self-contained `data:` URI settings form for birth data/language/notification, submitting via AppMessage to the existing app_message.c receiver. package.json needed "capabilities": ["configurable"] for the Pebble mobile app to expose a Settings entry for the sideloaded app at all - app_message.c's own receiving logic was already correct. Added config_log() (config.c) to confirm on-device, via APP_LOG, exactly which values a shown reading is based on - both right after a config submission and at every app launch. --- res/app_icon_18x28_64_color.png | Bin 0 -> 10089 bytes res/app_icon_50x50_64_color.png | Bin 0 -> 12661 bytes res/app_icon_64_gray.png | Bin 13401 -> 0 bytes res/app_icon_color.xcf | Bin 0 -> 8230 bytes res/{app_icon.xcf => app_icon_gray.xcf} | Bin watch/package.json | 139 +++++++++++--- watch/scripts/gen_card_images.py | 27 ++- watch/src/c/app_message.c | 1 + watch/src/c/config.c | 16 ++ watch/src/c/config.h | 6 + watch/src/c/main.c | 13 +- watch/src/c/resource_map.c | 4 + watch/src/c/resource_map.h | 6 + watch/src/c/ui_card_window.c | 78 -------- watch/src/c/ui_card_window.h | 12 -- watch/src/c/ui_menu_window.c | 191 ------------------- watch/src/c/ui_menu_window.h | 19 -- watch/src/c/ui_report_window.c | 243 ++++++++++++++++++++++++ watch/src/c/ui_report_window.h | 23 +++ watch/src/c/ui_text_window.c | 6 +- watch/src/pkjs/index.js | 225 +++++++++++++++++++++- 21 files changed, 659 insertions(+), 350 deletions(-) create mode 100644 res/app_icon_18x28_64_color.png create mode 100644 res/app_icon_50x50_64_color.png delete mode 100644 res/app_icon_64_gray.png create mode 100644 res/app_icon_color.xcf rename res/{app_icon.xcf => app_icon_gray.xcf} (100%) delete mode 100644 watch/src/c/ui_card_window.c delete mode 100644 watch/src/c/ui_card_window.h delete mode 100644 watch/src/c/ui_menu_window.c delete mode 100644 watch/src/c/ui_menu_window.h create mode 100644 watch/src/c/ui_report_window.c create mode 100644 watch/src/c/ui_report_window.h diff --git a/res/app_icon_18x28_64_color.png b/res/app_icon_18x28_64_color.png new file mode 100644 index 0000000000000000000000000000000000000000..5f6905aed74272de39bbd307384e97b126785e61 GIT binary patch literal 10089 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 W%rKb~S>A|s9H6D9uUeyQ8}>g^WJKHm literal 0 HcmV?d00001 diff --git a/res/app_icon_50x50_64_color.png b/res/app_icon_50x50_64_color.png new file mode 100644 index 0000000000000000000000000000000000000000..f585d3ce9f0aacb1d3817b35f2fcc510408504a7 GIT binary patch literal 12661 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 O0v;%7Db~nah5aADLJN8T literal 0 HcmV?d00001 diff --git a/res/app_icon_64_gray.png b/res/app_icon_64_gray.png deleted file mode 100644 index 935dba4676ec200075753f4f7bf47e8b4c38ffc1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 13401 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 Hj diff --git a/res/app_icon_color.xcf b/res/app_icon_color.xcf new file mode 100644 index 0000000000000000000000000000000000000000..18f2dcf0b022cc055c8492518fcb69d803f2e0e3 GIT binary patch literal 8230 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 literal 0 HcmV?d00001 diff --git a/res/app_icon.xcf b/res/app_icon_gray.xcf similarity index 100% rename from res/app_icon.xcf rename to res/app_icon_gray.xcf diff --git a/watch/package.json b/watch/package.json index e6d721a..8a7325a 100644 --- a/watch/package.json +++ b/watch/package.json @@ -2,7 +2,9 @@ "name": "deck_in_a_dash_watch", "author": "Matthias Ladkau", "version": "1.0.0", - "keywords": ["pebble-app"], + "keywords": [ + "pebble-app" + ], "private": true, "dependencies": {}, "pebble": { @@ -10,6 +12,9 @@ "uuid": "7f47ed93-f44e-4c1a-9998-7edcfa61218e", "sdkVersion": "3", "enableMultiJS": true, + "capabilities": [ + "configurable" + ], "targetPlatforms": [ "basalt", "chalk", @@ -37,28 +42,116 @@ ], "resources": { "media": [ - { "type": "bitmap", "name": "IMG_CARD_FOOL", "file": "img/fool.png" }, - { "type": "bitmap", "name": "IMG_CARD_MAGICIAN", "file": "img/magician.png" }, - { "type": "bitmap", "name": "IMG_CARD_HIGH_PRIESTESS", "file": "img/high_priestess.png" }, - { "type": "bitmap", "name": "IMG_CARD_EMPRESS", "file": "img/empress.png" }, - { "type": "bitmap", "name": "IMG_CARD_EMPEROR", "file": "img/emperor.png" }, - { "type": "bitmap", "name": "IMG_CARD_HIEROPHANT", "file": "img/hierophant.png" }, - { "type": "bitmap", "name": "IMG_CARD_LOVERS", "file": "img/lovers.png" }, - { "type": "bitmap", "name": "IMG_CARD_CHARIOT", "file": "img/chariot.png" }, - { "type": "bitmap", "name": "IMG_CARD_STRENGTH", "file": "img/strength.png" }, - { "type": "bitmap", "name": "IMG_CARD_HERMIT", "file": "img/hermit.png" }, - { "type": "bitmap", "name": "IMG_CARD_WHEEL_OF_FORTUNE", "file": "img/wheel_of_fortune.png" }, - { "type": "bitmap", "name": "IMG_CARD_JUSTICE", "file": "img/justice.png" }, - { "type": "bitmap", "name": "IMG_CARD_HANGED_MAN", "file": "img/hanged_man.png" }, - { "type": "bitmap", "name": "IMG_CARD_DEATH", "file": "img/death.png" }, - { "type": "bitmap", "name": "IMG_CARD_TEMPERANCE", "file": "img/temperance.png" }, - { "type": "bitmap", "name": "IMG_CARD_DEVIL", "file": "img/devil.png" }, - { "type": "bitmap", "name": "IMG_CARD_TOWER", "file": "img/tower.png" }, - { "type": "bitmap", "name": "IMG_CARD_STAR", "file": "img/star.png" }, - { "type": "bitmap", "name": "IMG_CARD_MOON", "file": "img/moon.png" }, - { "type": "bitmap", "name": "IMG_CARD_SUN", "file": "img/sun.png" }, - { "type": "bitmap", "name": "IMG_CARD_JUDGEMENT", "file": "img/judgement.png" }, - { "type": "bitmap", "name": "IMG_CARD_WORLD", "file": "img/world.png" } + { + "type": "bitmap", + "name": "IMG_CARD_FOOL", + "file": "img/fool.png" + }, + { + "type": "bitmap", + "name": "IMG_CARD_MAGICIAN", + "file": "img/magician.png" + }, + { + "type": "bitmap", + "name": "IMG_CARD_HIGH_PRIESTESS", + "file": "img/high_priestess.png" + }, + { + "type": "bitmap", + "name": "IMG_CARD_EMPRESS", + "file": "img/empress.png" + }, + { + "type": "bitmap", + "name": "IMG_CARD_EMPEROR", + "file": "img/emperor.png" + }, + { + "type": "bitmap", + "name": "IMG_CARD_HIEROPHANT", + "file": "img/hierophant.png" + }, + { + "type": "bitmap", + "name": "IMG_CARD_LOVERS", + "file": "img/lovers.png" + }, + { + "type": "bitmap", + "name": "IMG_CARD_CHARIOT", + "file": "img/chariot.png" + }, + { + "type": "bitmap", + "name": "IMG_CARD_STRENGTH", + "file": "img/strength.png" + }, + { + "type": "bitmap", + "name": "IMG_CARD_HERMIT", + "file": "img/hermit.png" + }, + { + "type": "bitmap", + "name": "IMG_CARD_WHEEL_OF_FORTUNE", + "file": "img/wheel_of_fortune.png" + }, + { + "type": "bitmap", + "name": "IMG_CARD_JUSTICE", + "file": "img/justice.png" + }, + { + "type": "bitmap", + "name": "IMG_CARD_HANGED_MAN", + "file": "img/hanged_man.png" + }, + { + "type": "bitmap", + "name": "IMG_CARD_DEATH", + "file": "img/death.png" + }, + { + "type": "bitmap", + "name": "IMG_CARD_TEMPERANCE", + "file": "img/temperance.png" + }, + { + "type": "bitmap", + "name": "IMG_CARD_DEVIL", + "file": "img/devil.png" + }, + { + "type": "bitmap", + "name": "IMG_CARD_TOWER", + "file": "img/tower.png" + }, + { + "type": "bitmap", + "name": "IMG_CARD_STAR", + "file": "img/star.png" + }, + { + "type": "bitmap", + "name": "IMG_CARD_MOON", + "file": "img/moon.png" + }, + { + "type": "bitmap", + "name": "IMG_CARD_SUN", + "file": "img/sun.png" + }, + { + "type": "bitmap", + "name": "IMG_CARD_JUDGEMENT", + "file": "img/judgement.png" + }, + { + "type": "bitmap", + "name": "IMG_CARD_WORLD", + "file": "img/world.png" + } ] } } diff --git a/watch/scripts/gen_card_images.py b/watch/scripts/gen_card_images.py index e89b412..7df80a0 100644 --- a/watch/scripts/gen_card_images.py +++ b/watch/scripts/gen_card_images.py @@ -3,21 +3,20 @@ The 22 Major Arcana JPEGs in res/img/ are ~1MB/~1086x1810px each - suitable for the desktop HTML reading (an tag pointed at the file -as-is) but far too large for a Pebble app's own resource budget (256KB -total on basalt-class platforms, shared with every other resource this -app ships). This script resizes each to CARD_WIDTH px wide (preserving -aspect ratio) and writes a PNG per card, named after tarot_data.c's own -k_card_slug table so watch/package.json's "resources.media" entries -(IMG_CARD_, file img/.png) line up without a -separate mapping table anywhere. +as-is) but far too large for a Pebble app's own resource budget. This +script resizes each to CARD_WIDTH px wide (preserving aspect ratio) and +writes a PNG per card, named after tarot_data.c's own k_card_slug table +so watch/package.json's "resources.media" entries (IMG_CARD_, file img/.png) line up without a separate mapping +table anywhere. -CARD_WIDTH=72 (-> 72x120) was picked empirically: a real `pebble build` -of two sample cards at this size measured ~5KB/card on color platforms -(basalt/chalk/emery/gabbro) and ~2.9KB/card on the black-and-white -diorite/flint - so all 22 cards together cost roughly 111KB/64KB -respectively, comfortably inside the 256KB resource budget with room -left for other future resources (fonts, icons), while still rendering -at close to a full basalt-class screen's height (120px of 168px). +CARD_WIDTH=72 (-> 72x120) is a hardware-verified ceiling, not a design +choice: full-display-width images (144-260px depending on platform) were +tried first and reliably crash real hardware with "PNG memory allocation +failed" - a runtime PNG-decoder memory constraint independent of the +app's own heap budget (lazy-loading only one image at a time didn't help +either). 72px is the size the original per-card screens used +successfully all session before that experiment. Generated output, like i18n_tables.auto.c - not committed (see .gitignore), regenerate by running this script directly or via the root diff --git a/watch/src/c/app_message.c b/watch/src/c/app_message.c index e14acb8..c36fc90 100644 --- a/watch/src/c/app_message.c +++ b/watch/src/c/app_message.c @@ -60,6 +60,7 @@ static void inbox_received_handler(DictionaryIterator *iter, void *context) { * of the settings page) must not un-configure an already-set-up watch. */ if (got_birth) cfg.configured = true; + config_log(&cfg); /* confirms what was actually received/parsed */ config_save(&cfg); if (s_callback) s_callback(&cfg); } diff --git a/watch/src/c/config.c b/watch/src/c/config.c index 6a5c8b0..27f2995 100644 --- a/watch/src/c/config.c +++ b/watch/src/c/config.c @@ -68,3 +68,19 @@ void config_save(const WatchConfig *cfg) { persist_write_int(PKEY_NOTIFY_HOUR, cfg->notify_hour); persist_write_int(PKEY_NOTIFY_MINUTE, cfg->notify_minute); } + +/* Cast to int/long rather than %f - Pebble's APP_LOG doesn't support + * floating-point format specifiers. */ +void config_log(const WatchConfig *cfg) { + if (!cfg->configured) { + APP_LOG(APP_LOG_LEVEL_INFO, "config: not configured yet"); + return; + } + APP_LOG(APP_LOG_LEVEL_INFO, "config: birth %04d-%02d-%02d %02d:%02d UTC offset %dmin", + cfg->birth.year, cfg->birth.month, cfg->birth.day, cfg->birth.hour, cfg->birth.minute, + (int)(cfg->birth.utc_offset_hours * 60)); + APP_LOG(APP_LOG_LEVEL_INFO, "config: lat=%ld/1e6 lon=%ld/1e6 lang=%s", + (long)(cfg->birth.latitude * 1000000), (long)(cfg->birth.longitude * 1000000), cfg->lang); + APP_LOG(APP_LOG_LEVEL_INFO, "config: notify_enabled=%d at %02d:%02d", + cfg->notify_enabled, cfg->notify_hour, cfg->notify_minute); +} diff --git a/watch/src/c/config.h b/watch/src/c/config.h index 2894fa1..0bfc87f 100644 --- a/watch/src/c/config.h +++ b/watch/src/c/config.h @@ -28,4 +28,10 @@ void config_load(WatchConfig *out); void config_save(const WatchConfig *cfg); +/* Prints `cfg` via APP_LOG - the same fields a reading is computed from, + * so callers can confirm on-device what values a shown reading is + * actually based on (there's no way to read them back from the watch + * otherwise). Prints "not configured yet" instead if !cfg->configured. */ +void config_log(const WatchConfig *cfg); + #endif diff --git a/watch/src/c/main.c b/watch/src/c/main.c index c2657a9..51732ee 100644 --- a/watch/src/c/main.c +++ b/watch/src/c/main.c @@ -5,7 +5,7 @@ #include "config.h" #include "i18n_tables.h" #include "reading_state.h" -#include "ui_menu_window.h" +#include "ui_report_window.h" #include "ui_text_window.h" static WatchConfig s_config; @@ -24,11 +24,12 @@ static void on_config_updated(const WatchConfig *cfg) { if (!s_config.configured) return; + config_log(&s_config); /* the values this newly-recomputed reading is based on */ reading_state_recompute(&s_config.birth); if (was_configured) { - menu_window_reload(); + report_window_reload(); } else { - menu_window_push(); + report_window_push(); } } @@ -37,14 +38,16 @@ int main(void) { watch_i18n_load(s_config.lang); app_message_init(on_config_updated); + config_log(&s_config); /* the values this session's reading (if any) is based on */ + if (s_config.configured) { reading_state_recompute(&s_config.birth); - menu_window_push(); + report_window_push(); } else { show_setup_required(); } app_event_loop(); - menu_window_deinit(); + report_window_deinit(); } diff --git a/watch/src/c/resource_map.c b/watch/src/c/resource_map.c index 3442a09..1ec9916 100644 --- a/watch/src/c/resource_map.c +++ b/watch/src/c/resource_map.c @@ -29,3 +29,7 @@ uint32_t card_resource_id(TarotCard card) { } return RESOURCE_ID_IMG_CARD_FOOL; /* unreachable for a valid TarotCard */ } + +int16_t card_image_height_for_width(int16_t width) { + return (int16_t)((width * 1810 + 543) / 1086); /* +543 = round, not truncate */ +} diff --git a/watch/src/c/resource_map.h b/watch/src/c/resource_map.h index 7ab9b77..82edf21 100644 --- a/watch/src/c/resource_map.h +++ b/watch/src/c/resource_map.h @@ -15,4 +15,10 @@ * place in the app that switches over. */ uint32_t card_resource_id(TarotCard card); +/* Height in pixels of a card image resized to `width` wide, preserving + * the source art's fixed 1086x1810 aspect ratio - matches + * gen_card_images.py's own resize math, so this stays correct for + * whichever per-platform width variant package.json resolves to. */ +int16_t card_image_height_for_width(int16_t width); + #endif diff --git a/watch/src/c/ui_card_window.c b/watch/src/c/ui_card_window.c deleted file mode 100644 index c4c4905..0000000 --- a/watch/src/c/ui_card_window.c +++ /dev/null @@ -1,78 +0,0 @@ -#include "ui_card_window.h" - -#include - -#include "resource_map.h" - -/* Matches the fixed size gen_card_images.py resizes every card to. */ -#define IMAGE_WIDTH 72 -#define IMAGE_HEIGHT 120 - -typedef struct { - TarotCard card; - char *text; /* title + "\n" + body, combined into one scrollable block */ - GBitmap *bitmap; - BitmapLayer *bitmap_layer; - ScrollLayer *scroll_layer; - TextLayer *text_layer; -} CardWindowState; - -static void window_load(Window *window) { - CardWindowState *state = window_get_user_data(window); - Layer *window_layer = window_get_root_layer(window); - GRect bounds = layer_get_bounds(window_layer); - - state->bitmap = gbitmap_create_with_resource(card_resource_id(state->card)); - int image_x = (bounds.size.w - IMAGE_WIDTH) / 2; - state->bitmap_layer = bitmap_layer_create(GRect(image_x, 0, IMAGE_WIDTH, IMAGE_HEIGHT)); - bitmap_layer_set_bitmap(state->bitmap_layer, state->bitmap); - bitmap_layer_set_alignment(state->bitmap_layer, GAlignCenter); - layer_add_child(window_layer, bitmap_layer_get_layer(state->bitmap_layer)); - - GRect scroll_bounds = GRect(0, IMAGE_HEIGHT, bounds.size.w, bounds.size.h - IMAGE_HEIGHT); - state->scroll_layer = scroll_layer_create(scroll_bounds); - scroll_layer_set_click_config_onto_window(state->scroll_layer, window); - - state->text_layer = text_layer_create(GRect(0, 0, scroll_bounds.size.w, 2000)); - text_layer_set_font(state->text_layer, fonts_get_system_font(FONT_KEY_GOTHIC_18)); - text_layer_set_text(state->text_layer, state->text); - text_layer_set_overflow_mode(state->text_layer, GTextOverflowModeWordWrap); - - GSize content_size = text_layer_get_content_size(state->text_layer); - content_size.w = scroll_bounds.size.w; - content_size.h += 4; - text_layer_set_size(state->text_layer, content_size); - scroll_layer_set_content_size(state->scroll_layer, content_size); - - scroll_layer_add_child(state->scroll_layer, text_layer_get_layer(state->text_layer)); - layer_add_child(window_layer, scroll_layer_get_layer(state->scroll_layer)); -} - -static void window_unload(Window *window) { - CardWindowState *state = window_get_user_data(window); - bitmap_layer_destroy(state->bitmap_layer); - gbitmap_destroy(state->bitmap); - text_layer_destroy(state->text_layer); - scroll_layer_destroy(state->scroll_layer); - free(state->text); - free(state); - window_destroy(window); -} - -void card_window_push(TarotCard card, const char *title, const char *body) { - CardWindowState *state = malloc(sizeof(CardWindowState)); - memset(state, 0, sizeof *state); - state->card = card; - - size_t len = strlen(title) + 1 + strlen(body) + 1; - state->text = malloc(len); - snprintf(state->text, len, "%s\n%s", title, body); - - Window *window = window_create(); - window_set_user_data(window, state); - window_set_window_handlers(window, (WindowHandlers){ - .load = window_load, - .unload = window_unload, - }); - window_stack_push(window, true); -} diff --git a/watch/src/c/ui_card_window.h b/watch/src/c/ui_card_window.h deleted file mode 100644 index 5d45e63..0000000 --- a/watch/src/c/ui_card_window.h +++ /dev/null @@ -1,12 +0,0 @@ -#ifndef WATCH_UI_CARD_WINDOW_H -#define WATCH_UI_CARD_WINDOW_H - -#include "../../../engine/src/tarot.h" - -/* Pushes a window showing `card`'s art (via resource_map.h) pinned at - * the top, with `title` and `body` below it as scrollable text (e.g. the - * Celtic Cross position name + orientation, and the position description - * + card meaning). Copies both strings, same as text_window_push(). */ -void card_window_push(TarotCard card, const char *title, const char *body); - -#endif diff --git a/watch/src/c/ui_menu_window.c b/watch/src/c/ui_menu_window.c deleted file mode 100644 index 4ec70ee..0000000 --- a/watch/src/c/ui_menu_window.c +++ /dev/null @@ -1,191 +0,0 @@ -#include "ui_menu_window.h" - -#include - -#include "../../../engine/src/i18n.h" -#include "../../../interpreter/src/guidance.h" -#include "../../../interpreter/src/narrative.h" -#include "reading_state.h" -#include "ui_card_window.h" -#include "ui_text_window.h" - -typedef enum { - SECTION_TODAY = 0, - SECTION_TRANSITS, - SECTION_CELTIC_CROSS, - NUM_SECTIONS -} MenuSection; - -static Window *s_window; -static MenuLayer *s_menu_layer; - -/* Small local day-level name table, same duplication policy as - * interpreter/src/main.c's own day_level_name() - see significance.c's - * comment on why each independently-testable/linkable module keeps its - * own copy rather than sharing one across translation units that aren't - * otherwise coupled. */ -static const char *day_level_name(DaySignificance level) { - switch (level) { - case DAY_QUIET: return i18n_get("interp.day_level.quiet", "Quiet"); - case DAY_NOTABLE: return i18n_get("interp.day_level.notable", "Notable"); - case DAY_SIGNIFICANT: return i18n_get("interp.day_level.significant", "Significant"); - case DAY_MAJOR: return i18n_get("interp.day_level.major", "Major"); - default: return ""; - } -} - -static const char *reversed_marker(bool reversed) { - return reversed ? i18n_get("ui.reversed", "(Reversed)") : ""; -} - -static uint16_t get_num_sections_callback(MenuLayer *menu_layer, void *data) { - return NUM_SECTIONS; -} - -static uint16_t get_num_rows_callback(MenuLayer *menu_layer, uint16_t section_index, void *data) { - const DailyInterpretation *interp = reading_state_get_interpretation(); - switch (section_index) { - case SECTION_TODAY: return 1; - case SECTION_TRANSITS: return interp->top_item_count > 0 ? interp->top_item_count : 1; - case SECTION_CELTIC_CROSS: return TAROT_SPREAD_SIZE; - default: return 0; - } -} - -static int16_t get_header_height_callback(MenuLayer *menu_layer, uint16_t section_index, void *data) { - return MENU_CELL_BASIC_HEADER_HEIGHT; -} - -static void draw_header_callback(GContext *ctx, const Layer *cell_layer, uint16_t section_index, void *data) { - const char *title = ""; - switch (section_index) { - case SECTION_TODAY: title = i18n_get("interp.heading_day_significance", "Day Significance"); break; - case SECTION_TRANSITS: title = i18n_get("interp.heading_transits", "Significant Transits"); break; - case SECTION_CELTIC_CROSS: title = i18n_get("ui.celtic_cross", "Celtic Cross"); break; - } - menu_cell_basic_header_draw(ctx, cell_layer, title); -} - -static void draw_row_callback(GContext *ctx, const Layer *cell_layer, MenuIndex *cell_index, void *data) { - const DailyReading *reading = reading_state_get_reading(); - const DailyInterpretation *interp = reading_state_get_interpretation(); - - char title[48]; - char subtitle[64]; - - switch (cell_index->section) { - case SECTION_TODAY: - snprintf(title, sizeof title, "%s (%d/%d)", day_level_name(interp->day_level), - interp->day_level + 1, DAY_SIGNIFICANCE_COUNT); - menu_cell_basic_draw(ctx, cell_layer, title, NULL, NULL); - break; - - case SECTION_TRANSITS: - if (interp->top_item_count == 0) { - menu_cell_basic_draw(ctx, cell_layer, - i18n_get("interp.no_notable_transits", "No notable transits today."), - NULL, NULL); - break; - } - { - const Aspect *a = &interp->top_items[cell_index->row].aspect; - snprintf(title, sizeof title, "%s %s", i18n_get("ui.transiting", "Transiting"), - astro_body_name(a->transiting_planet)); - snprintf(subtitle, sizeof subtitle, "%s %s %s", astro_aspect_name(a->type), - i18n_get("ui.natal", "natal"), astro_body_name(a->natal_planet)); - menu_cell_basic_draw(ctx, cell_layer, title, subtitle, NULL); - } - break; - - case SECTION_CELTIC_CROSS: { - const TarotDraw *draw = &reading->spread.positions[cell_index->row]; - snprintf(title, sizeof title, "%s", tarot_position_name((CelticCrossPosition)cell_index->row)); - snprintf(subtitle, sizeof subtitle, "%s %s", tarot_card_name(draw->card), - reversed_marker(draw->reversed)); - menu_cell_basic_draw(ctx, cell_layer, title, subtitle, NULL); - break; - } - } -} - -static void select_callback(MenuLayer *menu_layer, MenuIndex *cell_index, void *data) { - const DailyReading *reading = reading_state_get_reading(); - const DailyInterpretation *interp = reading_state_get_interpretation(); - - switch (cell_index->section) { - case SECTION_TODAY: { - char body[GUIDANCE_TEXT_MAX]; - guidance_write(body, sizeof body, "", interp, &reading->spread); - text_window_push(i18n_get("interp.heading_day_significance", "Day Significance"), body); - break; - } - - case SECTION_TRANSITS: { - if (interp->top_item_count == 0) break; - const Aspect *a = &interp->top_items[cell_index->row].aspect; - char title[48]; - snprintf(title, sizeof title, "%s %s", i18n_get("ui.transiting", "Transiting"), - astro_body_name(a->transiting_planet)); - char body[NARRATIVE_TEXT_MAX]; - narrative_write(body, sizeof body, "", a, reading->transits.bodies[a->transiting_planet].house); - text_window_push(title, body); - break; - } - - case SECTION_CELTIC_CROSS: { - const TarotDraw *draw = &reading->spread.positions[cell_index->row]; - char title[64]; - snprintf(title, sizeof title, "%s: %s %s", - tarot_position_name((CelticCrossPosition)cell_index->row), - tarot_card_name(draw->card), reversed_marker(draw->reversed)); - char body[600]; - snprintf(body, sizeof body, "%s\n\n%s", - tarot_position_description((CelticCrossPosition)cell_index->row), - tarot_card_meaning(draw->card, draw->reversed)); - card_window_push(draw->card, title, body); - break; - } - } -} - -static void window_load(Window *window) { - Layer *window_layer = window_get_root_layer(window); - GRect bounds = layer_get_bounds(window_layer); - - s_menu_layer = menu_layer_create(bounds); - menu_layer_set_callbacks(s_menu_layer, NULL, (MenuLayerCallbacks){ - .get_num_sections = get_num_sections_callback, - .get_num_rows = get_num_rows_callback, - .get_header_height = get_header_height_callback, - .draw_header = draw_header_callback, - .draw_row = draw_row_callback, - .select_click = select_callback, - }); - menu_layer_set_click_config_onto_window(s_menu_layer, window); - layer_add_child(window_layer, menu_layer_get_layer(s_menu_layer)); -} - -static void window_unload(Window *window) { - menu_layer_destroy(s_menu_layer); - s_menu_layer = NULL; -} - -void menu_window_push(void) { - if (!s_window) { - s_window = window_create(); - window_set_window_handlers(s_window, (WindowHandlers){ - .load = window_load, - .unload = window_unload, - }); - } - window_stack_push(s_window, true); -} - -void menu_window_reload(void) { - if (s_menu_layer) menu_layer_reload_data(s_menu_layer); -} - -void menu_window_deinit(void) { - if (s_window) window_destroy(s_window); - s_window = NULL; -} diff --git a/watch/src/c/ui_menu_window.h b/watch/src/c/ui_menu_window.h deleted file mode 100644 index 5ece5da..0000000 --- a/watch/src/c/ui_menu_window.h +++ /dev/null @@ -1,19 +0,0 @@ -#ifndef WATCH_UI_MENU_WINDOW_H -#define WATCH_UI_MENU_WINDOW_H - -/* The app's root screen: a 3-section MenuLayer (Day Significance / - * Significant Transits / Celtic Cross) reading directly from - * reading_state.h's current reading - see reading.h's own doc comment - * ("walks the returned struct directly to lay out its own screens"). - * Selecting a row pushes a detail screen (ui_text_window.h for prose, - * ui_card_window.h for a Celtic Cross position). A persistent singleton - - * created once and left at the bottom of the window stack; call - * menu_window_push() once at startup (and again if the day rolls over - * and the underlying reading changes - the MenuLayer re-reads - * reading_state.h on every redraw, so no explicit refresh call is - * needed beyond menu_layer_reload_data()). */ -void menu_window_push(void); -void menu_window_reload(void); -void menu_window_deinit(void); - -#endif diff --git a/watch/src/c/ui_report_window.c b/watch/src/c/ui_report_window.c new file mode 100644 index 0000000..1deb4e7 --- /dev/null +++ b/watch/src/c/ui_report_window.c @@ -0,0 +1,243 @@ +#include "ui_report_window.h" + +#include + +#include "../../../engine/src/i18n.h" +#include "../../../interpreter/src/guidance.h" +#include "../../../interpreter/src/narrative.h" +#include "reading_state.h" +#include "resource_map.h" + +#define MARGIN 6 + +/* Matches gen_card_images.py's own CARD_WIDTH - a hardware-verified + * ceiling, not a design choice: full-display-width images reliably + * crashed the real PNG decoder ("PNG memory allocation failed", a + * firmware-level constraint independent of this app's own heap budget, + * confirmed unaffected by lazy-loading only one image at a time). */ +#define IMAGE_WIDTH 72 + +/* fonts_get_system_font() needs Pebble's runtime initialized, so these + * are resolved once in window_load() rather than at static-init time. */ +static GFont s_font_section; +static GFont s_font_subhead; +static GFont s_font_body; + +/* Static buffers for every dynamically-built string this window shows - + * tarot_position_description()/tarot_card_meaning()/i18n_get() fallback + * text all point straight at flash/ROM data (see i18n.h's own comment on + * i18n_load_table()), so only the pieces actually assembled at runtime + * (via snprintf/narrative_write/guidance_write) need their own storage. + * Static rather than malloc'd - fixed, known-in-advance sizes, and it + * keeps report_window_reload() from having to reason about freeing the + * previous run's buffers before reusing them. */ +static char s_day_title[64]; +static char s_transit_titles[MAX_SIGNIFICANT_ITEMS][96]; +static char s_narratives[MAX_SIGNIFICANT_ITEMS][NARRATIVE_TEXT_MAX]; +static char s_position_titles[TAROT_SPREAD_SIZE][96]; +static char s_guidance[GUIDANCE_TEXT_MAX]; + +/* Loads/frees each position's GBitmap as scroll_offset_changed() sees it + * enter/leave the visible viewport, rather than decoding all ten Celtic + * Cross images up front - a defensive habit left over from an earlier, + * larger IMAGE_WIDTH (see that constant's own comment); at 72px this + * struct's images are small enough that it's no longer strictly + * necessary, but it's harmless to keep. */ +typedef struct { + TarotCard card; + int16_t y; + int16_t height; + BitmapLayer *bitmap_layer; + GBitmap *bitmap; +} ImageSlot; + +static ImageSlot s_slots[TAROT_SPREAD_SIZE]; + +#define MAX_TEXT_LAYERS 50 +static TextLayer *s_text_layers[MAX_TEXT_LAYERS]; +static int s_text_layer_count; + +static Window *s_window; +static ScrollLayer *s_scroll_layer; +static int16_t s_screen_height; + +static const char *day_level_name(DaySignificance level) { + switch (level) { + case DAY_QUIET: return i18n_get("interp.day_level.quiet", "Quiet"); + case DAY_NOTABLE: return i18n_get("interp.day_level.notable", "Notable"); + case DAY_SIGNIFICANT: return i18n_get("interp.day_level.significant", "Significant"); + case DAY_MAJOR: return i18n_get("interp.day_level.major", "Major"); + default: return ""; + } +} + +static const char *reversed_marker(bool reversed) { + return reversed ? i18n_get("ui.reversed", "(Reversed)") : ""; +} + +static int16_t add_text(int16_t width, int16_t y, const char *text, GFont font) { + TextLayer *tl = text_layer_create(GRect(0, y, width, 4000)); + text_layer_set_font(tl, font); + text_layer_set_text(tl, text); + text_layer_set_overflow_mode(tl, GTextOverflowModeWordWrap); + + GSize size = text_layer_get_content_size(tl); + size.w = width; + size.h += 4; + text_layer_set_size(tl, size); + + scroll_layer_add_child(s_scroll_layer, text_layer_get_layer(tl)); + s_text_layers[s_text_layer_count++] = tl; + return y + size.h + MARGIN; +} + +static void teardown_content(void) { + for (int i = 0; i < s_text_layer_count; i++) { + text_layer_destroy(s_text_layers[i]); + } + s_text_layer_count = 0; + + for (int i = 0; i < TAROT_SPREAD_SIZE; i++) { + if (s_slots[i].bitmap) { + gbitmap_destroy(s_slots[i].bitmap); + s_slots[i].bitmap = NULL; + } + if (s_slots[i].bitmap_layer) { + bitmap_layer_destroy(s_slots[i].bitmap_layer); + s_slots[i].bitmap_layer = NULL; + } + } +} + +static void update_visible_images(void) { + GPoint offset = scroll_layer_get_content_offset(s_scroll_layer); + int16_t visible_top = -offset.y; + int16_t visible_bottom = visible_top + s_screen_height; + + for (int i = 0; i < TAROT_SPREAD_SIZE; i++) { + ImageSlot *slot = &s_slots[i]; + bool visible = slot->y < visible_bottom && slot->y + slot->height > visible_top; + + if (visible && !slot->bitmap) { + slot->bitmap = gbitmap_create_with_resource(card_resource_id(slot->card)); + bitmap_layer_set_bitmap(slot->bitmap_layer, slot->bitmap); + } else if (!visible && slot->bitmap) { + bitmap_layer_set_bitmap(slot->bitmap_layer, NULL); + gbitmap_destroy(slot->bitmap); + slot->bitmap = NULL; + } + } +} + +static void scroll_offset_changed(ScrollLayer *scroll_layer, void *context) { + update_visible_images(); +} + +static void build_content(int16_t width) { + const DailyReading *reading = reading_state_get_reading(); + const DailyInterpretation *interp = reading_state_get_interpretation(); + + int16_t y = MARGIN; + + y = add_text(width, y, i18n_get("interp.heading_day_significance", "Day Significance"), s_font_section); + snprintf(s_day_title, sizeof s_day_title, "%s (%d/%d)", day_level_name(interp->day_level), + interp->day_level + 1, DAY_SIGNIFICANCE_COUNT); + y = add_text(width, y, s_day_title, s_font_body); + + y = add_text(width, y, i18n_get("interp.heading_transits", "Significant Transits"), s_font_section); + if (interp->top_item_count == 0) { + y = add_text(width, y, i18n_get("interp.no_notable_transits", "No notable transits today."), s_font_body); + } else { + for (int i = 0; i < interp->top_item_count; i++) { + const Aspect *a = &interp->top_items[i].aspect; + snprintf(s_transit_titles[i], sizeof s_transit_titles[i], "%s %s %s %s %s", + i18n_get("ui.transiting", "Transiting"), astro_body_name(a->transiting_planet), + astro_aspect_name(a->type), i18n_get("ui.natal", "natal"), + astro_body_name(a->natal_planet)); + y = add_text(width, y, s_transit_titles[i], s_font_subhead); + + narrative_write(s_narratives[i], sizeof s_narratives[i], "", a, + reading->transits.bodies[a->transiting_planet].house); + y = add_text(width, y, s_narratives[i], s_font_body); + } + } + + y = add_text(width, y, i18n_get("ui.celtic_cross", "Celtic Cross"), s_font_section); + for (int i = 0; i < TAROT_SPREAD_SIZE; i++) { + const TarotDraw *draw = &reading->spread.positions[i]; + + snprintf(s_position_titles[i], sizeof s_position_titles[i], "%s: %s %s", + tarot_position_name((CelticCrossPosition)i), tarot_card_name(draw->card), + reversed_marker(draw->reversed)); + y = add_text(width, y, s_position_titles[i], s_font_subhead); + + s_slots[i].card = draw->card; + s_slots[i].y = y; + s_slots[i].height = card_image_height_for_width(IMAGE_WIDTH); + int16_t image_x = (width - IMAGE_WIDTH) / 2; + s_slots[i].bitmap_layer = bitmap_layer_create(GRect(image_x, y, IMAGE_WIDTH, s_slots[i].height)); + bitmap_layer_set_alignment(s_slots[i].bitmap_layer, GAlignCenter); + scroll_layer_add_child(s_scroll_layer, bitmap_layer_get_layer(s_slots[i].bitmap_layer)); + y += s_slots[i].height + MARGIN; + + y = add_text(width, y, tarot_position_description((CelticCrossPosition)i), s_font_body); + y = add_text(width, y, tarot_card_meaning(draw->card, draw->reversed), s_font_body); + } + + y = add_text(width, y, i18n_get("interp.heading_guidance", "Guidance"), s_font_section); + guidance_write(s_guidance, sizeof s_guidance, "", interp, &reading->spread); + y = add_text(width, y, s_guidance, s_font_body); + + scroll_layer_set_content_size(s_scroll_layer, GSize(width, y)); +} + +static void window_load(Window *window) { + s_font_section = fonts_get_system_font(FONT_KEY_GOTHIC_28_BOLD); + s_font_subhead = fonts_get_system_font(FONT_KEY_GOTHIC_24_BOLD); + s_font_body = fonts_get_system_font(FONT_KEY_GOTHIC_24_BOLD); + + Layer *window_layer = window_get_root_layer(window); + GRect bounds = layer_get_bounds(window_layer); + s_screen_height = bounds.size.h; + + s_scroll_layer = scroll_layer_create(bounds); + scroll_layer_set_click_config_onto_window(s_scroll_layer, window); + scroll_layer_set_callbacks(s_scroll_layer, (ScrollLayerCallbacks){ + .content_offset_changed_handler = scroll_offset_changed, + }); + layer_add_child(window_layer, scroll_layer_get_layer(s_scroll_layer)); + + build_content(bounds.size.w); + update_visible_images(); +} + +static void window_unload(Window *window) { + teardown_content(); + scroll_layer_destroy(s_scroll_layer); + s_scroll_layer = NULL; +} + +void report_window_push(void) { + if (!s_window) { + s_window = window_create(); + window_set_window_handlers(s_window, (WindowHandlers){ + .load = window_load, + .unload = window_unload, + }); + } + window_stack_push(s_window, true); +} + +void report_window_reload(void) { + if (!s_window || !s_scroll_layer) return; + Layer *window_layer = window_get_root_layer(s_window); + int16_t width = layer_get_bounds(window_layer).size.w; + teardown_content(); + build_content(width); + update_visible_images(); +} + +void report_window_deinit(void) { + if (s_window) window_destroy(s_window); + s_window = NULL; +} diff --git a/watch/src/c/ui_report_window.h b/watch/src/c/ui_report_window.h new file mode 100644 index 0000000..4ba602e --- /dev/null +++ b/watch/src/c/ui_report_window.h @@ -0,0 +1,23 @@ +#ifndef WATCH_UI_REPORT_WINDOW_H +#define WATCH_UI_REPORT_WINDOW_H + +/* The app's only real screen: one continuously scrollable page mirroring + * the interpreter's own --format text/html report - day significance, + * each significant transit's narrative, the full Celtic Cross spread + * (each position's card art at the watch's own display width, name, + * description, and meaning), and the guidance paragraph last - see + * reading_state.h for where all of this data comes from. + * + * Card art is loaded lazily as it scrolls into view and freed once it + * scrolls back out - a full-display-width card image is too large to + * keep all ten resident at once on this app's RAM budget (see + * ui_report_window.c's own comment) - so unlike a plain ScrollLayer of + * static content, this needs report_window_push()'s window to actually + * be on-screen to drive that loading; call report_window_reload() (not + * push() again) to refresh already-built content after the day rolls + * over or new birth data is submitted. */ +void report_window_push(void); +void report_window_reload(void); +void report_window_deinit(void); + +#endif diff --git a/watch/src/c/ui_text_window.c b/watch/src/c/ui_text_window.c index 6a5afa1..f38cc95 100644 --- a/watch/src/c/ui_text_window.c +++ b/watch/src/c/ui_text_window.c @@ -2,7 +2,7 @@ #include -#define TITLE_HEIGHT 24 +#define TITLE_HEIGHT 30 typedef struct { char *title; @@ -18,7 +18,7 @@ static void window_load(Window *window) { GRect bounds = layer_get_bounds(window_layer); state->title_layer = text_layer_create(GRect(0, 0, bounds.size.w, TITLE_HEIGHT)); - text_layer_set_font(state->title_layer, fonts_get_system_font(FONT_KEY_GOTHIC_18_BOLD)); + text_layer_set_font(state->title_layer, fonts_get_system_font(FONT_KEY_GOTHIC_24_BOLD)); text_layer_set_text(state->title_layer, state->title); text_layer_set_overflow_mode(state->title_layer, GTextOverflowModeTrailingEllipsis); layer_add_child(window_layer, text_layer_get_layer(state->title_layer)); @@ -31,7 +31,7 @@ static void window_load(Window *window) { * measure the real wrapped height - standard Pebble ScrollLayer * pattern (a TextLayer taller than its content just clips nothing). */ state->body_layer = text_layer_create(GRect(0, 0, scroll_bounds.size.w, 2000)); - text_layer_set_font(state->body_layer, fonts_get_system_font(FONT_KEY_GOTHIC_18)); + text_layer_set_font(state->body_layer, fonts_get_system_font(FONT_KEY_GOTHIC_24_BOLD)); text_layer_set_text(state->body_layer, state->body); text_layer_set_overflow_mode(state->body_layer, GTextOverflowModeWordWrap); diff --git a/watch/src/pkjs/index.js b/watch/src/pkjs/index.js index 09d4264..18c5b7b 100644 --- a/watch/src/pkjs/index.js +++ b/watch/src/pkjs/index.js @@ -1,5 +1,220 @@ -// Skeleton only - the real config webview (birth data/language/ -// notification settings) is built out in a later task, see CLAUDE.md/ -// TaskList. No companion app, no server: this file's only future job is -// showConfiguration/webviewclosed handling for a self-contained `data:` -// URI settings form. +// Self-contained Pebble config page - no server, no companion app. The +// form itself is a `data:` URI built entirely here in pkjs and opened +// via Pebble.openURL(); app_message.c's inbox_received_handler is the +// receiving side this pairs with (see its own comment for the wire +// format - lat/lon/UTC offset cross as scaled int32s since AppMessage +// has no float tuple type). +// +// The webview's own storage can't be relied on to remember values +// between openings (Pebble treats each `data:` URI load as a fresh, +// effectively unique origin), so previously-submitted values are kept +// in *pkjs's* own localStorage (persists across app launches on the +// phone) and baked into the generated HTML each time the page opens. +// +// Plain ES5 throughout (var, string concatenation, no arrow functions/ +// template literals) - the webview runs in whatever WebView the Pebble +// mobile app itself embeds, not a guaranteed-modern engine. + +var STORAGE_KEY = 'deckInADashConfig'; +var LANGUAGES = [ + { code: 'en', label: 'English' }, + { code: 'de', label: 'Deutsch' } +]; + +function loadSavedConfig() { + try { + var raw = window.localStorage.getItem(STORAGE_KEY); + if (raw) return JSON.parse(raw); + } catch (e) { + // Corrupt/inaccessible storage - fall through to defaults below. + } + return {}; +} + +function saveConfig(cfg) { + try { + window.localStorage.setItem(STORAGE_KEY, JSON.stringify(cfg)); + } catch (e) { + // Best-effort only - the watch already has the real submitted + // values via AppMessage regardless of whether this cache succeeds. + } +} + +function escapeHtml(value) { + var s = value === null || value === undefined ? '' : String(value); + return s.replace(/[&<>"']/g, function (c) { + return { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]; + }); +} + +function buildLanguageOptions(selected) { + var html = ''; + for (var i = 0; i < LANGUAGES.length; i++) { + var lang = LANGUAGES[i]; + var isSelected = lang.code === selected ? ' selected' : ''; + html += ''; + } + return html; +} + +// Runs *inside* the webview (embedded as a ' + + ''; +} + +Pebble.addEventListener('ready', function () { + console.log('Deck in a Dash: pkjs ready, showConfiguration listener registered'); +}); + +Pebble.addEventListener('showConfiguration', function () { + console.log('Deck in a Dash: showConfiguration fired'); + var html = buildConfigHtml(loadSavedConfig()); + Pebble.openURL('data:text/html;charset=utf-8,' + encodeURIComponent(html)); +}); + +Pebble.addEventListener('webviewclosed', function (e) { + if (!e || !e.response) return; + + var result; + try { + result = JSON.parse(decodeURIComponent(e.response)); + } catch (err) { + return; // Cancelled (no response) or a malformed payload - ignore. + } + + saveConfig({ + birth_date: result.saved_birth_date, + birth_time: result.saved_birth_time, + birth_utc_offset: result.saved_birth_utc_offset, + birth_lat: result.saved_birth_lat, + birth_lon: result.saved_birth_lon, + lang: result.lang, + notify_enabled: result.notify_enabled === 1, + notify_time: result.saved_notify_time + }); + + Pebble.sendAppMessage({ + BIRTH_YEAR: result.birth_year, + BIRTH_MONTH: result.birth_month, + BIRTH_DAY: result.birth_day, + BIRTH_HOUR: result.birth_hour, + BIRTH_MINUTE: result.birth_minute, + BIRTH_UTC_OFFSET_MINUTES: result.birth_utc_offset_minutes, + BIRTH_LAT_MICRODEG: result.birth_lat_microdeg, + BIRTH_LON_MICRODEG: result.birth_lon_microdeg, + LANG: result.lang, + NOTIFY_ENABLED: result.notify_enabled, + NOTIFY_HOUR: result.notify_hour, + NOTIFY_MINUTE: result.notify_minute + }, function () { + console.log('Deck in a Dash: config sent to watch'); + }, function (err) { + console.log('Deck in a Dash: config send failed: ' + JSON.stringify(err)); + }); +});