1#include <pebble.h>
2
3#define MAX_ROWS 4
4#define ANIMATION_INTERVAL_MS 33
5#define REFRESH_DOT_FRAMES 12
6#define SLIDE_OUT_FRAMES 4
7#define SLIDE_IN_FRAMES 5
8#define SLIDE_OUT_DISTANCE_PX 18
9#define SLIDE_IN_DISTANCE_PX 14
10#define BAR_FILL_FRAMES 10
11#define BAR_STAGGER_FRAMES 3
12#define BOUNCE_FRAMES 3
13#define BOUNCE_DISTANCE_PX 5
14#define STAMP_FRAMES 4
15
16// Page navigation choreography: the card content slides out in the travel
17// direction while we wait for the phone, the new card slides in from the
18// opposite side, then the quota bars pour in top-down. On failure the card
19// slides back into place. When the page order wraps around, the incoming
20// card overshoots and rubber-bands back to signal the seam in the loop.
21typedef enum {
22 MOTION_IDLE,
23 MOTION_SLIDE_OUT,
24 MOTION_WAITING,
25 MOTION_SLIDE_IN,
26 MOTION_BOUNCE,
27 MOTION_BARS,
28} MotionPhase;
29
30typedef struct {
31 char label[24];
32 char detail[28];
33 int percent;
34 int status_rank;
35 bool has_bar;
36} QuotaRow;
37
38static Window *s_main_window;
39static Layer *s_card_layer;
40static AppTimer *s_animation_timer;
41
42static int s_page_index = 0;
43static int s_page_count = 0;
44static int s_row_count = 0;
45static MotionPhase s_motion_phase = MOTION_IDLE;
46static int s_motion_frame = 0;
47static int s_slide_offset = 0;
48static int s_slide_from = 0;
49static int s_slide_to = 0;
50static int s_slide_direction = 1;
51static int s_bar_frame = BAR_FILL_FRAMES + BAR_STAGGER_FRAMES * (MAX_ROWS - 1);
52static int s_refresh_frame = 0;
53static bool s_is_refreshing = false;
54static bool s_wrap_pending = false;
55
56static char s_title_text[40] = "Pebblexus";
57static char s_group_text[40] = "";
58static char s_reset_text[40] = "Set URL and admin key";
59static char s_status_text[16] = "Waiting";
60static char s_updated_text[16] = "";
61static QuotaRow s_rows[MAX_ROWS];
62
63static uint32_t row_label_key(int row_index) {
64 switch (row_index) {
65 case 0:
66 return MESSAGE_KEY_ROW_0_LABEL;
67 case 1:
68 return MESSAGE_KEY_ROW_1_LABEL;
69 case 2:
70 return MESSAGE_KEY_ROW_2_LABEL;
71 default:
72 return MESSAGE_KEY_ROW_3_LABEL;
73 }
74}
75
76static uint32_t row_detail_key(int row_index) {
77 switch (row_index) {
78 case 0:
79 return MESSAGE_KEY_ROW_0_DETAIL;
80 case 1:
81 return MESSAGE_KEY_ROW_1_DETAIL;
82 case 2:
83 return MESSAGE_KEY_ROW_2_DETAIL;
84 default:
85 return MESSAGE_KEY_ROW_3_DETAIL;
86 }
87}
88
89static uint32_t row_percent_key(int row_index) {
90 switch (row_index) {
91 case 0:
92 return MESSAGE_KEY_ROW_0_PERCENT;
93 case 1:
94 return MESSAGE_KEY_ROW_1_PERCENT;
95 case 2:
96 return MESSAGE_KEY_ROW_2_PERCENT;
97 default:
98 return MESSAGE_KEY_ROW_3_PERCENT;
99 }
100}
101
102static uint32_t row_status_key(int row_index) {
103 switch (row_index) {
104 case 0:
105 return MESSAGE_KEY_ROW_0_STATUS;
106 case 1:
107 return MESSAGE_KEY_ROW_1_STATUS;
108 case 2:
109 return MESSAGE_KEY_ROW_2_STATUS;
110 default:
111 return MESSAGE_KEY_ROW_3_STATUS;
112 }
113}
114
115static uint32_t row_has_bar_key(int row_index) {
116 switch (row_index) {
117 case 0:
118 return MESSAGE_KEY_ROW_0_HAS_BAR;
119 case 1:
120 return MESSAGE_KEY_ROW_1_HAS_BAR;
121 case 2:
122 return MESSAGE_KEY_ROW_2_HAS_BAR;
123 default:
124 return MESSAGE_KEY_ROW_3_HAS_BAR;
125 }
126}
127
128static int clamp_percent(int percent) {
129 if (percent < 0)
130 return 0;
131 if (percent > 100)
132 return 100;
133 return percent;
134}
135
136static GColor color_background(void) { return PBL_IF_COLOR_ELSE(GColorWhite, GColorWhite); }
137
138static GColor color_card_outline(void) { return PBL_IF_COLOR_ELSE(GColorFromHEX(0xAAFFFF), GColorBlack); }
139
140static GColor color_text_primary(void) { return GColorBlack; }
141
142static GColor color_text_secondary(void) { return PBL_IF_COLOR_ELSE(GColorCobaltBlue, GColorBlack); }
143
144static GColor color_text_muted(void) { return PBL_IF_COLOR_ELSE(GColorDarkGray, GColorBlack); }
145
146static GColor color_text_soft(void) { return PBL_IF_COLOR_ELSE(GColorDukeBlue, GColorBlack); }
147
148static GColor color_track(void) { return PBL_IF_COLOR_ELSE(GColorLightGray, GColorBlack); }
149
150static GColor color_card_highlight(void) { return PBL_IF_COLOR_ELSE(GColorVividCerulean, GColorBlack); }
151
152static GColor color_warning_soft(void) { return PBL_IF_COLOR_ELSE(GColorIcterine, GColorWhite); }
153
154static GColor color_ok_soft(void) { return PBL_IF_COLOR_ELSE(GColorMintGreen, GColorWhite); }
155
156static GColor color_for_status(int status_rank) {
157#ifdef PBL_COLOR
158 if (status_rank >= 4)
159 return GColorRed;
160 if (status_rank >= 3)
161 return GColorOrange;
162 return GColorJaegerGreen;
163#else
164 return GColorBlack;
165#endif
166}
167
168static GColor color_status_fill(void) {
169 if (strcmp(s_status_text, "CRITICAL") == 0)
170 return PBL_IF_COLOR_ELSE(GColorMelon, GColorWhite);
171 if (strcmp(s_status_text, "WARNING") == 0)
172 return color_warning_soft();
173 return color_ok_soft();
174}
175
176static GColor color_for_status_text(void) {
177 if (strcmp(s_status_text, "CRITICAL") == 0)
178 return color_for_status(4);
179 if (strcmp(s_status_text, "WARNING") == 0)
180 return color_for_status(3);
181 return color_for_status(1);
182}
183
184static int ease_quad_in(int from, int to, int frame, int frame_count) {
185 if (frame <= 0)
186 return from;
187 if (frame >= frame_count)
188 return to;
189 int t = (frame * 100) / frame_count;
190 return from + ((to - from) * t * t) / 10000;
191}
192
193static int ease_quad_out(int from, int to, int frame, int frame_count) {
194 if (frame <= 0)
195 return from;
196 if (frame >= frame_count)
197 return to;
198 int t = (frame * 100) / frame_count;
199 return from - ((to - from) * t * (t - 200)) / 10000;
200}
201
202static int current_slide_offset(void) { return s_slide_offset; }
203
204static int bar_cascade_total_frames(void) {
205 int rows = s_row_count > MAX_ROWS ? MAX_ROWS : s_row_count;
206 if (rows < 1)
207 rows = 1;
208 int cascade_frames = BAR_FILL_FRAMES + BAR_STAGGER_FRAMES * (rows - 1);
209 int stamp_end_frame = BAR_FILL_FRAMES + STAMP_FRAMES;
210 return cascade_frames > stamp_end_frame ? cascade_frames : stamp_end_frame;
211}
212
213static int current_bar_percent(int row_index, int percent) {
214 int clamped = clamp_percent(percent);
215 if (s_motion_phase == MOTION_SLIDE_IN || s_motion_phase == MOTION_BOUNCE)
216 return 0;
217 if (s_motion_phase != MOTION_BARS)
218 return clamped;
219 int frame = s_bar_frame - (row_index * BAR_STAGGER_FRAMES);
220 return ease_quad_out(0, clamped, frame, BAR_FILL_FRAMES);
221}
222
223static bool status_is_alarming(void) {
224 return strcmp(s_status_text, "CRITICAL") == 0 || strcmp(s_status_text, "WARNING") == 0;
225}
226
227// The status pill stamps in as the worst quota bar lands: briefly oversized,
228// then settled. Only when the news warrants it.
229static int status_pill_inflation(void) {
230 if (s_motion_phase != MOTION_BARS || !status_is_alarming())
231 return 0;
232 int stamp_frame = s_bar_frame - BAR_FILL_FRAMES;
233 if (stamp_frame < 0 || stamp_frame >= STAMP_FRAMES)
234 return 0;
235 return stamp_frame < STAMP_FRAMES / 2 ? 2 : 1;
236}
237
238static void mark_card_dirty(void) {
239 if (s_card_layer != NULL)
240 layer_mark_dirty(s_card_layer);
241}
242
243static void motion_timer_callback(void *data) {
244 s_animation_timer = NULL;
245 bool needs_tick = false;
246
247 switch (s_motion_phase) {
248 case MOTION_SLIDE_OUT:
249 s_motion_frame += 1;
250 s_slide_offset = ease_quad_in(s_slide_from, s_slide_to, s_motion_frame, SLIDE_OUT_FRAMES);
251 if (s_motion_frame >= SLIDE_OUT_FRAMES) {
252 s_motion_phase = MOTION_WAITING;
253 } else {
254 needs_tick = true;
255 }
256 break;
257 case MOTION_SLIDE_IN:
258 s_motion_frame += 1;
259 s_slide_offset = ease_quad_out(s_slide_from, s_slide_to, s_motion_frame, SLIDE_IN_FRAMES);
260 if (s_motion_frame >= SLIDE_IN_FRAMES) {
261 if (s_slide_to != 0) {
262 s_motion_phase = MOTION_BOUNCE;
263 s_motion_frame = 0;
264 s_slide_from = s_slide_to;
265 s_slide_to = 0;
266 } else {
267 s_motion_phase = MOTION_BARS;
268 s_bar_frame = 0;
269 }
270 }
271 needs_tick = true;
272 break;
273 case MOTION_BOUNCE:
274 s_motion_frame += 1;
275 s_slide_offset = ease_quad_out(s_slide_from, 0, s_motion_frame, BOUNCE_FRAMES);
276 if (s_motion_frame >= BOUNCE_FRAMES) {
277 s_motion_phase = MOTION_BARS;
278 s_bar_frame = 0;
279 }
280 needs_tick = true;
281 break;
282 case MOTION_BARS:
283 s_bar_frame += 1;
284 if (s_bar_frame >= bar_cascade_total_frames()) {
285 s_motion_phase = MOTION_IDLE;
286 } else {
287 needs_tick = true;
288 }
289 break;
290 default:
291 break;
292 }
293
294 if (s_is_refreshing) {
295 s_refresh_frame = (s_refresh_frame + 1) % REFRESH_DOT_FRAMES;
296 needs_tick = true;
297 }
298
299 mark_card_dirty();
300 if (needs_tick)
301 s_animation_timer = app_timer_register(ANIMATION_INTERVAL_MS, motion_timer_callback, NULL);
302}
303
304static void ensure_motion_timer(void) {
305 if (s_animation_timer == NULL)
306 s_animation_timer = app_timer_register(ANIMATION_INTERVAL_MS, motion_timer_callback, NULL);
307}
308
309// Card departs in the travel direction and holds offset until the phone replies.
310static void start_slide_out(int direction) {
311 s_slide_direction = direction < 0 ? -1 : 1;
312 s_slide_from = s_slide_offset;
313 s_slide_to = -s_slide_direction * SLIDE_OUT_DISTANCE_PX;
314 s_motion_phase = MOTION_SLIDE_OUT;
315 s_motion_frame = 0;
316 mark_card_dirty();
317 ensure_motion_timer();
318}
319
320// New card arrives from the opposite side, settles, then bars pour in. On a
321// wraparound it overshoots past its resting position and rubber-bands back.
322static void start_slide_in(void) {
323 s_slide_from = s_slide_direction * SLIDE_IN_DISTANCE_PX;
324 s_slide_to = s_wrap_pending ? -s_slide_direction * BOUNCE_DISTANCE_PX : 0;
325 s_wrap_pending = false;
326 s_slide_offset = s_slide_from;
327 s_motion_phase = MOTION_SLIDE_IN;
328 s_motion_frame = 0;
329 mark_card_dirty();
330 ensure_motion_timer();
331}
332
333// The page never arrived: the card slides back into place.
334static void start_slide_back(void) {
335 s_wrap_pending = false;
336 if (s_motion_phase != MOTION_SLIDE_OUT && s_motion_phase != MOTION_WAITING)
337 return;
338 s_slide_from = s_slide_offset;
339 s_slide_to = 0;
340 s_motion_phase = MOTION_SLIDE_IN;
341 s_motion_frame = 0;
342 mark_card_dirty();
343 ensure_motion_timer();
344}
345
346// Data arrived in place (initial load or select refresh): cascade the bars only.
347static void start_bar_cascade(void) {
348 s_wrap_pending = false;
349 s_slide_offset = 0;
350 s_motion_phase = MOTION_BARS;
351 s_motion_frame = 0;
352 s_bar_frame = 0;
353 mark_card_dirty();
354 ensure_motion_timer();
355}
356
357static void snap_motion_idle(void) {
358 s_wrap_pending = false;
359 s_motion_phase = MOTION_IDLE;
360 s_motion_frame = 0;
361 s_slide_offset = 0;
362 s_bar_frame = bar_cascade_total_frames();
363}
364
365static void request_quota(void) {
366 DictionaryIterator *iter;
367 AppMessageResult result = app_message_outbox_begin(&iter);
368 if (result != APP_MSG_OK || iter == NULL) {
369 snprintf(s_status_text, sizeof(s_status_text), "Send fail");
370 s_is_refreshing = false;
371 mark_card_dirty();
372 return;
373 }
374
375 dict_write_uint8(iter, MESSAGE_KEY_REQUEST_QUOTA, 1);
376 dict_write_int32(iter, MESSAGE_KEY_PAGE_INDEX, s_page_index);
377 result = app_message_outbox_send();
378 if (result == APP_MSG_OK) {
379 snprintf(s_status_text, sizeof(s_status_text), "Refreshing");
380 s_updated_text[0] = '\0';
381 s_is_refreshing = true;
382 s_refresh_frame = 0;
383 mark_card_dirty();
384 ensure_motion_timer();
385 } else {
386 snprintf(s_status_text, sizeof(s_status_text), "Send fail");
387 s_is_refreshing = false;
388 mark_card_dirty();
389 }
390}
391
392static void request_page(int page_index) {
393 if (s_page_count <= 0)
394 return;
395 int direction = page_index >= s_page_index ? 1 : -1;
396 bool wrapped = false;
397 if (page_index < 0) {
398 page_index = s_page_count - 1;
399 direction = -1;
400 wrapped = true;
401 }
402 if (page_index >= s_page_count) {
403 page_index = 0;
404 direction = 1;
405 wrapped = true;
406 }
407
408 DictionaryIterator *iter;
409 AppMessageResult result = app_message_outbox_begin(&iter);
410 if (result != APP_MSG_OK || iter == NULL) {
411 snprintf(s_status_text, sizeof(s_status_text), "Page fail");
412 mark_card_dirty();
413 return;
414 }
415
416 s_page_index = page_index;
417 dict_write_int32(iter, MESSAGE_KEY_REQUEST_PAGE, page_index);
418 result = app_message_outbox_send();
419 if (result == APP_MSG_OK) {
420 s_wrap_pending = wrapped;
421 start_slide_out(direction);
422 } else {
423 snprintf(s_status_text, sizeof(s_status_text), "Send fail");
424 mark_card_dirty();
425 }
426}
427
428static void up_click_handler(ClickRecognizerRef recognizer, void *context) { request_page(s_page_index - 1); }
429
430static void down_click_handler(ClickRecognizerRef recognizer, void *context) { request_page(s_page_index + 1); }
431
432static void select_click_handler(ClickRecognizerRef recognizer, void *context) { request_quota(); }
433
434static void click_config_provider(void *context) {
435 window_single_repeating_click_subscribe(BUTTON_ID_UP, 180, up_click_handler);
436 window_single_click_subscribe(BUTTON_ID_SELECT, select_click_handler);
437 window_single_repeating_click_subscribe(BUTTON_ID_DOWN, 180, down_click_handler);
438}
439
440static void draw_text(GContext *ctx, const char *text, GRect box, GFont font, GColor color, GTextAlignment alignment) {
441 graphics_context_set_text_color(ctx, color);
442 graphics_draw_text(ctx, text, font, box, GTextOverflowModeTrailingEllipsis, alignment, NULL);
443}
444
445static int text_width(const char *text, GFont font) {
446 GSize size = graphics_text_layout_get_content_size(text, font, GRect(0, 0, 500, 30), GTextOverflowModeWordWrap,
447 GTextAlignmentLeft);
448 return size.w;
449}
450
451static void draw_progress_bar(GContext *ctx, GRect frame, int row_index, int percent, GColor color) {
452 graphics_context_set_fill_color(ctx, color_track());
453 graphics_fill_rect(ctx, frame, 3, GCornersAll);
454
455 int fill_width = (frame.size.w * current_bar_percent(row_index, percent)) / 100;
456 if (fill_width < 2 && percent > 0)
457 fill_width = 2;
458 graphics_context_set_fill_color(ctx, color);
459 graphics_fill_rect(ctx, GRect(frame.origin.x, frame.origin.y, fill_width, frame.size.h), 3, GCornersAll);
460}
461
462static void draw_status_pill(GContext *ctx, GRect frame) {
463 int inflation = status_pill_inflation();
464 GRect pill = GRect(frame.origin.x - inflation, frame.origin.y - inflation, frame.size.w + (inflation * 2),
465 frame.size.h + (inflation * 2));
466 graphics_context_set_fill_color(ctx, color_status_fill());
467 graphics_fill_rect(ctx, pill, 6, GCornersAll);
468 graphics_context_set_stroke_color(ctx, color_for_status_text());
469 graphics_draw_rect(ctx, pill);
470 draw_text(ctx, s_status_text, GRect(frame.origin.x, frame.origin.y - 4, frame.size.w, frame.size.h + 4),
471 fonts_get_system_font(FONT_KEY_GOTHIC_18_BOLD), color_text_primary(), GTextAlignmentCenter);
472}
473
474static void draw_refresh_dots(GContext *ctx, GRect bounds) {
475 if (!s_is_refreshing)
476 return;
477 int phase = s_refresh_frame;
478 int center_x = bounds.size.w - 23;
479 int center_y = 18;
480
481 graphics_context_set_fill_color(ctx, color_text_muted());
482 graphics_fill_circle(ctx, GPoint(center_x - 7, center_y), phase < 4 ? 3 : 2);
483 graphics_fill_circle(ctx, GPoint(center_x, center_y), phase >= 3 && phase < 7 ? 3 : 2);
484 graphics_fill_circle(ctx, GPoint(center_x + 7, center_y), phase >= 6 ? 3 : 2);
485}
486
487// Vertical rail along the right edge: pages travel up/down, so does the rail.
488static void draw_page_dots(GContext *ctx, GRect frame) {
489 if (s_page_count <= 1)
490 return;
491
492 int dots = s_page_count > 7 ? 7 : s_page_count;
493 int total_height = dots * 7 - 3;
494 int x = frame.origin.x + frame.size.w - 5;
495 int y = frame.origin.y + (frame.size.h - total_height) / 2;
496
497 for (int i = 0; i < dots; i += 1) {
498 int active = i == s_page_index || (s_page_count > 7 && i == 6 && s_page_index >= 6);
499 graphics_context_set_fill_color(ctx, active ? color_card_highlight() : color_text_muted());
500 int radius = active ? 2 : 1;
501 bool card_in_flight = s_motion_phase == MOTION_SLIDE_OUT || s_motion_phase == MOTION_WAITING ||
502 s_motion_phase == MOTION_SLIDE_IN || s_motion_phase == MOTION_BOUNCE;
503 if (active && card_in_flight)
504 radius = 3;
505 graphics_fill_circle(ctx, GPoint(x, y + (i * 7)), radius);
506 }
507}
508
509static void card_update_proc(Layer *layer, GContext *ctx) {
510 GRect bounds = layer_get_bounds(layer);
511 graphics_context_set_fill_color(ctx, color_background());
512 graphics_fill_rect(ctx, bounds, 0, GCornerNone);
513
514 const int margin = 10;
515 int offset = current_slide_offset();
516 int content_x = margin;
517 int content_w = bounds.size.w - (margin * 2);
518 int y = 8 + offset;
519
520 draw_text(ctx, s_title_text, GRect(content_x, y, content_w - 50, 29), fonts_get_system_font(FONT_KEY_GOTHIC_24_BOLD),
521 color_text_primary(), GTextAlignmentLeft);
522
523 char page_text[16];
524 if (s_page_count > 0) {
525 int display_page_index = s_page_index + 1;
526 if (display_page_index < 1)
527 display_page_index = 1;
528 if (display_page_index > 99)
529 display_page_index = 99;
530 int display_page_count = s_page_count;
531 if (display_page_count < 1)
532 display_page_count = 1;
533 if (display_page_count > 99)
534 display_page_count = 99;
535 snprintf(page_text, sizeof(page_text), "%d/%d", display_page_index, display_page_count);
536 } else {
537 snprintf(page_text, sizeof(page_text), "--");
538 }
539 draw_text(ctx, page_text, GRect(content_x + content_w - 46, y + 2, 46, 24),
540 fonts_get_system_font(FONT_KEY_GOTHIC_18_BOLD), color_text_secondary(), GTextAlignmentRight);
541 draw_refresh_dots(ctx, bounds);
542
543 y += 26;
544 if (s_status_text[0] != '\0') {
545 draw_status_pill(ctx, GRect(content_x + content_w - 60, y + 1, 60, 18));
546 }
547
548 y += s_status_text[0] == '\0' ? 8 : 26;
549 if (s_row_count <= 0) {
550 graphics_context_set_fill_color(ctx, color_card_highlight());
551 graphics_fill_circle(ctx, GPoint(content_x + (content_w / 2), y + 26), 15);
552 graphics_context_set_fill_color(ctx, color_background());
553 graphics_fill_circle(ctx, GPoint(content_x + (content_w / 2), y + 26), 9);
554 draw_text(ctx, "Quota unavailable", GRect(content_x, y + 48, content_w, 34),
555 fonts_get_system_font(FONT_KEY_GOTHIC_28_BOLD), color_text_primary(), GTextAlignmentCenter);
556 draw_text(ctx, s_reset_text, GRect(content_x, y + 86, content_w, 46), fonts_get_system_font(FONT_KEY_GOTHIC_24),
557 color_text_secondary(), GTextAlignmentCenter);
558 } else {
559 // Row layout heights (42/28/50 + 3px separators) are mirrored by the
560 // phone-side page packing in src/pkjs/index.js; keep them in sync.
561 GFont row_font = fonts_get_system_font(FONT_KEY_GOTHIC_18_BOLD);
562 int row_bottom_limit = bounds.size.h - 34 + offset;
563
564 // One detail column for all bar rows keeps the bar tracks aligned.
565 int detail_column_w = 28;
566 for (int i = 0; i < s_row_count && i < MAX_ROWS; i += 1) {
567 if (!s_rows[i].has_bar)
568 continue;
569 int detail_w = text_width(s_rows[i].detail, row_font);
570 if (detail_w > detail_column_w)
571 detail_column_w = detail_w;
572 }
573 if (detail_column_w > 100)
574 detail_column_w = 100;
575
576 int rows_drawn = 0;
577 for (int i = 0; i < s_row_count && i < MAX_ROWS; i += 1) {
578 QuotaRow *row = &s_rows[i];
579 GColor accent = color_for_status(row->status_rank);
580 if (row->has_bar) {
581 if (y + 42 > row_bottom_limit)
582 break;
583 draw_text(ctx, row->label, GRect(content_x, y, content_w, 22), row_font, color_text_primary(),
584 GTextAlignmentLeft);
585 y += 22;
586 int bar_w = content_w - detail_column_w - 8;
587 draw_text(ctx, row->detail, GRect(content_x + bar_w + 8, y - 3, detail_column_w, 22), row_font,
588 color_text_soft(), GTextAlignmentRight);
589 draw_progress_bar(ctx, GRect(content_x, y + 6, bar_w, 6), i, row->percent, accent);
590 y += 20;
591 } else {
592 int detail_w = text_width(row->detail, row_font);
593 if (detail_w > 100)
594 detail_w = 100;
595 int label_w = content_w - detail_w - 8;
596 bool two_line = text_width(row->label, row_font) > label_w;
597 if (y + (two_line ? 50 : 28) > row_bottom_limit)
598 break;
599 if (two_line) {
600 draw_text(ctx, row->label, GRect(content_x, y, content_w, 22), row_font, color_text_primary(),
601 GTextAlignmentLeft);
602 y += 22;
603 draw_text(ctx, row->detail, GRect(content_x, y, content_w, 22), row_font, color_text_primary(),
604 GTextAlignmentRight);
605 y += 28;
606 } else {
607 draw_text(ctx, row->label, GRect(content_x, y, label_w, 22), row_font, color_text_primary(),
608 GTextAlignmentLeft);
609 draw_text(ctx, row->detail, GRect(content_x + label_w + 8, y, detail_w, 22), row_font, color_text_primary(),
610 GTextAlignmentRight);
611 y += 28;
612 }
613 }
614 rows_drawn += 1;
615 if (i + 1 < s_row_count && i + 1 < MAX_ROWS) {
616 graphics_context_set_stroke_color(ctx, color_card_outline());
617 graphics_draw_line(ctx, GPoint(content_x, y), GPoint(content_x + content_w, y));
618 y += 3;
619 }
620 }
621
622 int rows_visible = s_row_count > MAX_ROWS ? MAX_ROWS : s_row_count;
623 if (rows_drawn < rows_visible || s_row_count > MAX_ROWS) {
624 draw_text(ctx, "+ more", GRect(content_x, y, content_w, 22), fonts_get_system_font(FONT_KEY_GOTHIC_18),
625 color_text_muted(), GTextAlignmentCenter);
626 }
627 }
628
629 draw_page_dots(ctx, bounds);
630
631 draw_text(ctx, s_reset_text, GRect(content_x, bounds.size.h - 32 + offset, content_w, 24),
632 fonts_get_system_font(FONT_KEY_GOTHIC_18), color_text_muted(), GTextAlignmentCenter);
633}
634
635static void copy_string_tuple(DictionaryIterator *iterator, uint32_t key, char *buffer, size_t buffer_size) {
636 Tuple *tuple = dict_find(iterator, key);
637 if (tuple == NULL)
638 return;
639 snprintf(buffer, buffer_size, "%s", tuple->value->cstring);
640}
641
642static void read_row(DictionaryIterator *iterator, int row_index) {
643 QuotaRow *row = &s_rows[row_index];
644 copy_string_tuple(iterator, row_label_key(row_index), row->label, sizeof(row->label));
645 copy_string_tuple(iterator, row_detail_key(row_index), row->detail, sizeof(row->detail));
646
647 Tuple *percent_tuple = dict_find(iterator, row_percent_key(row_index));
648 row->percent = percent_tuple != NULL ? clamp_percent((int)percent_tuple->value->int32) : 0;
649
650 Tuple *status_tuple = dict_find(iterator, row_status_key(row_index));
651 row->status_rank = status_tuple != NULL ? (int)status_tuple->value->int32 : 1;
652
653 Tuple *has_bar_tuple = dict_find(iterator, row_has_bar_key(row_index));
654 row->has_bar = has_bar_tuple != NULL && has_bar_tuple->value->int32 != 0;
655}
656
657static void inbox_received_callback(DictionaryIterator *iterator, void *context) {
658 Tuple *error_tuple = dict_find(iterator, MESSAGE_KEY_ERROR);
659 if (error_tuple != NULL) {
660 s_is_refreshing = false;
661 s_row_count = 0;
662 s_page_index = 0;
663 s_page_count = 0;
664 snprintf(s_title_text, sizeof(s_title_text), "Pebblexus");
665 snprintf(s_reset_text, sizeof(s_reset_text), "%s", error_tuple->value->cstring);
666 snprintf(s_status_text, sizeof(s_status_text), "Select retries");
667 s_updated_text[0] = '\0';
668 snap_motion_idle();
669 mark_card_dirty();
670 return;
671 }
672
673 static char s_last_data_status[16] = "";
674 bool was_refreshing = s_is_refreshing;
675
676 s_is_refreshing = false;
677 copy_string_tuple(iterator, MESSAGE_KEY_QUOTA_TITLE, s_title_text, sizeof(s_title_text));
678 copy_string_tuple(iterator, MESSAGE_KEY_QUOTA_LABEL, s_group_text, sizeof(s_group_text));
679 copy_string_tuple(iterator, MESSAGE_KEY_QUOTA_RESET, s_reset_text, sizeof(s_reset_text));
680 copy_string_tuple(iterator, MESSAGE_KEY_QUOTA_STATUS, s_status_text, sizeof(s_status_text));
681 copy_string_tuple(iterator, MESSAGE_KEY_UPDATED_AT, s_updated_text, sizeof(s_updated_text));
682
683 // Redundant haptic feedback only when a refresh reveals a newly critical
684 // page; browsing between known-critical pages stays silent.
685 if (was_refreshing && strcmp(s_status_text, "CRITICAL") == 0 && strcmp(s_last_data_status, "CRITICAL") != 0)
686 vibes_short_pulse();
687 snprintf(s_last_data_status, sizeof(s_last_data_status), "%s", s_status_text);
688
689 Tuple *page_index_tuple = dict_find(iterator, MESSAGE_KEY_PAGE_INDEX);
690 if (page_index_tuple != NULL)
691 s_page_index = (int)page_index_tuple->value->int32;
692
693 Tuple *page_count_tuple = dict_find(iterator, MESSAGE_KEY_PAGE_COUNT);
694 if (page_count_tuple != NULL)
695 s_page_count = (int)page_count_tuple->value->int32;
696 if (s_page_count < 0)
697 s_page_count = 0;
698 if (s_page_index >= s_page_count)
699 s_page_index = s_page_count - 1;
700 if (s_page_index < 0)
701 s_page_index = 0;
702
703 Tuple *row_count_tuple = dict_find(iterator, MESSAGE_KEY_ROW_COUNT);
704 int row_count = row_count_tuple != NULL ? (int)row_count_tuple->value->int32 : 0;
705 if (row_count < 0)
706 row_count = 0;
707 s_row_count = row_count;
708
709 int rows_to_read = row_count > MAX_ROWS ? MAX_ROWS : row_count;
710 for (int i = 0; i < rows_to_read; i += 1) {
711 s_rows[i].label[0] = '\0';
712 s_rows[i].detail[0] = '\0';
713 s_rows[i].percent = 0;
714 s_rows[i].status_rank = 1;
715 s_rows[i].has_bar = false;
716 read_row(iterator, i);
717 }
718
719 if (s_motion_phase == MOTION_SLIDE_OUT || s_motion_phase == MOTION_WAITING) {
720 start_slide_in();
721 } else {
722 start_bar_cascade();
723 }
724}
725
726static void inbox_dropped_callback(AppMessageResult reason, void *context) {
727 s_is_refreshing = false;
728 snprintf(s_status_text, sizeof(s_status_text), "Dropped");
729 start_slide_back();
730 mark_card_dirty();
731}
732
733static void outbox_failed_callback(DictionaryIterator *iterator, AppMessageResult reason, void *context) {
734 s_is_refreshing = false;
735 snprintf(s_status_text, sizeof(s_status_text), "No phone");
736 start_slide_back();
737 mark_card_dirty();
738}
739
740static void main_window_load(Window *window) {
741 Layer *window_layer = window_get_root_layer(window);
742 GRect bounds = layer_get_bounds(window_layer);
743
744 s_card_layer = layer_create(bounds);
745 layer_set_update_proc(s_card_layer, card_update_proc);
746 layer_add_child(window_layer, s_card_layer);
747}
748
749static void main_window_unload(Window *window) {
750 if (s_animation_timer != NULL) {
751 app_timer_cancel(s_animation_timer);
752 s_animation_timer = NULL;
753 }
754 layer_destroy(s_card_layer);
755}
756
757static void init(void) {
758 s_main_window = window_create();
759 window_set_background_color(s_main_window, color_background());
760 window_set_click_config_provider(s_main_window, click_config_provider);
761 window_set_window_handlers(s_main_window, (WindowHandlers){.load = main_window_load, .unload = main_window_unload});
762
763 app_message_register_inbox_received(inbox_received_callback);
764 app_message_register_inbox_dropped(inbox_dropped_callback);
765 app_message_register_outbox_failed(outbox_failed_callback);
766 app_message_open(2048, 256);
767
768 window_stack_push(s_main_window, true);
769 request_quota();
770}
771
772static void deinit(void) { window_destroy(s_main_window); }
773
774int main(void) {
775 init();
776 app_event_loop();
777 deinit();
778}