├── .gitignore ├── README.md ├── package-lock.json ├── package.json ├── screenshots ├── aplite-1.png ├── basalt-1.png ├── basalt-charging-1.png ├── basalt-steps.png ├── chalk-steps.png └── chalk.png ├── src ├── js │ ├── config.js │ └── index.js ├── main.c ├── modules │ ├── comm.c │ ├── comm.h │ ├── data.c │ ├── data.h │ ├── health.c │ └── health.h └── windows │ ├── main_window.c │ └── main_window.h └── wscript /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Ignore build generated files 3 | build 4 | 5 | .lock-* 6 | releases 7 | assets 8 | *.DS_Store 9 | node_modules/ 10 | .vscode/** 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # thin 2 | 3 | Simple, customizable Pebble watchface with splashes of color. 4 | 5 | Runs on all Pebble watches. 6 | 7 | ![](screenshots/aplite-1.png) ![](screenshots/basalt-1.png) 8 | 9 | Use the config page (powered by [Clay](https://github.com/pebble/clay) to 10 | adjust which features you want from the following: 11 | 12 | - Show weekday/month 13 | - Show day of the month 14 | - Show dis/connected indicator 15 | - Show battery level 16 | - Show seconds hand 17 | - Show step count 18 | 19 | ![](screenshots/basalt-charging-1.png) 20 | ![](screenshots/basalt-steps.png) 21 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Thin", 3 | "version": "1.10.0", 4 | "lockfileVersion": 2, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "Thin", 9 | "version": "1.10.0", 10 | "dependencies": { 11 | "pebble-clay": "^1.0.4" 12 | } 13 | }, 14 | "node_modules/pebble-clay": { 15 | "version": "1.0.4", 16 | "resolved": "https://registry.npmjs.org/pebble-clay/-/pebble-clay-1.0.4.tgz", 17 | "integrity": "sha1-/fkvD9x3CpecBodOqiRXzC52I0Q=" 18 | } 19 | }, 20 | "dependencies": { 21 | "pebble-clay": { 22 | "version": "1.0.4", 23 | "resolved": "https://registry.npmjs.org/pebble-clay/-/pebble-clay-1.0.4.tgz", 24 | "integrity": "sha1-/fkvD9x3CpecBodOqiRXzC52I0Q=" 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.10.0", 3 | "author": "Chris Lewis", 4 | "private": true, 5 | "dependencies": { 6 | "pebble-clay": "^1.0.4" 7 | }, 8 | "keywords": [ 9 | "pebble-app" 10 | ], 11 | "pebble": { 12 | "sdkVersion": "3", 13 | "resources": { 14 | "media": [] 15 | }, 16 | "projectType": "native", 17 | "uuid": "09988afe-adbb-4941-a126-9b2c92aea311", 18 | "messageKeys": { 19 | "DataKeyBattery": 3, 20 | "DataKeySecondHand": 4, 21 | "DataKeyDate": 0, 22 | "DataKeyDay": 1, 23 | "DataKeyBT": 2, 24 | "DataKeySteps": 5 25 | }, 26 | "enableMultiJS": true, 27 | "displayName": "Thin", 28 | "watchapp": { 29 | "onlyShownOnCommunication": false, 30 | "hiddenApp": false, 31 | "watchface": true 32 | }, 33 | "targetPlatforms": [ 34 | "aplite", 35 | "basalt", 36 | "chalk" 37 | ], 38 | "capabilities": [ 39 | "configurable" 40 | ] 41 | }, 42 | "name": "Thin" 43 | } 44 | -------------------------------------------------------------------------------- /screenshots/aplite-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/C-D-Lewis/thin/1c96ee90dc8fea3f289a8c78707e9dcfa9119ec9/screenshots/aplite-1.png -------------------------------------------------------------------------------- /screenshots/basalt-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/C-D-Lewis/thin/1c96ee90dc8fea3f289a8c78707e9dcfa9119ec9/screenshots/basalt-1.png -------------------------------------------------------------------------------- /screenshots/basalt-charging-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/C-D-Lewis/thin/1c96ee90dc8fea3f289a8c78707e9dcfa9119ec9/screenshots/basalt-charging-1.png -------------------------------------------------------------------------------- /screenshots/basalt-steps.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/C-D-Lewis/thin/1c96ee90dc8fea3f289a8c78707e9dcfa9119ec9/screenshots/basalt-steps.png -------------------------------------------------------------------------------- /screenshots/chalk-steps.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/C-D-Lewis/thin/1c96ee90dc8fea3f289a8c78707e9dcfa9119ec9/screenshots/chalk-steps.png -------------------------------------------------------------------------------- /screenshots/chalk.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/C-D-Lewis/thin/1c96ee90dc8fea3f289a8c78707e9dcfa9119ec9/screenshots/chalk.png -------------------------------------------------------------------------------- /src/js/config.js: -------------------------------------------------------------------------------- 1 | module.exports = [ 2 | { 3 | "type": "heading", 4 | "defaultValue": "Thin Configuration" , 5 | "size": 3 6 | }, 7 | { 8 | "type": "section", 9 | "items": [ 10 | { 11 | "type": "heading", 12 | "defaultValue": "Features" 13 | }, 14 | { 15 | "type": "text", 16 | "defaultValue": "Turn additional features on or off." 17 | }, 18 | { 19 | "type": "toggle", 20 | "label": "Show weekday and month", 21 | "messageKey": "DataKeyDate", 22 | "defaultValue": true 23 | }, 24 | { 25 | "type": "toggle", 26 | "label": "Show day of the month", 27 | "messageKey": "DataKeyDay", 28 | "defaultValue": true 29 | }, 30 | { 31 | "type": "toggle", 32 | "label": "Show disconnected indicator", 33 | "messageKey": "DataKeyBT", 34 | "defaultValue": true 35 | }, 36 | { 37 | "type": "toggle", 38 | "label": "Show battery level (hour markers)", 39 | "messageKey": "DataKeyBattery", 40 | "defaultValue": true 41 | }, 42 | { 43 | "type": "toggle", 44 | "label": "Show second hand (uses more power)", 45 | "messageKey": "DataKeySecondHand", 46 | "defaultValue": true 47 | }, 48 | { 49 | "type": "toggle", 50 | "label": "Show step count", 51 | "messageKey": "DataKeySteps", 52 | "defaultValue": true 53 | } 54 | ] 55 | }, 56 | { 57 | "type": "submit", 58 | "defaultValue": "Save" 59 | } 60 | ] 61 | -------------------------------------------------------------------------------- /src/js/index.js: -------------------------------------------------------------------------------- 1 | var Clay = require('pebble-clay'); 2 | var clayConfig = require('./config'); 3 | var clay = new Clay(clayConfig); -------------------------------------------------------------------------------- /src/main.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "modules/comm.h" 4 | #include "modules/data.h" 5 | #include "windows/main_window.h" 6 | 7 | static void init() { 8 | setlocale(LC_ALL, ""); 9 | comm_init(); 10 | data_init(); 11 | 12 | if (data_get(DataKeySteps)) { 13 | health_init(); 14 | } 15 | 16 | main_window_push(); 17 | } 18 | 19 | static void deinit() { 20 | data_deinit(); 21 | } 22 | 23 | int main() { 24 | init(); 25 | app_event_loop(); 26 | deinit(); 27 | } 28 | -------------------------------------------------------------------------------- /src/modules/comm.c: -------------------------------------------------------------------------------- 1 | #include "comm.h" 2 | 3 | static void in_recv_handler(DictionaryIterator *iter, void *context) { 4 | Tuple *t = dict_read_first(iter); 5 | while(t) { 6 | data_set(t->key, t->value->int32); 7 | t = dict_read_next(iter); 8 | } 9 | 10 | main_window_reload_config(); 11 | } 12 | 13 | void comm_init() { 14 | const int buffer_size = 128; 15 | app_message_register_inbox_received(in_recv_handler); 16 | app_message_open(buffer_size, buffer_size); 17 | } 18 | -------------------------------------------------------------------------------- /src/modules/comm.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include "data.h" 6 | 7 | #include "../windows/main_window.h" 8 | 9 | void comm_init(); 10 | -------------------------------------------------------------------------------- /src/modules/data.c: -------------------------------------------------------------------------------- 1 | #include "data.h" 2 | 3 | #define PERSIST_SET 3489 4 | 5 | // Cache for speed 6 | static bool s_arr[DataKeyCount]; 7 | 8 | void data_init() { 9 | if(!persist_exists(PERSIST_SET)) { 10 | // Set defaults 11 | s_arr[DataKeyDate] = true; 12 | s_arr[DataKeyDay] = true; 13 | s_arr[DataKeyBT] = true; 14 | s_arr[DataKeyBattery] = true; 15 | s_arr[DataKeySecondHand] = true; 16 | s_arr[DataKeySteps] = true; 17 | } else { 18 | // Read previously stored values 19 | for(int i = 0; i < DataKeyCount; i++) { 20 | s_arr[i] = persist_read_bool(i); 21 | } 22 | } 23 | } 24 | 25 | void data_deinit() { 26 | // Save current values 27 | persist_write_bool(PERSIST_SET, true); 28 | 29 | for(int i = 0; i < DataKeyCount; i++) { 30 | persist_write_bool(i, s_arr[i]); 31 | } 32 | } 33 | 34 | void data_set(int key, bool value) { 35 | s_arr[key] = value; 36 | } 37 | 38 | bool data_get(int key) { 39 | return s_arr[key]; 40 | } 41 | -------------------------------------------------------------------------------- /src/modules/data.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | typedef enum { 6 | DataKeyDate = 0, 7 | DataKeyDay, 8 | DataKeyBT, 9 | DataKeyBattery, 10 | DataKeySecondHand, 11 | DataKeySteps, 12 | 13 | DataKeyCount 14 | } DataKey; 15 | 16 | void data_init(); 17 | 18 | void data_deinit(); 19 | 20 | void data_set(int key, bool value); 21 | 22 | bool data_get(int key); 23 | -------------------------------------------------------------------------------- /src/modules/health.c: -------------------------------------------------------------------------------- 1 | #include "health.h" 2 | 3 | static bool s_health_available; 4 | static int s_step_count = 0; 5 | 6 | static void health_handler(HealthEventType event, void *context) { 7 | if(event != HealthEventSleepUpdate) { 8 | s_step_count = (int)health_service_sum_today(HealthMetricStepCount); 9 | } 10 | } 11 | 12 | void health_init() { 13 | s_health_available = health_service_events_subscribe(health_handler, NULL); 14 | 15 | if (s_health_available) { 16 | s_step_count = (int)health_service_sum_today(HealthMetricStepCount); 17 | } 18 | } 19 | 20 | bool is_health_available() { 21 | return s_health_available; 22 | } 23 | 24 | int get_step_count() { 25 | return s_step_count; 26 | } 27 | 28 | bool step_data_is_available() { 29 | return is_health_available() && (HealthServiceAccessibilityMaskAvailable & 30 | health_service_metric_accessible(HealthMetricStepCount, 31 | time_start_of_today(), time(NULL))); 32 | } 33 | -------------------------------------------------------------------------------- /src/modules/health.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include "data.h" 6 | 7 | void health_init(); 8 | 9 | bool is_health_available(); 10 | 11 | int get_step_count(); 12 | 13 | bool step_data_is_available(); -------------------------------------------------------------------------------- /src/windows/main_window.c: -------------------------------------------------------------------------------- 1 | #include "main_window.h" 2 | 3 | #define MARGIN 5 4 | #define THICKNESS 3 5 | #define ANIMATION_DELAY 300 6 | #define ANIMATION_DURATION 1000 7 | #define HAND_LENGTH_SEC 65 8 | #define HAND_LENGTH_MIN HAND_LENGTH_SEC 9 | #define HAND_LENGTH_HOUR (HAND_LENGTH_SEC - 20) 10 | 11 | typedef struct { 12 | int days; 13 | int hours; 14 | int minutes; 15 | int seconds; 16 | } SimpleTime; 17 | 18 | static Window *s_main_window; 19 | static TextLayer *s_weekday_layer, *s_day_in_month_layer, *s_month_layer, *s_step_layer; 20 | static Layer *s_canvas_layer, *s_bg_layer; 21 | 22 | // One each of these to represent the current time and an animated pseudo-time 23 | static SimpleTime s_current_time, s_anim_time; 24 | 25 | static char s_weekday_buffer[8], s_month_buffer[8], s_day_buffer[3], s_current_steps_buffer[16]; 26 | static bool s_animating, s_is_connected; 27 | 28 | /******************************* Event Services *******************************/ 29 | 30 | static void tick_handler(struct tm *tick_time, TimeUnits changed) { 31 | s_current_time.days = tick_time->tm_mday; 32 | s_current_time.hours = tick_time->tm_hour; 33 | s_current_time.minutes = tick_time->tm_min; 34 | s_current_time.seconds = tick_time->tm_sec; 35 | 36 | s_current_time.hours -= (s_current_time.hours > 12) ? 12 : 0; 37 | 38 | snprintf(s_day_buffer, sizeof(s_day_buffer), "%d", s_current_time.days); 39 | strftime(s_weekday_buffer, sizeof(s_weekday_buffer), "%a", tick_time); 40 | strftime(s_month_buffer, sizeof(s_month_buffer), "%b", tick_time); 41 | 42 | int steps = get_step_count(); 43 | memset(s_current_steps_buffer, 0, strlen(s_current_steps_buffer)); 44 | while (steps > 0) { 45 | int digit = steps % 10; 46 | steps = steps / 10; 47 | char buffer[4]; 48 | snprintf(buffer, sizeof(buffer), "%d\n", digit); 49 | strcat(buffer, s_current_steps_buffer); 50 | strcpy(s_current_steps_buffer, buffer); 51 | } 52 | 53 | text_layer_set_text(s_weekday_layer, s_weekday_buffer); 54 | text_layer_set_text(s_day_in_month_layer, s_day_buffer); 55 | text_layer_set_text(s_month_layer, s_month_buffer); 56 | text_layer_set_text(s_step_layer, s_current_steps_buffer); 57 | center_step_layer(); 58 | 59 | // Finally 60 | layer_mark_dirty(s_canvas_layer); 61 | } 62 | 63 | static void bt_handler(bool connected) { 64 | // Notify disconnection 65 | if(!connected && s_is_connected) { 66 | vibes_long_pulse(); 67 | } 68 | 69 | s_is_connected = connected; 70 | layer_mark_dirty(s_canvas_layer); 71 | } 72 | 73 | static void batt_handler(BatteryChargeState state) { 74 | layer_mark_dirty(s_canvas_layer); 75 | } 76 | 77 | /************************** AnimationImplementation ***************************/ 78 | 79 | static void animation_started(Animation *anim, void *context) { 80 | s_animating = true; 81 | } 82 | 83 | static void animation_stopped(Animation *anim, bool stopped, void *context) { 84 | s_animating = false; 85 | 86 | main_window_reload_config(); 87 | } 88 | 89 | static void animate(int duration, int delay, AnimationImplementation *implementation, bool handlers) { 90 | Animation *anim = animation_create(); 91 | if(anim) { 92 | animation_set_duration(anim, duration); 93 | animation_set_delay(anim, delay); 94 | animation_set_curve(anim, AnimationCurveEaseInOut); 95 | animation_set_implementation(anim, implementation); 96 | if(handlers) { 97 | animation_set_handlers(anim, (AnimationHandlers) { 98 | .started = animation_started, 99 | .stopped = animation_stopped 100 | }, NULL); 101 | } 102 | animation_schedule(anim); 103 | } 104 | } 105 | 106 | /****************************** Drawing Functions *****************************/ 107 | 108 | static void bg_update_proc(Layer *layer, GContext *ctx) { 109 | GRect bounds = layer_get_bounds(layer); 110 | GPoint center = grect_center_point(&bounds); 111 | 112 | BatteryChargeState state = battery_state_service_peek(); 113 | bool plugged = state.is_plugged; 114 | int perc = state.charge_percent; 115 | int batt_hours = (int)(12.0F * ((float)perc / 100.0F)) + 1; 116 | 117 | for(int h = 0; h < 12; h++) { 118 | for(int y = 0; y < THICKNESS; y++) { 119 | for(int x = 0; x < THICKNESS; x++) { 120 | GPoint point = (GPoint) { 121 | .x = (int16_t)(sin_lookup(TRIG_MAX_ANGLE * h / 12) * (int32_t)(3 * HAND_LENGTH_SEC) / TRIG_MAX_RATIO) + center.x, 122 | .y = (int16_t)(-cos_lookup(TRIG_MAX_ANGLE * h / 12) * (int32_t)(3 * HAND_LENGTH_SEC) / TRIG_MAX_RATIO) + center.y, 123 | }; 124 | 125 | if(data_get(DataKeyBattery)) { 126 | if(h < batt_hours) { 127 | #ifdef PBL_COLOR 128 | if(plugged) { 129 | // Charging 130 | graphics_context_set_stroke_color(ctx, GColorGreen); 131 | } else { 132 | // Discharging at this level 133 | graphics_context_set_stroke_color(ctx, GColorWhite); 134 | } 135 | #else 136 | graphics_context_set_stroke_color(ctx, GColorWhite); 137 | #endif 138 | } else { 139 | // Empty segment 140 | graphics_context_set_stroke_color(ctx, PBL_IF_COLOR_ELSE(GColorDarkGray, GColorBlack)); 141 | } 142 | } else { 143 | // No battery indicator, show all 144 | graphics_context_set_stroke_color(ctx, GColorWhite); 145 | } 146 | graphics_draw_line(ctx, GPoint(center.x + x, center.y + y), GPoint(point.x + x, point.y + y)); 147 | } 148 | } 149 | } 150 | 151 | // Make markers 152 | graphics_context_set_fill_color(ctx, GColorBlack); 153 | #ifdef PBL_ROUND 154 | graphics_fill_circle(ctx, center, (bounds.size.w / 2) - (2 * MARGIN)); 155 | #else 156 | graphics_fill_rect(ctx, GRect(MARGIN, MARGIN, bounds.size.w - (2 * MARGIN), bounds.size.h - (2 * MARGIN)), 0, GCornerNone); 157 | #endif 158 | } 159 | 160 | static GPoint make_hand_point(int quantity, int intervals, int len, GPoint center) { 161 | return (GPoint) { 162 | .x = (int16_t)(sin_lookup(TRIG_MAX_ANGLE * quantity / intervals) * (int32_t)len / TRIG_MAX_RATIO) + center.x, 163 | .y = (int16_t)(-cos_lookup(TRIG_MAX_ANGLE * quantity / intervals) * (int32_t)len / TRIG_MAX_RATIO) + center.y, 164 | }; 165 | } 166 | 167 | static int hours_to_minutes(int hours_out_of_12) { 168 | return (hours_out_of_12 * 60) / 12; 169 | } 170 | 171 | static void draw_proc(Layer *layer, GContext *ctx) { 172 | GRect bounds = layer_get_bounds(layer); 173 | GPoint center = grect_center_point(&bounds); 174 | 175 | SimpleTime mode_time = (s_animating) ? s_anim_time : s_current_time; 176 | 177 | int len_sec = HAND_LENGTH_SEC; 178 | int len_min = HAND_LENGTH_MIN; 179 | int len_hour = HAND_LENGTH_HOUR; 180 | 181 | // Plot hand ends 182 | GPoint second_hand_long = make_hand_point(mode_time.seconds, 60, len_sec, center); 183 | GPoint minute_hand_long = make_hand_point(mode_time.minutes, 60, len_min, center); 184 | 185 | // Plot shorter overlaid hands 186 | len_sec -= (MARGIN + 2); 187 | len_min -= (MARGIN + 2); 188 | GPoint second_hand_short = make_hand_point(mode_time.seconds, 60, len_sec, center); 189 | GPoint minute_hand_short = make_hand_point(mode_time.minutes, 60, len_min, center); 190 | 191 | float minute_angle = TRIG_MAX_ANGLE * mode_time.minutes / 60; 192 | float hour_angle; 193 | if(s_animating) { 194 | // Hours out of 60 for smoothness 195 | hour_angle = TRIG_MAX_ANGLE * mode_time.hours / 60; 196 | } else { 197 | hour_angle = TRIG_MAX_ANGLE * mode_time.hours / 12; 198 | } 199 | hour_angle += (minute_angle / TRIG_MAX_ANGLE) * (TRIG_MAX_ANGLE / 12); 200 | 201 | // Hour is more accurate 202 | GPoint hour_hand_long = (GPoint) { 203 | .x = (int16_t)(sin_lookup(hour_angle) * (int32_t)len_hour / TRIG_MAX_RATIO) + center.x, 204 | .y = (int16_t)(-cos_lookup(hour_angle) * (int32_t)len_hour / TRIG_MAX_RATIO) + center.y, 205 | }; 206 | 207 | // Shorter hour overlay 208 | len_hour -= (MARGIN + 2); 209 | GPoint hour_hand_short = (GPoint) { 210 | .x = (int16_t)(sin_lookup(hour_angle) * (int32_t)len_hour / TRIG_MAX_RATIO) + center.x, 211 | .y = (int16_t)(-cos_lookup(hour_angle) * (int32_t)len_hour / TRIG_MAX_RATIO) + center.y, 212 | }; 213 | 214 | // Draw hands 215 | graphics_context_set_stroke_color(ctx, PBL_IF_COLOR_ELSE(GColorLightGray, GColorWhite)); 216 | for(int y = 0; y < THICKNESS; y++) { 217 | for(int x = 0; x < THICKNESS; x++) { 218 | graphics_draw_line(ctx, GPoint(center.x + x, center.y + y), GPoint(minute_hand_short.x + x, minute_hand_short.y + y)); 219 | graphics_draw_line(ctx, GPoint(center.x + x, center.y + y), GPoint(hour_hand_short.x + x, hour_hand_short.y + y)); 220 | } 221 | } 222 | graphics_context_set_stroke_color(ctx, GColorWhite); 223 | for(int y = 0; y < THICKNESS; y++) { 224 | for(int x = 0; x < THICKNESS; x++) { 225 | graphics_draw_line(ctx, GPoint(minute_hand_short.x + x, minute_hand_short.y + y), GPoint(minute_hand_long.x + x, minute_hand_long.y + y)); 226 | graphics_draw_line(ctx, GPoint(hour_hand_short.x + x, hour_hand_short.y + y), GPoint(hour_hand_long.x + x, hour_hand_long.y + y)); 227 | } 228 | } 229 | 230 | // Draw second hand 231 | if(data_get(DataKeySecondHand)) { 232 | // Use loops 233 | for(int y = 0; y < THICKNESS - 1; y++) { 234 | for(int x = 0; x < THICKNESS - 1; x++) { 235 | graphics_context_set_stroke_color(ctx, PBL_IF_COLOR_ELSE(GColorDarkCandyAppleRed, GColorWhite)); 236 | graphics_draw_line(ctx, GPoint(center.x + x, center.y + y), GPoint(second_hand_short.x + x, second_hand_short.y + y)); 237 | 238 | // Draw second hand tip 239 | graphics_context_set_stroke_color(ctx, PBL_IF_COLOR_ELSE(GColorChromeYellow, GColorWhite)); 240 | graphics_draw_line(ctx, GPoint(second_hand_short.x + x, second_hand_short.y + y), GPoint(second_hand_long.x + x, second_hand_long.y + y)); 241 | } 242 | } 243 | } 244 | 245 | // Center 246 | graphics_context_set_fill_color(ctx, GColorWhite); 247 | graphics_fill_circle(ctx, GPoint(center.x + 1, center.y + 1), 4); 248 | 249 | // Draw black if disconnected 250 | if(data_get(DataKeyBT) && !s_is_connected) { 251 | graphics_context_set_fill_color(ctx, GColorBlack); 252 | graphics_fill_circle(ctx, GPoint(center.x + 1, center.y + 1), 3); 253 | } 254 | } 255 | 256 | /*********************************** Window ***********************************/ 257 | 258 | static void window_load(Window *window) { 259 | Layer *window_layer = window_get_root_layer(window); 260 | GRect bounds = layer_get_bounds(window_layer); 261 | 262 | s_bg_layer = layer_create(bounds); 263 | layer_set_update_proc(s_bg_layer, bg_update_proc); 264 | layer_add_child(window_layer, s_bg_layer); 265 | 266 | int x_offset = (bounds.size.w * 62) / 100; 267 | 268 | s_weekday_layer = text_layer_create(GRect(x_offset, 55, 44, 40)); 269 | text_layer_set_text_alignment(s_weekday_layer, GTextAlignmentCenter); 270 | text_layer_set_font(s_weekday_layer, fonts_get_system_font(FONT_KEY_GOTHIC_14_BOLD)); 271 | text_layer_set_text_color(s_weekday_layer, GColorWhite); 272 | text_layer_set_background_color(s_weekday_layer, GColorClear); 273 | 274 | s_day_in_month_layer = text_layer_create(GRect(x_offset, 68, 44, 40)); 275 | text_layer_set_text_alignment(s_day_in_month_layer, GTextAlignmentCenter); 276 | text_layer_set_font(s_day_in_month_layer, fonts_get_system_font(FONT_KEY_GOTHIC_24_BOLD)); 277 | text_layer_set_text_color(s_day_in_month_layer, PBL_IF_COLOR_ELSE(GColorChromeYellow, GColorWhite)); 278 | text_layer_set_background_color(s_day_in_month_layer, GColorClear); 279 | 280 | s_month_layer = text_layer_create(GRect(x_offset, 95, 44, 40)); 281 | text_layer_set_text_alignment(s_month_layer, GTextAlignmentCenter); 282 | text_layer_set_font(s_month_layer, fonts_get_system_font(FONT_KEY_GOTHIC_14_BOLD)); 283 | text_layer_set_text_color(s_month_layer, GColorWhite); 284 | text_layer_set_background_color(s_month_layer, GColorClear); 285 | 286 | s_step_layer = text_layer_create(GRect(0, 0, 44, 90)); 287 | text_layer_set_text_alignment(s_step_layer, GTextAlignmentCenter); 288 | text_layer_set_font(s_step_layer, fonts_get_system_font(FONT_KEY_GOTHIC_18_BOLD)); 289 | text_layer_set_text_color(s_step_layer, GColorWhite); 290 | text_layer_set_background_color(s_step_layer, GColorClear); 291 | 292 | s_canvas_layer = layer_create(bounds); 293 | layer_set_update_proc(s_canvas_layer, draw_proc); 294 | } 295 | 296 | static void window_unload(Window *window) { 297 | layer_destroy(s_canvas_layer); 298 | layer_destroy(s_bg_layer); 299 | 300 | text_layer_destroy(s_weekday_layer); 301 | text_layer_destroy(s_day_in_month_layer); 302 | text_layer_destroy(s_month_layer); 303 | text_layer_destroy(s_step_layer); 304 | 305 | window_destroy(s_main_window); 306 | } 307 | 308 | static int anim_percentage(AnimationProgress dist_normalized, int max) { 309 | return (max * dist_normalized) / ANIMATION_NORMALIZED_MAX; 310 | } 311 | 312 | static void hands_update(Animation *anim, AnimationProgress dist_normalized) { 313 | s_current_time.hours -= (s_current_time.hours > 12) ? 12 : 0; 314 | 315 | s_anim_time.hours = anim_percentage(dist_normalized, hours_to_minutes(s_current_time.hours)); 316 | s_anim_time.minutes = anim_percentage(dist_normalized, s_current_time.minutes); 317 | s_anim_time.seconds = anim_percentage(dist_normalized, s_current_time.seconds); 318 | 319 | layer_mark_dirty(s_canvas_layer); 320 | } 321 | 322 | static void center_step_layer() { 323 | if (!data_get(DataKeySteps) || !is_health_available()) { 324 | return; 325 | } 326 | 327 | Layer *step_layer_root = text_layer_get_layer(s_step_layer); 328 | Layer *window_layer = window_get_root_layer(layer_get_window(step_layer_root)); 329 | GRect bounds = layer_get_bounds(window_layer); 330 | 331 | int x_offset = (bounds.size.w * 62) / 100; 332 | 333 | int length = 0; 334 | if (step_data_is_available()) { 335 | // Determine the length of steps so we can vertically center the step count 336 | int steps = get_step_count(); 337 | length = 0; 338 | while (steps > 0) { 339 | steps /= 10; 340 | length++; 341 | } 342 | } 343 | 344 | layer_set_frame(step_layer_root, GRect(bounds.size.w - (x_offset+54), 72 - (length-1)*9 , 44, 90)); 345 | } 346 | 347 | /************************************ API *************************************/ 348 | 349 | void main_window_push() { 350 | s_main_window = window_create(); 351 | window_set_background_color(s_main_window, GColorBlack); 352 | window_set_window_handlers(s_main_window, (WindowHandlers) { 353 | .load = window_load, 354 | .unload = window_unload, 355 | }); 356 | window_stack_push(s_main_window, true); 357 | 358 | // Begin smooth animation 359 | static AnimationImplementation hands_impl = { 360 | .update = hands_update 361 | }; 362 | animate(ANIMATION_DURATION, ANIMATION_DELAY, &hands_impl, true); 363 | 364 | main_window_reload_config(); 365 | } 366 | 367 | void main_window_reload_config() { 368 | time_t t = time(NULL); 369 | struct tm *tm_now = localtime(&t); 370 | s_current_time.hours = tm_now->tm_hour; 371 | s_current_time.minutes = tm_now->tm_min; 372 | s_current_time.seconds = tm_now->tm_sec; 373 | 374 | tick_timer_service_unsubscribe(); 375 | if(data_get(DataKeySecondHand)) { 376 | tick_timer_service_subscribe(SECOND_UNIT, tick_handler); 377 | } else { 378 | tick_timer_service_subscribe(MINUTE_UNIT, tick_handler); 379 | } 380 | 381 | connection_service_unsubscribe(); 382 | if(data_get(DataKeyBT)) { 383 | connection_service_subscribe((ConnectionHandlers) { 384 | .pebble_app_connection_handler = bt_handler 385 | }); 386 | bt_handler(connection_service_peek_pebble_app_connection()); 387 | } 388 | 389 | battery_state_service_unsubscribe(); 390 | if(data_get(DataKeyBattery)) { 391 | battery_state_service_subscribe(batt_handler); 392 | } 393 | 394 | Layer *window_layer = window_get_root_layer(s_main_window); 395 | layer_remove_from_parent(text_layer_get_layer(s_day_in_month_layer)); 396 | layer_remove_from_parent(text_layer_get_layer(s_weekday_layer)); 397 | layer_remove_from_parent(text_layer_get_layer(s_month_layer)); 398 | layer_remove_from_parent(text_layer_get_layer(s_step_layer)); 399 | if(data_get(DataKeyDay)) { 400 | layer_add_child(window_layer, text_layer_get_layer(s_day_in_month_layer)); 401 | } 402 | if(data_get(DataKeyDate)) { 403 | layer_add_child(window_layer, text_layer_get_layer(s_weekday_layer)); 404 | layer_add_child(window_layer, text_layer_get_layer(s_month_layer)); 405 | } 406 | if(data_get(DataKeySteps) && is_health_available()) { 407 | layer_add_child(window_layer, text_layer_get_layer(s_step_layer)); 408 | } 409 | 410 | layer_add_child(window_layer, s_canvas_layer); 411 | } 412 | 413 | -------------------------------------------------------------------------------- /src/windows/main_window.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include "modules/data.h" 6 | #include "modules/health.h" 7 | 8 | void main_window_push(); 9 | 10 | void main_window_reload_config(); 11 | 12 | static void center_step_layer(); 13 | -------------------------------------------------------------------------------- /wscript: -------------------------------------------------------------------------------- 1 | 2 | # 3 | # This file is the default set of rules to compile a Pebble project. 4 | # 5 | # Feel free to customize this to your needs. 6 | # 7 | 8 | import os.path 9 | top = '.' 10 | out = 'build' 11 | 12 | def options(ctx): 13 | ctx.load('pebble_sdk') 14 | 15 | def configure(ctx): 16 | ctx.load('pebble_sdk') 17 | 18 | def build(ctx): 19 | ctx.load('pebble_sdk') 20 | 21 | build_worker = os.path.exists('worker_src') 22 | binaries = [] 23 | 24 | for p in ctx.env.TARGET_PLATFORMS: 25 | ctx.set_env(ctx.all_envs[p]) 26 | ctx.set_group(ctx.env.PLATFORM_NAME) 27 | app_elf='{}/pebble-app.elf'.format(p) 28 | ctx.pbl_program(source=ctx.path.ant_glob('src/**/*.c'), 29 | target=app_elf) 30 | 31 | if build_worker: 32 | worker_elf='{}/pebble-worker.elf'.format(p) 33 | binaries.append({'platform': p, 'app_elf': app_elf, 'worker_elf': worker_elf}) 34 | ctx.pbl_worker(source=ctx.path.ant_glob('worker_src/**/*.c'), 35 | target=worker_elf) 36 | else: 37 | binaries.append({'platform': p, 'app_elf': app_elf}) 38 | 39 | ctx.set_group('bundle') 40 | ctx.pbl_bundle(binaries=binaries, 41 | js=ctx.path.ant_glob(['src/js/**/*.js', 42 | 'src/js/**/*.json', 43 | 'src/common/**/*.js']), 44 | js_entry_file='src/js/index.js') 45 | --------------------------------------------------------------------------------