main.c

  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}