Add watch/phone icons and logo, reorder the interpretation report, and stamp every report with the day it's for
build / build (push) Successful in 30s
build / build (push) Successful in 30s
This commit is contained in:
@@ -42,6 +42,12 @@
|
||||
],
|
||||
"resources": {
|
||||
"media": [
|
||||
{
|
||||
"type": "bitmap",
|
||||
"name": "IMAGE_APP_ICON",
|
||||
"file": "app_icon.png",
|
||||
"menuIcon": true
|
||||
},
|
||||
{
|
||||
"type": "bitmap",
|
||||
"name": "IMG_CARD_FOOL",
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Downsizes res/app_icon_50x50_64_color.png into watch/resources/app_icon.png.
|
||||
|
||||
Pebble's own build tooling (process_sdk_resources.py) hard-fails any
|
||||
"menuIcon": true resource larger than 25x25px - the fixed size Pebble OS
|
||||
renders app icons at in the watch's own app launcher list (and, synced
|
||||
from the watch, in the mobile app's installed-apps list too). The source
|
||||
art in res/ is 50x50 (square, so it downsamples to exactly 25x25 with no
|
||||
letterboxing) - the color variant is used, per the same "prefer color"
|
||||
choice as the rest of this app's card art, over the companion
|
||||
*_gray.png fallback also present in res/.
|
||||
|
||||
Generated output, like i18n_tables.auto.c and resources/img/*.png - not
|
||||
committed (see .gitignore), regenerate by running this script directly
|
||||
or via `pebble build` (wired into wscript alongside gen_card_images.py).
|
||||
Requires Pillow (`pip install Pillow`), not part of this repo's own
|
||||
desktop build/test toolchain.
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
|
||||
from PIL import Image
|
||||
|
||||
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
SRC_PATH = os.path.normpath(
|
||||
os.path.join(SCRIPT_DIR, "..", "..", "res", "app_icon_50x50_64_color.png")
|
||||
)
|
||||
OUT_PATH = os.path.normpath(os.path.join(SCRIPT_DIR, "..", "resources", "app_icon.png"))
|
||||
|
||||
MAX_ICON_SIZE = 25 # Pebble's own hard cap for "menuIcon": true resources.
|
||||
|
||||
|
||||
def main():
|
||||
if os.path.exists(OUT_PATH) and os.path.getmtime(OUT_PATH) >= os.path.getmtime(SRC_PATH):
|
||||
print(f"{OUT_PATH} already up to date", file=sys.stderr)
|
||||
return
|
||||
|
||||
os.makedirs(os.path.dirname(OUT_PATH), exist_ok=True)
|
||||
with Image.open(SRC_PATH) as img:
|
||||
ratio = MAX_ICON_SIZE / max(img.width, img.height)
|
||||
size = (round(img.width * ratio), round(img.height * ratio))
|
||||
resized = img.convert("RGBA").resize(size, Image.LANCZOS)
|
||||
resized.save(OUT_PATH, "PNG", optimize=True)
|
||||
|
||||
print(f"wrote {OUT_PATH} ({size[0]}x{size[1]})", file=sys.stderr)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
+111
-23
@@ -31,6 +31,7 @@ static GFont s_font_body;
|
||||
* 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_reading_date[48];
|
||||
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];
|
||||
@@ -53,6 +54,14 @@ typedef struct {
|
||||
|
||||
static ImageSlot s_slots[TAROT_SPREAD_SIZE];
|
||||
|
||||
/* Attitude (0) and Outcome (1) preview slots, drawn at half screen
|
||||
* width each so the two sit side by side - the BitmapLayer's own bounds
|
||||
* (narrower than IMAGE_WIDTH on 144px-wide platforms) clip the centered
|
||||
* image automatically, same as GAlignCenter already does for the
|
||||
* full-width slots above, no separate scaling logic needed. */
|
||||
#define REPEAT_SLOT_COUNT 2
|
||||
static ImageSlot s_repeat_slots[REPEAT_SLOT_COUNT];
|
||||
|
||||
#define MAX_TEXT_LAYERS 50
|
||||
static TextLayer *s_text_layers[MAX_TEXT_LAYERS];
|
||||
static int s_text_layer_count;
|
||||
@@ -75,8 +84,8 @@ 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));
|
||||
static int16_t add_text_at(int16_t x, int16_t width, int16_t y, const char *text, GFont font) {
|
||||
TextLayer *tl = text_layer_create(GRect(x, y, width, 4000));
|
||||
text_layer_set_font(tl, font);
|
||||
text_layer_set_text(tl, text);
|
||||
text_layer_set_overflow_mode(tl, GTextOverflowModeWordWrap);
|
||||
@@ -91,6 +100,10 @@ static int16_t add_text(int16_t width, int16_t y, const char *text, GFont font)
|
||||
return y + size.h + MARGIN;
|
||||
}
|
||||
|
||||
static int16_t add_text(int16_t width, int16_t y, const char *text, GFont font) {
|
||||
return add_text_at(0, width, y, text, font);
|
||||
}
|
||||
|
||||
static void teardown_content(void) {
|
||||
for (int i = 0; i < s_text_layer_count; i++) {
|
||||
text_layer_destroy(s_text_layers[i]);
|
||||
@@ -107,6 +120,30 @@ static void teardown_content(void) {
|
||||
s_slots[i].bitmap_layer = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 0; i < REPEAT_SLOT_COUNT; i++) {
|
||||
if (s_repeat_slots[i].bitmap) {
|
||||
gbitmap_destroy(s_repeat_slots[i].bitmap);
|
||||
s_repeat_slots[i].bitmap = NULL;
|
||||
}
|
||||
if (s_repeat_slots[i].bitmap_layer) {
|
||||
bitmap_layer_destroy(s_repeat_slots[i].bitmap_layer);
|
||||
s_repeat_slots[i].bitmap_layer = NULL;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void update_slot_visibility(ImageSlot *slot, int16_t visible_top, int16_t visible_bottom) {
|
||||
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 update_visible_images(void) {
|
||||
@@ -115,17 +152,10 @@ static void update_visible_images(void) {
|
||||
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;
|
||||
}
|
||||
update_slot_visibility(&s_slots[i], visible_top, visible_bottom);
|
||||
}
|
||||
for (int i = 0; i < REPEAT_SLOT_COUNT; i++) {
|
||||
update_slot_visibility(&s_repeat_slots[i], visible_top, visible_bottom);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -139,25 +169,87 @@ static void build_content(int16_t width) {
|
||||
|
||||
int16_t y = MARGIN;
|
||||
|
||||
/* The day this reading is for (reading->utc_moment, set by
|
||||
* reading_generate() - see reading_state_recompute()), not necessarily
|
||||
* "today" if the report is viewed right at a UTC-midnight boundary
|
||||
* before the next recompute. Plain gmtime(), same justification as
|
||||
* reading_state.c's own use of it. */
|
||||
struct tm *utc = gmtime(&reading->utc_moment);
|
||||
snprintf(s_reading_date, sizeof s_reading_date, "%s %04d-%02d-%02d",
|
||||
i18n_get("interp.reading_date", "Reading for:"), utc->tm_year + 1900,
|
||||
utc->tm_mon + 1, utc->tm_mday);
|
||||
y = add_text(width, y, s_reading_date, s_font_body);
|
||||
|
||||
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 {
|
||||
/* Build every transit's title + narrative up front (used both for the
|
||||
* repeated top-item line below and the full list further down), so
|
||||
* neither buffer is computed twice for item 0. */
|
||||
if (interp->top_item_count > 0) {
|
||||
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, i18n_get("interp.heading_guidance", "Guidance"), s_font_section);
|
||||
|
||||
/* Attitude/Outcome preview, side by side, image only - no per-card
|
||||
* caption text. A "Position: Card Name (Reversed)" label doesn't fit
|
||||
* a half-width column without wrapping into 3+ short lines, and with
|
||||
* both columns wrapping independently the reader ends up reading
|
||||
* fragments across columns rather than down one column at a time
|
||||
* (confirmed on-device: it reads as one garbled interleaved sentence,
|
||||
* not two). Captions are unnecessary anyway - the guidance text
|
||||
* printed immediately below already names and quotes both cards in
|
||||
* full ("Deine Haltungskarte ist ..." / "Das wahrscheinliche Ergebnis
|
||||
* ... ist ..."), and both reappear with their full position/name/
|
||||
* meaning again in the Celtic Cross walkthrough further down. */
|
||||
{
|
||||
int16_t half_w = width / 2;
|
||||
const TarotDraw *attitude = &reading->spread.positions[POSITION_ATTITUDE];
|
||||
const TarotDraw *outcome = &reading->spread.positions[POSITION_OUTCOME];
|
||||
int16_t img_height = card_image_height_for_width(IMAGE_WIDTH);
|
||||
|
||||
s_repeat_slots[0].card = attitude->card;
|
||||
s_repeat_slots[0].y = y;
|
||||
s_repeat_slots[0].height = img_height;
|
||||
s_repeat_slots[0].bitmap_layer = bitmap_layer_create(GRect(0, y, half_w, img_height));
|
||||
bitmap_layer_set_alignment(s_repeat_slots[0].bitmap_layer, GAlignCenter);
|
||||
scroll_layer_add_child(s_scroll_layer, bitmap_layer_get_layer(s_repeat_slots[0].bitmap_layer));
|
||||
|
||||
s_repeat_slots[1].card = outcome->card;
|
||||
s_repeat_slots[1].y = y;
|
||||
s_repeat_slots[1].height = img_height;
|
||||
s_repeat_slots[1].bitmap_layer =
|
||||
bitmap_layer_create(GRect(half_w, y, width - half_w, img_height));
|
||||
bitmap_layer_set_alignment(s_repeat_slots[1].bitmap_layer, GAlignCenter);
|
||||
scroll_layer_add_child(s_scroll_layer, bitmap_layer_get_layer(s_repeat_slots[1].bitmap_layer));
|
||||
|
||||
y += img_height + MARGIN;
|
||||
}
|
||||
|
||||
if (interp->top_item_count > 0) {
|
||||
y = add_text(width, y, s_transit_titles[0], s_font_subhead);
|
||||
y = add_text(width, y, s_narratives[0], s_font_body);
|
||||
}
|
||||
guidance_write(s_guidance, sizeof s_guidance, "", interp, &reading->spread);
|
||||
y = add_text(width, y, s_guidance, 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++) {
|
||||
y = add_text(width, y, s_transit_titles[i], s_font_subhead);
|
||||
y = add_text(width, y, s_narratives[i], s_font_body);
|
||||
}
|
||||
}
|
||||
@@ -184,10 +276,6 @@ static void build_content(int16_t width) {
|
||||
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));
|
||||
}
|
||||
|
||||
|
||||
+12
-1
File diff suppressed because one or more lines are too long
+10
-7
@@ -63,15 +63,18 @@ def configure(ctx):
|
||||
def build(ctx):
|
||||
ctx.load('pebble_sdk')
|
||||
|
||||
# Regenerate src/c/i18n_tables.auto.c from engine/i18n/*.lang, and
|
||||
# resources/img/*.png from res/img/*.jpeg, before compiling/bundling
|
||||
# anything - see each script's own doc comment. Both run eagerly (not
|
||||
# as waf Tasks) since the bundle step below depends on their output
|
||||
# existing; gen_card_images.py skips any card whose PNG is already
|
||||
# up to date, so this stays cheap on repeat builds. Requires Pillow
|
||||
# (`pip install Pillow`) - see watch/README.md.
|
||||
# Regenerate src/c/i18n_tables.auto.c from engine/i18n/*.lang,
|
||||
# resources/img/*.png from res/img/*.jpeg, and resources/app_icon.png
|
||||
# from res/app_icon_50x50_64_color.png, before compiling/bundling
|
||||
# anything - see each script's own doc comment. All three run eagerly
|
||||
# (not as waf Tasks) since the bundle step below depends on their
|
||||
# output existing; gen_card_images.py/gen_app_icon.py both skip
|
||||
# regenerating anything already up to date, so this stays cheap on
|
||||
# repeat builds. Requires Pillow (`pip install Pillow`) - see
|
||||
# watch/README.md.
|
||||
subprocess.check_call([sys.executable, 'scripts/gen_i18n_tables.py'])
|
||||
subprocess.check_call([sys.executable, 'scripts/gen_card_images.py'])
|
||||
subprocess.check_call([sys.executable, 'scripts/gen_app_icon.py'])
|
||||
|
||||
engine_dir = ctx.path.parent.find_dir('engine/src')
|
||||
engine_nodes = [engine_dir.find_node(name) for name in ENGINE_SRCS]
|
||||
|
||||
Reference in New Issue
Block a user