diff --git a/countdown_watchface/package.json b/countdown_watchface/package.json index 465c4fe..9dd82ee 100644 --- a/countdown_watchface/package.json +++ b/countdown_watchface/package.json @@ -23,7 +23,9 @@ "watchface": true }, "messageKeys": [ - "APPOINTMENT_MINUTES" + "BAR_CHUNK_MINS", + "DAY_START_HOUR", + "DAY_END_HOUR" ], "resources": { "media": [ diff --git a/countdown_watchface/src/c/watchface.c b/countdown_watchface/src/c/watchface.c index 095fa90..93817c0 100644 --- a/countdown_watchface/src/c/watchface.c +++ b/countdown_watchface/src/c/watchface.c @@ -1,64 +1,53 @@ #include +#define PERSIST_KEY_DAY_START 1 +#define PERSIST_KEY_DAY_END 2 +#define PERSIST_KEY_CHUNK_MINS 3 + static Window *s_main_window; static TextLayer *s_time_layer; static TextLayer *s_date_layer; static Layer *s_bar_layer; +static Layer *s_day_bar_layer; static Layer *s_battery_layer; -static Layer *s_calendar_layer; static GFont s_date_font; -static bool s_tracking_appt = false; - static const char * const MONTHS[] = { "JAN","FEB","MAR","APR","MAY","JUN", "JUL","AUG","SEP","OCT","NOV","DEC" }; static int s_battery_level = 100; -static int s_bar_fill = 30; // 0–30: minutes remaining -static int s_appt_mins = -1; // minutes to next appointment, -1 = none within 30 min +static int s_bar_fill = 30; +static int s_day_bar_fill = 16; -static int mins_to_next_half(void) { +static int s_day_start_hour = 6; +static int s_day_end_hour = 22; +static int s_bar_chunk_mins = 30; + +static int mins_to_next_chunk(void) { time_t now = time(NULL); struct tm *t = localtime(&now); - return 30 - (t->tm_min % 30); // returns 1–30 + return s_bar_chunk_mins - (t->tm_min % s_bar_chunk_mins); } -static void update_bar(void) { - int half_mins = mins_to_next_half(); - int target = half_mins; - - s_tracking_appt = (s_appt_mins >= 0 && s_appt_mins < half_mins); - if (s_tracking_appt) { - target = s_appt_mins; - } - - s_bar_fill = target; - if (s_bar_fill > 30) s_bar_fill = 30; - if (s_bar_fill < 0) s_bar_fill = 0; - - layer_mark_dirty(s_bar_layer); - layer_mark_dirty(s_calendar_layer); -} - -static void bar_draw(Layer *layer, GContext *ctx) { - GRect bounds = layer_get_bounds(layer); - +static void draw_progress_bar(GContext *ctx, GRect bounds, int num_blocks, int fill) { graphics_context_set_fill_color(ctx, GColorBlack); graphics_fill_rect(ctx, bounds, 0, GCornerNone); - // Fit 30 blocks with 1px gaps; centre the result - int block_w = (bounds.size.w - 29) / 30; - int total_w = 30 * block_w + 29; + if (num_blocks <= 0) return; + + int gap = 1; + int block_w = (bounds.size.w - (num_blocks - 1) * gap) / num_blocks; + if (block_w < 1) block_w = 1; + int total_w = num_blocks * block_w + (num_blocks - 1) * gap; int start_x = (bounds.size.w - total_w) / 2; - for (int i = 0; i < 30; i++) { - int x = start_x + i * (block_w + 1); - GRect block = GRect(x, 0, block_w, bounds.size.h); - if (i >= 30 - s_bar_fill) { + for (int i = 0; i < num_blocks; i++) { + int x = start_x + i * (block_w + gap); + if (i >= num_blocks - fill) { graphics_context_set_fill_color(ctx, GColorWhite); - graphics_fill_rect(ctx, block, 0, GCornerNone); + graphics_fill_rect(ctx, GRect(x, 0, block_w, bounds.size.h), 0, GCornerNone); } else { graphics_context_set_fill_color(ctx, GColorWhite); graphics_fill_rect(ctx, GRect(x, bounds.size.h - 1, block_w, 1), 0, GCornerNone); @@ -66,6 +55,41 @@ static void bar_draw(Layer *layer, GContext *ctx) { } } +static void bar_draw(Layer *layer, GContext *ctx) { + draw_progress_bar(ctx, layer_get_bounds(layer), s_bar_chunk_mins, s_bar_fill); +} + +static void day_bar_draw(Layer *layer, GContext *ctx) { + draw_progress_bar(ctx, layer_get_bounds(layer), + s_day_end_hour - s_day_start_hour, s_day_bar_fill); +} + +static void update_bar(void) { + time_t now = time(NULL); + struct tm *t = localtime(&now); + + // Top bar: countdown to next chunk boundary + int chunk_mins = mins_to_next_chunk(); + s_bar_fill = chunk_mins < 0 ? 0 : (chunk_mins > s_bar_chunk_mins ? s_bar_chunk_mins : chunk_mins); + + // Day bar: whole hours remaining (ceiling) within the configured window + int total_blocks = s_day_end_hour - s_day_start_hour; + int current_mins = t->tm_hour * 60 + t->tm_min; + int mins_left = s_day_end_hour * 60 - current_mins; + + if (mins_left <= 0) { + s_day_bar_fill = 0; + } else if (current_mins < s_day_start_hour * 60) { + s_day_bar_fill = total_blocks; + } else { + s_day_bar_fill = (mins_left + 59) / 60; + if (s_day_bar_fill > total_blocks) s_day_bar_fill = total_blocks; + } + + layer_mark_dirty(s_bar_layer); + layer_mark_dirty(s_day_bar_layer); +} + static void battery_draw(Layer *layer, GContext *ctx) { GRect bounds = layer_get_bounds(layer); graphics_context_set_fill_color(ctx, GColorBlack); @@ -73,43 +97,38 @@ static void battery_draw(Layer *layer, GContext *ctx) { if (s_battery_level >= 20) return; - // Body outline graphics_context_set_stroke_color(ctx, GColorWhite); graphics_draw_rect(ctx, GRect(0, 1, 19, 10)); - // Terminal nub graphics_context_set_fill_color(ctx, GColorWhite); graphics_fill_rect(ctx, GRect(19, 4, 3, 4), 0, GCornerNone); - // Charge fill int fill_w = (s_battery_level * 15) / 100; if (fill_w > 0) { graphics_fill_rect(ctx, GRect(2, 3, fill_w, 6), 0, GCornerNone); } } -static void calendar_draw(Layer *layer, GContext *ctx) { - GRect bounds = layer_get_bounds(layer); - graphics_context_set_fill_color(ctx, GColorBlack); - graphics_fill_rect(ctx, bounds, 0, GCornerNone); - - if (!s_tracking_appt) return; - - // Outline - graphics_context_set_stroke_color(ctx, GColorWhite); - graphics_draw_rect(ctx, GRect(0, 0, 12, 12)); - // Header bar - graphics_context_set_fill_color(ctx, GColorWhite); - graphics_fill_rect(ctx, GRect(1, 1, 10, 3), 0, GCornerNone); - // Two date squares - graphics_fill_rect(ctx, GRect(2, 6, 3, 3), 0, GCornerNone); - graphics_fill_rect(ctx, GRect(7, 6, 3, 3), 0, GCornerNone); -} - static void inbox_received(DictionaryIterator *iter, void *context) { - Tuple *t = dict_find(iter, MESSAGE_KEY_APPOINTMENT_MINUTES); + Tuple *t; + + t = dict_find(iter, MESSAGE_KEY_DAY_START_HOUR); if (t) { - s_appt_mins = (int)t->value->int32; - update_bar(); + s_day_start_hour = (int)t->value->int32; + persist_write_int(PERSIST_KEY_DAY_START, s_day_start_hour); } + + t = dict_find(iter, MESSAGE_KEY_DAY_END_HOUR); + if (t) { + s_day_end_hour = (int)t->value->int32; + persist_write_int(PERSIST_KEY_DAY_END, s_day_end_hour); + } + + t = dict_find(iter, MESSAGE_KEY_BAR_CHUNK_MINS); + if (t) { + s_bar_chunk_mins = (int)t->value->int32; + persist_write_int(PERSIST_KEY_CHUNK_MINS, s_bar_chunk_mins); + } + + update_bar(); } static void update_display(struct tm *tick_time) { @@ -137,13 +156,11 @@ static void main_window_load(Window *window) { Layer *root = window_get_root_layer(window); GRect bounds = layer_get_bounds(root); - // Shared horizontal geometry matching the bar's block layout + // Text area aligned to 30-block bar width (middle setting) int block_w = (bounds.size.w - 29) / 30; int bar_w = 30 * block_w + 29; int bar_x = (bounds.size.w - bar_w) / 2; - // Time — right-aligned to bar, almost touching bar - // Leco 60 is emery-exclusive; fall back to Leco 42 on other platforms #ifdef PBL_PLATFORM_EMERY s_time_layer = text_layer_create(GRect(bar_x, 18, bar_w, 68)); text_layer_set_font(s_time_layer, fonts_get_system_font(FONT_KEY_LECO_60_BOLD_NUMBERS_AM_PM)); @@ -157,14 +174,19 @@ static void main_window_load(Window *window) { text_layer_set_text(s_time_layer, "00:00"); layer_add_child(root, text_layer_get_layer(s_time_layer)); - // Countdown bar — 30 blocks, right-to-left - s_bar_layer = layer_create(GRect(0, 90, bounds.size.w, 14)); + // Chunk countdown bar + s_bar_layer = layer_create(GRect(0, 90, bounds.size.w, 10)); layer_set_update_proc(s_bar_layer, bar_draw); layer_add_child(root, s_bar_layer); - // Date — Roboto Bold 36 custom font, right-aligned to bar + // Day progress bar + s_day_bar_layer = layer_create(GRect(0, 102, bounds.size.w, 10)); + layer_set_update_proc(s_day_bar_layer, day_bar_draw); + layer_add_child(root, s_day_bar_layer); + + // Date s_date_font = fonts_load_custom_font(resource_get_handle(RESOURCE_ID_FONT_ROBOTO_BOLD_36)); - s_date_layer = text_layer_create(GRect(bar_x, 106, bar_w, 42)); + s_date_layer = text_layer_create(GRect(bar_x, 114, bar_w, 40)); text_layer_set_background_color(s_date_layer, GColorClear); text_layer_set_text_color(s_date_layer, GColorWhite); text_layer_set_font(s_date_layer, s_date_font); @@ -172,13 +194,8 @@ static void main_window_load(Window *window) { text_layer_set_text(s_date_layer, "1 JAN"); layer_add_child(root, text_layer_get_layer(s_date_layer)); - // Calendar icon — bottom right, visible when counting down to an appointment - s_calendar_layer = layer_create(GRect(bounds.size.w - 42, bounds.size.h - 16, 12, 12)); - layer_set_update_proc(s_calendar_layer, calendar_draw); - layer_add_child(root, s_calendar_layer); - // Battery icon — bottom right, only visible below 20% - s_battery_layer = layer_create(GRect(bounds.size.w - 26, bounds.size.h - 16, 23, 12)); + s_battery_layer = layer_create(GRect(bounds.size.w - 26, bounds.size.h - 14, 23, 12)); layer_set_update_proc(s_battery_layer, battery_draw); layer_add_child(root, s_battery_layer); } @@ -187,12 +204,16 @@ static void main_window_unload(Window *window) { text_layer_destroy(s_time_layer); text_layer_destroy(s_date_layer); layer_destroy(s_bar_layer); - layer_destroy(s_calendar_layer); + layer_destroy(s_day_bar_layer); layer_destroy(s_battery_layer); fonts_unload_custom_font(s_date_font); } static void init(void) { + s_day_start_hour = persist_exists(PERSIST_KEY_DAY_START) ? persist_read_int(PERSIST_KEY_DAY_START) : 6; + s_day_end_hour = persist_exists(PERSIST_KEY_DAY_END) ? persist_read_int(PERSIST_KEY_DAY_END) : 22; + s_bar_chunk_mins = persist_exists(PERSIST_KEY_CHUNK_MINS) ? persist_read_int(PERSIST_KEY_CHUNK_MINS) : 30; + s_main_window = window_create(); window_set_background_color(s_main_window, GColorBlack); @@ -207,7 +228,7 @@ static void init(void) { battery_callback(battery_state_service_peek()); app_message_register_inbox_received(inbox_received); - app_message_open(64, 0); + app_message_open(128, 0); time_t now = time(NULL); update_display(localtime(&now)); diff --git a/countdown_watchface/src/pkjs/index.js b/countdown_watchface/src/pkjs/index.js index b9dda3b..4b8d2d2 100644 --- a/countdown_watchface/src/pkjs/index.js +++ b/countdown_watchface/src/pkjs/index.js @@ -1,64 +1,87 @@ -// Calendar integration via Google Calendar API. -// To enable: store your API key with: -// localStorage.setItem('gcal_api_key', 'YOUR_API_KEY'); -// A public/service-account read-only key for your calendar is sufficient. -// Without it, the bar only counts down to the next :00 or :30. +// --- Configuration --- -var CALENDAR_ID = 'primary'; +function getStoredConfig() { + return { + chunk: parseInt(localStorage.getItem('chunk') || '30', 10), + start: parseInt(localStorage.getItem('start') || '6', 10), + end: parseInt(localStorage.getItem('end') || '22', 10) + }; +} -function sendAppointmentMinutes(minutes) { +function sendConfig(cfg) { Pebble.sendAppMessage( - { 'APPOINTMENT_MINUTES': minutes }, + { + 'BAR_CHUNK_MINS': cfg.chunk, + 'DAY_START_HOUR': cfg.start, + 'DAY_END_HOUR': cfg.end + }, function() {}, - function(e) { console.log('AppMessage send failed: ' + JSON.stringify(e)); } + function(e) { console.log('Config send failed: ' + JSON.stringify(e)); } ); } -function checkCalendar() { - var apiKey = localStorage.getItem('gcal_api_key'); - if (!apiKey) { - sendAppointmentMinutes(-1); - return; - } - - var now = new Date(); - var horizon = new Date(now.getTime() + 30 * 60 * 1000); - - var url = 'https://www.googleapis.com/calendar/v3/calendars/' + - encodeURIComponent(CALENDAR_ID) + '/events' + - '?key=' + apiKey + - '&timeMin=' + now.toISOString() + - '&timeMax=' + horizon.toISOString() + - '&singleEvents=true&orderBy=startTime&maxResults=1'; - - var xhr = new XMLHttpRequest(); - xhr.onload = function() { - try { - if (xhr.status !== 200) { - sendAppointmentMinutes(-1); - return; - } - var data = JSON.parse(xhr.responseText); - if (data.items && data.items.length > 0) { - var item = data.items[0]; - var start = new Date(item.start.dateTime || item.start.date); - var mins = Math.round((start - now) / 60000); - sendAppointmentMinutes(Math.max(0, Math.min(30, mins))); - } else { - sendAppointmentMinutes(-1); - } - } catch (e) { - sendAppointmentMinutes(-1); - } - }; - xhr.onerror = function() { - sendAppointmentMinutes(-1); - }; - xhr.open('GET', url); - xhr.send(); +function buildConfigPage(chunk, start, end) { + return '' + + '' + + '' + + '' + + 'Watchface Settings' + + '' + + '

Watchface Settings

' + + '' + + '' + + '' + + '
' + + '