1#include <pebble.h>
2
3#define MAX_ROWS 4
4#define ANIMATION_INTERVAL_MS 33
5#define ANIMATION_FRAMES 12
6
7typedef struct {
8 char label[24];
9 char detail[28];
10 int percent;
11 int status_rank;
12 bool has_bar;
13} QuotaRow;
14
15static Window *s_main_window;
16static Layer *s_card_layer;
17static AppTimer *s_animation_timer;
18
19static int s_page_index = 0;
20static int s_page_count = 0;
21static int s_row_count = 0;
22static int s_animation_frame = ANIMATION_FRAMES;
23static int s_refresh_frame = 0;
24static int s_animation_direction = 1;
25static bool s_is_refreshing = false;
26
27static char s_title_text[40] = "Pebblexus";
28static char s_group_text[40] = "";
29static char s_reset_text[40] = "Set URL and admin key";
30static char s_status_text[16] = "Waiting";
31static char s_updated_text[16] = "";
32static QuotaRow s_rows[MAX_ROWS];
33
34static uint32_t row_label_key(int row_index) {
35 switch (row_index) {
36 case 0:
37 return MESSAGE_KEY_ROW_0_LABEL;
38 case 1:
39 return MESSAGE_KEY_ROW_1_LABEL;
40 case 2:
41 return MESSAGE_KEY_ROW_2_LABEL;
42 default:
43 return MESSAGE_KEY_ROW_3_LABEL;
44 }
45}
46
47static uint32_t row_detail_key(int row_index) {
48 switch (row_index) {
49 case 0:
50 return MESSAGE_KEY_ROW_0_DETAIL;
51 case 1:
52 return MESSAGE_KEY_ROW_1_DETAIL;
53 case 2:
54 return MESSAGE_KEY_ROW_2_DETAIL;
55 default:
56 return MESSAGE_KEY_ROW_3_DETAIL;
57 }
58}
59
60static uint32_t row_percent_key(int row_index) {
61 switch (row_index) {
62 case 0:
63 return MESSAGE_KEY_ROW_0_PERCENT;
64 case 1:
65 return MESSAGE_KEY_ROW_1_PERCENT;
66 case 2:
67 return MESSAGE_KEY_ROW_2_PERCENT;
68 default:
69 return MESSAGE_KEY_ROW_3_PERCENT;
70 }
71}
72
73static uint32_t row_status_key(int row_index) {
74 switch (row_index) {
75 case 0:
76 return MESSAGE_KEY_ROW_0_STATUS;
77 case 1:
78 return MESSAGE_KEY_ROW_1_STATUS;
79 case 2:
80 return MESSAGE_KEY_ROW_2_STATUS;
81 default:
82 return MESSAGE_KEY_ROW_3_STATUS;
83 }
84}
85
86static uint32_t row_has_bar_key(int row_index) {
87 switch (row_index) {
88 case 0:
89 return MESSAGE_KEY_ROW_0_HAS_BAR;
90 case 1:
91 return MESSAGE_KEY_ROW_1_HAS_BAR;
92 case 2:
93 return MESSAGE_KEY_ROW_2_HAS_BAR;
94 default:
95 return MESSAGE_KEY_ROW_3_HAS_BAR;
96 }
97}
98
99static int clamp_percent(int percent) {
100 if (percent < 0)
101 return 0;
102 if (percent > 100)
103 return 100;
104 return percent;
105}
106
107static GColor color_background(void) { return PBL_IF_COLOR_ELSE(GColorWhite, GColorWhite); }
108
109static GColor color_card_outline(void) { return PBL_IF_COLOR_ELSE(GColorFromHEX(0xAAFFFF), GColorBlack); }
110
111static GColor color_text_primary(void) { return GColorBlack; }
112
113static GColor color_text_secondary(void) { return PBL_IF_COLOR_ELSE(GColorCobaltBlue, GColorBlack); }
114
115static GColor color_text_muted(void) { return PBL_IF_COLOR_ELSE(GColorDarkGray, GColorBlack); }
116
117static GColor color_text_soft(void) { return PBL_IF_COLOR_ELSE(GColorDukeBlue, GColorBlack); }
118
119static GColor color_track(void) { return PBL_IF_COLOR_ELSE(GColorLightGray, GColorBlack); }
120
121static GColor color_card_highlight(void) { return PBL_IF_COLOR_ELSE(GColorVividCerulean, GColorBlack); }
122
123static GColor color_warning_soft(void) { return PBL_IF_COLOR_ELSE(GColorIcterine, GColorWhite); }
124
125static GColor color_ok_soft(void) { return PBL_IF_COLOR_ELSE(GColorMintGreen, GColorWhite); }
126
127static GColor color_for_status(int status_rank) {
128#ifdef PBL_COLOR
129 if (status_rank >= 4)
130 return GColorRed;
131 if (status_rank >= 3)
132 return GColorOrange;
133 return GColorJaegerGreen;
134#else
135 return GColorBlack;
136#endif
137}
138
139static GColor color_status_fill(void) {
140 if (strcmp(s_status_text, "CRITICAL") == 0)
141 return PBL_IF_COLOR_ELSE(GColorMelon, GColorWhite);
142 if (strcmp(s_status_text, "WARNING") == 0)
143 return color_warning_soft();
144 return color_ok_soft();
145}
146
147static GColor color_for_status_text(void) {
148 if (strcmp(s_status_text, "CRITICAL") == 0)
149 return color_for_status(4);
150 if (strcmp(s_status_text, "WARNING") == 0)
151 return color_for_status(3);
152 return color_for_status(1);
153}
154
155static int current_slide_offset(void) { return 0; }
156
157static int current_bar_percent(int percent) {
158 int clamped = clamp_percent(percent);
159 if (s_animation_frame >= ANIMATION_FRAMES)
160 return clamped;
161 int frame = s_animation_frame;
162 int eased = ANIMATION_FRAMES - (((ANIMATION_FRAMES - frame) * (ANIMATION_FRAMES - frame)) / ANIMATION_FRAMES);
163 return (clamped * eased) / ANIMATION_FRAMES;
164}
165
166static void mark_card_dirty(void) {
167 if (s_card_layer != NULL)
168 layer_mark_dirty(s_card_layer);
169}
170
171static void animation_timer_callback(void *data) {
172 s_animation_timer = NULL;
173 if (s_animation_frame < ANIMATION_FRAMES || s_is_refreshing) {
174 if (s_animation_frame < ANIMATION_FRAMES) {
175 s_animation_frame += 1;
176 }
177 s_refresh_frame = (s_refresh_frame + 1) % ANIMATION_FRAMES;
178 mark_card_dirty();
179 s_animation_timer = app_timer_register(ANIMATION_INTERVAL_MS, animation_timer_callback, NULL);
180 }
181}
182
183static void start_card_animation(int direction) {
184 if (s_animation_timer != NULL) {
185 app_timer_cancel(s_animation_timer);
186 s_animation_timer = NULL;
187 }
188
189 s_animation_direction = direction < 0 ? -1 : 1;
190 s_animation_frame = 0;
191 s_refresh_frame = 0;
192 mark_card_dirty();
193 s_animation_timer = app_timer_register(ANIMATION_INTERVAL_MS, animation_timer_callback, NULL);
194}
195
196static void request_quota(void) {
197 DictionaryIterator *iter;
198 AppMessageResult result = app_message_outbox_begin(&iter);
199 if (result != APP_MSG_OK || iter == NULL) {
200 snprintf(s_status_text, sizeof(s_status_text), "Send fail");
201 s_is_refreshing = false;
202 mark_card_dirty();
203 return;
204 }
205
206 dict_write_uint8(iter, MESSAGE_KEY_REQUEST_QUOTA, 1);
207 dict_write_int32(iter, MESSAGE_KEY_PAGE_INDEX, s_page_index);
208 result = app_message_outbox_send();
209 if (result == APP_MSG_OK) {
210 snprintf(s_status_text, sizeof(s_status_text), "Refreshing");
211 s_updated_text[0] = '\0';
212 s_is_refreshing = true;
213 start_card_animation(1);
214 } else {
215 snprintf(s_status_text, sizeof(s_status_text), "Send fail");
216 s_is_refreshing = false;
217 mark_card_dirty();
218 }
219}
220
221static void request_page(int page_index) {
222 if (s_page_count <= 0)
223 return;
224 int direction = page_index >= s_page_index ? 1 : -1;
225 if (page_index < 0) {
226 page_index = s_page_count - 1;
227 direction = -1;
228 }
229 if (page_index >= s_page_count) {
230 page_index = 0;
231 direction = 1;
232 }
233
234 DictionaryIterator *iter;
235 AppMessageResult result = app_message_outbox_begin(&iter);
236 if (result != APP_MSG_OK || iter == NULL) {
237 snprintf(s_status_text, sizeof(s_status_text), "Page fail");
238 mark_card_dirty();
239 return;
240 }
241
242 s_page_index = page_index;
243 dict_write_int32(iter, MESSAGE_KEY_REQUEST_PAGE, page_index);
244 result = app_message_outbox_send();
245 if (result == APP_MSG_OK) {
246 start_card_animation(direction);
247 } else {
248 snprintf(s_status_text, sizeof(s_status_text), "Send fail");
249 mark_card_dirty();
250 }
251}
252
253static void up_click_handler(ClickRecognizerRef recognizer, void *context) { request_page(s_page_index - 1); }
254
255static void down_click_handler(ClickRecognizerRef recognizer, void *context) { request_page(s_page_index + 1); }
256
257static void select_click_handler(ClickRecognizerRef recognizer, void *context) { request_quota(); }
258
259static void click_config_provider(void *context) {
260 window_single_repeating_click_subscribe(BUTTON_ID_UP, 180, up_click_handler);
261 window_single_click_subscribe(BUTTON_ID_SELECT, select_click_handler);
262 window_single_repeating_click_subscribe(BUTTON_ID_DOWN, 180, down_click_handler);
263}
264
265static void draw_text(GContext *ctx, const char *text, GRect box, GFont font, GColor color, GTextAlignment alignment) {
266 graphics_context_set_text_color(ctx, color);
267 graphics_draw_text(ctx, text, font, box, GTextOverflowModeTrailingEllipsis, alignment, NULL);
268}
269
270static void draw_progress_bar(GContext *ctx, GRect frame, int percent, GColor color) {
271 graphics_context_set_fill_color(ctx, color_track());
272 graphics_fill_rect(ctx, frame, 3, GCornersAll);
273
274 int fill_width = (frame.size.w * current_bar_percent(percent)) / 100;
275 if (fill_width < 2 && percent > 0)
276 fill_width = 2;
277 graphics_context_set_fill_color(ctx, color);
278 graphics_fill_rect(ctx, GRect(frame.origin.x, frame.origin.y, fill_width, frame.size.h), 3, GCornersAll);
279}
280
281static void draw_status_pill(GContext *ctx, GRect frame) {
282 graphics_context_set_fill_color(ctx, color_status_fill());
283 graphics_fill_rect(ctx, frame, 6, GCornersAll);
284 graphics_context_set_stroke_color(ctx, color_for_status_text());
285 graphics_draw_rect(ctx, frame);
286 draw_text(ctx, s_status_text, GRect(frame.origin.x, frame.origin.y - 4, frame.size.w, frame.size.h + 4),
287 fonts_get_system_font(FONT_KEY_GOTHIC_18_BOLD), color_text_primary(), GTextAlignmentCenter);
288}
289
290static void draw_refresh_dots(GContext *ctx, GRect bounds) {
291 if (!s_is_refreshing)
292 return;
293 int phase = s_refresh_frame;
294 int center_x = bounds.size.w - 23;
295 int center_y = 18;
296
297 graphics_context_set_fill_color(ctx, color_text_muted());
298 graphics_fill_circle(ctx, GPoint(center_x - 7, center_y), phase < 4 ? 3 : 2);
299 graphics_fill_circle(ctx, GPoint(center_x, center_y), phase >= 3 && phase < 7 ? 3 : 2);
300 graphics_fill_circle(ctx, GPoint(center_x + 7, center_y), phase >= 6 ? 3 : 2);
301}
302
303static void draw_page_dots(GContext *ctx, GRect frame) {
304 if (s_page_count <= 1)
305 return;
306
307 int dots = s_page_count > 7 ? 7 : s_page_count;
308 int total_width = dots * 7 - 3;
309 int x = frame.origin.x + (frame.size.w - total_width) / 2;
310 int y = frame.origin.y + frame.size.h - 10;
311
312 for (int i = 0; i < dots; i += 1) {
313 graphics_context_set_fill_color(ctx, i == s_page_index || (s_page_count > 7 && i == 6 && s_page_index >= 6)
314 ? color_card_highlight()
315 : color_text_muted());
316 int active = i == s_page_index || (s_page_count > 7 && i == 6 && s_page_index >= 6);
317 int radius = active ? 2 : 1;
318 if (active && s_animation_frame < ANIMATION_FRAMES / 2)
319 radius = 3;
320 graphics_fill_circle(ctx, GPoint(x + (i * 7), y), radius);
321 }
322}
323
324static void card_update_proc(Layer *layer, GContext *ctx) {
325 GRect bounds = layer_get_bounds(layer);
326 graphics_context_set_fill_color(ctx, color_background());
327 graphics_fill_rect(ctx, bounds, 0, GCornerNone);
328
329 const int margin = 10;
330 int offset = current_slide_offset();
331 int content_x = margin;
332 int content_w = bounds.size.w - (margin * 2);
333 int y = 8 + offset;
334
335 draw_text(ctx, s_title_text, GRect(content_x, y, content_w - 50, 29), fonts_get_system_font(FONT_KEY_GOTHIC_24_BOLD),
336 color_text_primary(), GTextAlignmentLeft);
337
338 char page_text[16];
339 if (s_page_count > 0) {
340 int display_page_index = s_page_index + 1;
341 if (display_page_index < 1)
342 display_page_index = 1;
343 if (display_page_index > 99)
344 display_page_index = 99;
345 int display_page_count = s_page_count;
346 if (display_page_count < 1)
347 display_page_count = 1;
348 if (display_page_count > 99)
349 display_page_count = 99;
350 snprintf(page_text, sizeof(page_text), "%d/%d", display_page_index, display_page_count);
351 } else {
352 snprintf(page_text, sizeof(page_text), "--");
353 }
354 draw_text(ctx, page_text, GRect(content_x + content_w - 46, y + 2, 46, 24),
355 fonts_get_system_font(FONT_KEY_GOTHIC_18_BOLD), color_text_secondary(), GTextAlignmentRight);
356 draw_refresh_dots(ctx, bounds);
357
358 y += 26;
359 if (s_status_text[0] != '\0') {
360 draw_status_pill(ctx, GRect(content_x + content_w - 60, y + 1, 60, 18));
361 }
362
363 y += s_status_text[0] == '\0' ? 8 : 26;
364 if (s_row_count <= 0) {
365 graphics_context_set_fill_color(ctx, color_card_highlight());
366 graphics_fill_circle(ctx, GPoint(content_x + (content_w / 2), y + 26), 15);
367 graphics_context_set_fill_color(ctx, color_background());
368 graphics_fill_circle(ctx, GPoint(content_x + (content_w / 2), y + 26), 9);
369 draw_text(ctx, "Quota unavailable", GRect(content_x, y + 48, content_w, 34),
370 fonts_get_system_font(FONT_KEY_GOTHIC_28_BOLD), color_text_primary(), GTextAlignmentCenter);
371 draw_text(ctx, s_reset_text, GRect(content_x, y + 86, content_w, 46), fonts_get_system_font(FONT_KEY_GOTHIC_24),
372 color_text_secondary(), GTextAlignmentCenter);
373 } else {
374 for (int i = 0; i < s_row_count && i < MAX_ROWS; i += 1) {
375 QuotaRow *row = &s_rows[i];
376 GColor accent = color_for_status(row->status_rank);
377 int detail_width = 62;
378 draw_text(ctx, row->label, GRect(content_x, y, content_w - detail_width - 8, 22),
379 fonts_get_system_font(FONT_KEY_GOTHIC_18_BOLD), color_text_primary(), GTextAlignmentLeft);
380 draw_text(ctx, row->detail, GRect(content_x + content_w - detail_width, y, detail_width, 22),
381 fonts_get_system_font(FONT_KEY_GOTHIC_18_BOLD), row->has_bar ? color_text_soft() : color_text_primary(),
382 GTextAlignmentRight);
383 y += 22;
384 if (row->has_bar) {
385 draw_progress_bar(ctx, GRect(content_x, y, content_w, 6), row->percent, accent);
386 y += 16;
387 } else {
388 y += 6;
389 }
390 if (i + 1 < s_row_count && i + 1 < MAX_ROWS) {
391 graphics_context_set_stroke_color(ctx, color_card_outline());
392 graphics_draw_line(ctx, GPoint(content_x, y), GPoint(content_x + content_w, y));
393 y += 3;
394 }
395 }
396 if (s_row_count > MAX_ROWS) {
397 draw_text(ctx, "+ more", GRect(content_x, y, content_w, 22), fonts_get_system_font(FONT_KEY_GOTHIC_18),
398 color_text_muted(), GTextAlignmentCenter);
399 }
400 }
401
402 draw_page_dots(ctx, bounds);
403
404 draw_text(ctx, s_reset_text, GRect(content_x, bounds.size.h - 55, content_w, 24),
405 fonts_get_system_font(FONT_KEY_GOTHIC_18), color_text_muted(), GTextAlignmentCenter);
406
407 char footer[32];
408 if (s_page_count > 0 && s_status_text[0] != '\0') {
409 snprintf(footer, sizeof(footer), "%.10s %.12s", s_status_text, s_updated_text);
410 } else if (s_page_count > 0) {
411 snprintf(footer, sizeof(footer), "%.31s", s_updated_text);
412 } else {
413 snprintf(footer, sizeof(footer), "%.31s", s_status_text);
414 }
415 draw_text(ctx, footer, GRect(content_x, bounds.size.h - 33, content_w, 24), fonts_get_system_font(FONT_KEY_GOTHIC_18),
416 color_text_secondary(), GTextAlignmentCenter);
417}
418
419static void copy_string_tuple(DictionaryIterator *iterator, uint32_t key, char *buffer, size_t buffer_size) {
420 Tuple *tuple = dict_find(iterator, key);
421 if (tuple == NULL)
422 return;
423 snprintf(buffer, buffer_size, "%s", tuple->value->cstring);
424}
425
426static void read_row(DictionaryIterator *iterator, int row_index) {
427 QuotaRow *row = &s_rows[row_index];
428 copy_string_tuple(iterator, row_label_key(row_index), row->label, sizeof(row->label));
429 copy_string_tuple(iterator, row_detail_key(row_index), row->detail, sizeof(row->detail));
430
431 Tuple *percent_tuple = dict_find(iterator, row_percent_key(row_index));
432 row->percent = percent_tuple != NULL ? clamp_percent((int)percent_tuple->value->int32) : 0;
433
434 Tuple *status_tuple = dict_find(iterator, row_status_key(row_index));
435 row->status_rank = status_tuple != NULL ? (int)status_tuple->value->int32 : 1;
436
437 Tuple *has_bar_tuple = dict_find(iterator, row_has_bar_key(row_index));
438 row->has_bar = has_bar_tuple != NULL && has_bar_tuple->value->int32 != 0;
439}
440
441static void inbox_received_callback(DictionaryIterator *iterator, void *context) {
442 Tuple *error_tuple = dict_find(iterator, MESSAGE_KEY_ERROR);
443 if (error_tuple != NULL) {
444 s_is_refreshing = false;
445 s_row_count = 0;
446 s_page_index = 0;
447 s_page_count = 0;
448 snprintf(s_title_text, sizeof(s_title_text), "Pebblexus");
449 snprintf(s_reset_text, sizeof(s_reset_text), "%s", error_tuple->value->cstring);
450 snprintf(s_status_text, sizeof(s_status_text), "Select retries");
451 s_updated_text[0] = '\0';
452 s_animation_frame = ANIMATION_FRAMES;
453 mark_card_dirty();
454 return;
455 }
456
457 s_is_refreshing = false;
458 copy_string_tuple(iterator, MESSAGE_KEY_QUOTA_TITLE, s_title_text, sizeof(s_title_text));
459 copy_string_tuple(iterator, MESSAGE_KEY_QUOTA_LABEL, s_group_text, sizeof(s_group_text));
460 copy_string_tuple(iterator, MESSAGE_KEY_QUOTA_RESET, s_reset_text, sizeof(s_reset_text));
461 copy_string_tuple(iterator, MESSAGE_KEY_QUOTA_STATUS, s_status_text, sizeof(s_status_text));
462 copy_string_tuple(iterator, MESSAGE_KEY_UPDATED_AT, s_updated_text, sizeof(s_updated_text));
463
464 Tuple *page_index_tuple = dict_find(iterator, MESSAGE_KEY_PAGE_INDEX);
465 if (page_index_tuple != NULL)
466 s_page_index = (int)page_index_tuple->value->int32;
467
468 Tuple *page_count_tuple = dict_find(iterator, MESSAGE_KEY_PAGE_COUNT);
469 if (page_count_tuple != NULL)
470 s_page_count = (int)page_count_tuple->value->int32;
471 if (s_page_count < 0)
472 s_page_count = 0;
473 if (s_page_index >= s_page_count)
474 s_page_index = s_page_count - 1;
475 if (s_page_index < 0)
476 s_page_index = 0;
477
478 Tuple *row_count_tuple = dict_find(iterator, MESSAGE_KEY_ROW_COUNT);
479 int row_count = row_count_tuple != NULL ? (int)row_count_tuple->value->int32 : 0;
480 if (row_count < 0)
481 row_count = 0;
482 s_row_count = row_count;
483
484 int rows_to_read = row_count > MAX_ROWS ? MAX_ROWS : row_count;
485 for (int i = 0; i < rows_to_read; i += 1) {
486 s_rows[i].label[0] = '\0';
487 s_rows[i].detail[0] = '\0';
488 s_rows[i].percent = 0;
489 s_rows[i].status_rank = 1;
490 s_rows[i].has_bar = false;
491 read_row(iterator, i);
492 }
493
494 start_card_animation(s_animation_direction);
495}
496
497static void inbox_dropped_callback(AppMessageResult reason, void *context) {
498 s_is_refreshing = false;
499 snprintf(s_status_text, sizeof(s_status_text), "Dropped");
500 mark_card_dirty();
501}
502
503static void outbox_failed_callback(DictionaryIterator *iterator, AppMessageResult reason, void *context) {
504 s_is_refreshing = false;
505 snprintf(s_status_text, sizeof(s_status_text), "No phone");
506 mark_card_dirty();
507}
508
509static void main_window_load(Window *window) {
510 Layer *window_layer = window_get_root_layer(window);
511 GRect bounds = layer_get_bounds(window_layer);
512
513 s_card_layer = layer_create(bounds);
514 layer_set_update_proc(s_card_layer, card_update_proc);
515 layer_add_child(window_layer, s_card_layer);
516}
517
518static void main_window_unload(Window *window) {
519 if (s_animation_timer != NULL) {
520 app_timer_cancel(s_animation_timer);
521 s_animation_timer = NULL;
522 }
523 layer_destroy(s_card_layer);
524}
525
526static void init(void) {
527 s_main_window = window_create();
528 window_set_background_color(s_main_window, color_background());
529 window_set_click_config_provider(s_main_window, click_config_provider);
530 window_set_window_handlers(s_main_window, (WindowHandlers){.load = main_window_load, .unload = main_window_unload});
531
532 app_message_register_inbox_received(inbox_received_callback);
533 app_message_register_inbox_dropped(inbox_dropped_callback);
534 app_message_register_outbox_failed(outbox_failed_callback);
535 app_message_open(2048, 256);
536
537 window_stack_push(s_main_window, true);
538 request_quota();
539}
540
541static void deinit(void) { window_destroy(s_main_window); }
542
543int main(void) {
544 init();
545 app_event_loop();
546 deinit();
547}