commit 3ca84a252d004419eee5e26cf7b3bd9c42f3d1c8 Author: Your Name Date: Sat Aug 22 20:14:56 2026 +0300 First diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f078361 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +build/ +Debug/ \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..a078e9f --- /dev/null +++ b/README.md @@ -0,0 +1,24 @@ +# Percentage + +This is source code for "Percentage" pebble app. It tracks success/fail rates of 9 items. It was designed in cloudpebble. + + + +## Features + +* Shows time +* Shows battery +* Side up/down buttons to scroll +* Scrollbar +* Touch buttons to add success/fail +* Phone config to add items +* Color coded success rate +* MAX 9 items = 3 pages +* MAX 10 characters +* Choose between percentage and success/total display + +## Resources + +* Pixelorama project files (Blue/Red/Green) +* C code +* Silkscreen font \ No newline at end of file diff --git a/design/Percentage.pxo b/design/Percentage.pxo new file mode 100644 index 0000000..c80ca40 Binary files /dev/null and b/design/Percentage.pxo differ diff --git a/design/PercentageGreen.pxo b/design/PercentageGreen.pxo new file mode 100644 index 0000000..7ad5aa7 Binary files /dev/null and b/design/PercentageGreen.pxo differ diff --git a/design/PercentageRed.pxo b/design/PercentageRed.pxo new file mode 100644 index 0000000..aabe6b9 Binary files /dev/null and b/design/PercentageRed.pxo differ diff --git a/jshintrc b/jshintrc new file mode 100644 index 0000000..4c5376d --- /dev/null +++ b/jshintrc @@ -0,0 +1,46 @@ + +/* + * Example jshint configuration file for Pebble development. + * + * Check out the full documentation at http://www.jshint.com/docs/options/ + */ +{ + // Declares the existence of the globals available in PebbleKit JS. + "globals": {"console": true, "setTimeout": true, "setInterval": true, "Int8Array": true, "Uint8Array": true, "Uint8ClampedArray": true, "Int16Array": true, "Uint16Array": true, "Int32Array": true, "Uint32Array": true, "Float32Array": true, "Float64Array": true, "Pebble": true, "WebSocket": true, "XMLHttpRequest": true, "navigator": true, "localStorage": true, "require": true, "exports": true, "module": true}, + + // Do not mess with standard JavaScript objects (Array, Date, etc) + "freeze": true, + + // Do not use eval! Keep this warning turned on (ie: false) + "evil": false, + + /* + * The options below are more style/developer dependent. + * Customize to your liking. + */ + + // All variables should be in camelcase - too specific for CloudPebble builds to fail + // "camelcase": true, + + // Do not allow blocks without { } - too specific for CloudPebble builds to fail. + // "curly": true, + + // Prohibits the use of immediate function invocations without wrapping them in parentheses + "immed": true, + + // Don't enforce indentation, because it's not worth failing builds over + // (especially given our somewhat lacklustre support for it) + "indent": false, + + // Do not use a variable before it's defined + "latedef": "nofunc", + + // Spot undefined variables + "undef": "true", + + // Spot unused variables + "unused": "true", + + // Enable ES6 syntax (const, let, arrow functions, etc) + "esversion": 6 +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..235f581 --- /dev/null +++ b/package.json @@ -0,0 +1,137 @@ +{ + "author": "aintmina@protonmail.com", + "dependencies": { + "@rebble/clay": "^1.0.10" + }, + "keywords": [], + "name": "percentage", + "pebble": { + "capabilities": [ + "configurable" + ], + "displayName": "Percentage", + "enableMultiJS": true, + "messageKeys": [ + "ITEM_1", + "ITEM_2", + "ITEM_3", + "ITEM_4", + "ITEM_5", + "ITEM_6", + "ITEM_7", + "ITEM_8", + "ITEM_9", + "ITEM_1_SUCCESS", + "ITEM_1_FAIL", + "ITEM_1_PERCENTAGE", + "ITEM_2_SUCCESS", + "ITEM_2_FAIL", + "ITEM_2_PERCENTAGE", + "ITEM_3_SUCCESS", + "ITEM_3_FAIL", + "ITEM_3_PERCENTAGE", + "ITEM_4_SUCCESS", + "ITEM_4_FAIL", + "ITEM_4_PERCENTAGE", + "ITEM_5_SUCCESS", + "ITEM_5_FAIL", + "ITEM_5_PERCENTAGE", + "ITEM_6_SUCCESS", + "ITEM_6_FAIL", + "ITEM_6_PERCENTAGE", + "ITEM_7_SUCCESS", + "ITEM_7_FAIL", + "ITEM_7_PERCENTAGE", + "ITEM_8_SUCCESS", + "ITEM_8_FAIL", + "ITEM_8_PERCENTAGE", + "ITEM_9_SUCCESS", + "ITEM_9_FAIL", + "ITEM_9_PERCENTAGE" + ], + "projectType": "native", + "resources": { + "media": [ + { + "file": "images/scroll_bar_half.png", + "name": "IMAGE_SCROLL_BAR_HALF", + "targetPlatforms": null, + "type": "bitmap" + }, + { + "file": "images/scroll_bar_full.png", + "name": "IMAGE_SCROLL_BAR_FULL", + "targetPlatforms": null, + "type": "bitmap" + }, + { + "file": "images/item_background.png", + "name": "IMAGE_ITEM_BACKGROUND", + "spaceOptimization": "storage", + "storageFormat": "png", + "targetPlatforms": null, + "type": "bitmap" + }, + { + "file": "images/background_minus_pressed.png", + "name": "IMAGE_BACKGROUND_MINUS_PRESSED", + "targetPlatforms": null, + "type": "bitmap" + }, + { + "file": "images/background_plus_pressed.png", + "name": "IMAGE_BACKGROUND_PLUS_PRESSED", + "targetPlatforms": null, + "type": "bitmap" + }, + { + "file": "images/plus_button.png", + "name": "IMAGE_PLUS_BUTTON", + "targetPlatforms": null, + "type": "bitmap" + }, + { + "file": "images/minus_button.png", + "name": "IMAGE_MINUS_BUTTON", + "targetPlatforms": null, + "type": "bitmap" + }, + { + "file": "images/background.png", + "name": "IMAGE_BACKGROUND", + "spaceOptimization": "storage", + "targetPlatforms": null, + "type": "bitmap" + }, + { + "file": "fonts/slkscr.ttf", + "name": "FONT_SLKSCR_16", + "targetPlatforms": null, + "type": "font" + }, + { + "file": "images/scroll_bar_third.png", + "name": "IMAGE_SCROLL_BAR_THIRD", + "targetPlatforms": null, + "type": "bitmap" + }, + { + "file": "images/menu_icon.png", + "menuIcon": true, + "name": "IMAGE_MENU_ICON", + "targetPlatforms": null, + "type": "bitmap" + } + ] + }, + "sdkVersion": "3", + "targetPlatforms": [ + "emery" + ], + "uuid": "7fbfd849-da80-4589-9e81-22f67339edc3", + "watchapp": { + "watchface": false + } + }, + "version": "1.0.0" +} diff --git a/resources/fonts/slkscr.ttf b/resources/fonts/slkscr.ttf new file mode 100644 index 0000000..b110719 Binary files /dev/null and b/resources/fonts/slkscr.ttf differ diff --git a/resources/images/background.png b/resources/images/background.png new file mode 100644 index 0000000..1197d02 Binary files /dev/null and b/resources/images/background.png differ diff --git a/resources/images/background_minus_pressed.png b/resources/images/background_minus_pressed.png new file mode 100644 index 0000000..9ef5de4 Binary files /dev/null and b/resources/images/background_minus_pressed.png differ diff --git a/resources/images/background_plus_pressed.png b/resources/images/background_plus_pressed.png new file mode 100644 index 0000000..5cbfea7 Binary files /dev/null and b/resources/images/background_plus_pressed.png differ diff --git a/resources/images/item_background.png b/resources/images/item_background.png new file mode 100644 index 0000000..9641600 Binary files /dev/null and b/resources/images/item_background.png differ diff --git a/resources/images/menu_icon.png b/resources/images/menu_icon.png new file mode 100644 index 0000000..de6f358 Binary files /dev/null and b/resources/images/menu_icon.png differ diff --git a/resources/images/minus_button.png b/resources/images/minus_button.png new file mode 100644 index 0000000..b73a7b6 Binary files /dev/null and b/resources/images/minus_button.png differ diff --git a/resources/images/plus_button.png b/resources/images/plus_button.png new file mode 100644 index 0000000..edba71e Binary files /dev/null and b/resources/images/plus_button.png differ diff --git a/resources/images/scroll_bar_full.png b/resources/images/scroll_bar_full.png new file mode 100644 index 0000000..8d12363 Binary files /dev/null and b/resources/images/scroll_bar_full.png differ diff --git a/resources/images/scroll_bar_half.png b/resources/images/scroll_bar_half.png new file mode 100644 index 0000000..0431b40 Binary files /dev/null and b/resources/images/scroll_bar_half.png differ diff --git a/resources/images/scroll_bar_third.png b/resources/images/scroll_bar_third.png new file mode 100644 index 0000000..2b6dbb9 Binary files /dev/null and b/resources/images/scroll_bar_third.png differ diff --git a/screenshots/pic1.png b/screenshots/pic1.png new file mode 100644 index 0000000..82b3e13 Binary files /dev/null and b/screenshots/pic1.png differ diff --git a/src/c/battery.c b/src/c/battery.c new file mode 100644 index 0000000..9ff37fa --- /dev/null +++ b/src/c/battery.c @@ -0,0 +1,98 @@ +#include "battery.h" +#include "src/c/colors.h" + +static Layer *s_battery_layer; +static int s_battery_percent; + + +static void battery_draw(Layer * layer, GContext *ctx) { + GRect bounds = layer_get_bounds(layer); + + int width = (bounds.size.w - 2) * s_battery_percent / 100; + + if (width == 0) { + return; + } + + // Get color + GColor normal; + GColor light; + GColor dark; + + if (s_battery_percent > 20) { + normal = GREEN; + light = GREEN_LIGHT; + dark = GREEN_DARK; + } + else if (s_battery_percent > 10) { + normal = ORANGE; + light = ORANGE_LIGHT; + dark = ORANGE_DARK; + } + else { + normal = RED; + light = RED_LIGHT; + dark = RED_DARK; + } + + // Draw infill + graphics_context_set_fill_color(ctx, normal); + graphics_fill_rect( + ctx, + GRect(2, 2, width-2, bounds.size.h-4), + 0, + GCornerNone + ); + + // Draw normal color conrers + graphics_fill_rect( + ctx, + GRect(0, bounds.size.h-2, 2, 2), + 0, + GCornerNone + ); + graphics_fill_rect( + ctx, + GRect(width, 0, 2, 2), + 0, + GCornerNone + ); + + // Draw light edges + graphics_context_set_stroke_color(ctx, light); + graphics_draw_line(ctx, GPoint(0, 0), GPoint(width-1, 0)); + graphics_draw_line(ctx, GPoint(0, 1), GPoint(width-1, 1)); + graphics_draw_line(ctx,GPoint(0, 0), GPoint(0, bounds.size.h-3)); + graphics_draw_line(ctx,GPoint(1, 0), GPoint(1, bounds.size.h-3)); + + // Draw dark edges + graphics_context_set_stroke_color(ctx, dark); + graphics_draw_line(ctx, GPoint(2, bounds.size.h-2), GPoint(width+1, bounds.size.h-2)); + graphics_draw_line(ctx, GPoint(2, bounds.size.h-1), GPoint(width+1, bounds.size.h-1)); + graphics_draw_line(ctx,GPoint(width, 2), GPoint(width, bounds.size.h-1)); + graphics_draw_line(ctx,GPoint(width+1, 2), GPoint(width+1, bounds.size.h-1)); +} + +static void battery_changed(BatteryChargeState state) { + s_battery_percent = state.charge_percent; + layer_mark_dirty(s_battery_layer); +} + +void battery_init(Layer *parent) { + GRect frame = BATTERY_RECT; + s_battery_layer = layer_create(frame); + + layer_set_update_proc(s_battery_layer, battery_draw); + layer_add_child(parent, s_battery_layer); + + battery_state_service_subscribe(battery_changed); + battery_changed(battery_state_service_peek()); +} + +void battery_deinit() { + // Battery unsubscribe + battery_state_service_unsubscribe(); + + // Destroy battery layer + layer_destroy(s_battery_layer); +} \ No newline at end of file diff --git a/src/c/battery.h b/src/c/battery.h new file mode 100644 index 0000000..59749f0 --- /dev/null +++ b/src/c/battery.h @@ -0,0 +1,12 @@ +#pragma once + + +#include + + +// BATTERY POSITION +#define BATTERY_RECT GRect(12, 206, 176, 10) + + +void battery_init(Layer *parent); +void battery_deinit(); \ No newline at end of file diff --git a/src/c/colors.h b/src/c/colors.h new file mode 100644 index 0000000..852b60d --- /dev/null +++ b/src/c/colors.h @@ -0,0 +1,21 @@ +#pragma once + + +// BLACK/WHITE +#define BLACK GColorFromHEX(0x000000) +#define WHITE GColorFromHEX(0xffffff) + +// GREEN +#define GREEN GColorFromHEX(0x55aa00) +#define GREEN_LIGHT GColorFromHEX(0xaaff00) +#define GREEN_DARK GColorFromHEX(0x005500) + +// ORANGE +#define ORANGE GColorFromHEX(0xffaa00) +#define ORANGE_LIGHT GColorFromHEX(0xffff00) +#define ORANGE_DARK GColorFromHEX(0xaa5500) + +// RED +#define RED GColorFromHEX(0xaa0000) +#define RED_LIGHT GColorFromHEX(0xff0000) +#define RED_DARK GColorFromHEX(0x550000) \ No newline at end of file diff --git a/src/c/items.c b/src/c/items.c new file mode 100644 index 0000000..3b530e8 --- /dev/null +++ b/src/c/items.c @@ -0,0 +1,342 @@ +#include +#include "items.h" +#include "src/c/scrolllayer.h" + + +item_list_t s_items; + + +// ------------ NVM ------------ +static void items_nvm_load() { + if (persist_exists(ITEMS_NVM_KEY)) { + persist_read_data(ITEMS_NVM_KEY, s_items.items, sizeof(s_items.items)); + } + else { + memset(s_items.items, 0, sizeof(s_items.items)); + } +} + +static void items_nvm_save() { + persist_write_data(ITEMS_NVM_KEY, s_items.items, sizeof(s_items.items)); +} + +// ------------ ITEMS ------------ +static void items_organize(item_list_t *item_list) { + item_list->num_items = 0; + + for (int i = 0; i < MAX_ITEMS; i++) { + if (item_list->items[i].name[0] != 0) { + if (item_list->num_items != i) { + // Copy item up + memcpy(&item_list->items[item_list->num_items], &item_list->items[i], sizeof(item_list->items[i])); + // Delete item + memset(&item_list->items[i], 0, sizeof(item_list->items[i])); + } + item_list->num_items++; + } + } +} + +item_list_t *items_get() { + return &s_items; +} + +// ------------ PHONE ------------ +static void update_items(item_list_t *items, const item_list_t *buffer) { + if (!items || !buffer) { + return; + } + + // Check existing items + for (int i = 0; i < MAX_ITEMS; i++) { + if (items->items[i].name[0] == '\0') { + continue; + } + + bool found = false; + for (int j = 0; j < MAX_ITEMS; j++) { + if (buffer->items[j].name[0] != '\0' && strcmp(items->items[i].name, buffer->items[j].name) == 0) { + + // Item still exists + items->items[i].show_percentage = buffer->items[j].show_percentage; + found = true; + break; + } + } + + // Item no longer exists + if (!found) { + memset(&items->items[i], 0, sizeof(items->items[i])); + } + } + + // Add items + for (int i = 0; i < MAX_ITEMS; i++) { + if (buffer->items[i].name[0] == '\0') { + continue; + } + + bool found = false; + for (int j = 0; j < MAX_ITEMS; j++) { + if (items->items[j].name[0] != '\0' && strcmp(items->items[j].name, buffer->items[i].name) == 0) { + found = true; + break; + } + } + + if (!found) { + for (int j = 0; j < MAX_ITEMS; j++) { + if (items->items[j].name[0] == '\0') { + memcpy(&items->items[j], &buffer->items[i], sizeof(items->items[j])); + break; + } + } + } + } + + items_organize(items); +} + +static void inbox_received_handler(DictionaryIterator *iter, void *context) { + // Remove scroll layer items + scroll_layer_remove_items(); + + Tuple *tuple; + item_list_t item_buffer; + + // Get the item 1 + tuple = dict_find(iter, MESSAGE_KEY_ITEM_1); + if (tuple) { + strncpy(item_buffer.items[0].name, tuple->value->cstring, MAX_CHARACTERS); + } + tuple = dict_find(iter, MESSAGE_KEY_ITEM_1_SUCCESS); + if (tuple) { + int success = atoi(tuple->value->cstring); + if (success < 0) { success = 0; } + item_buffer.items[0].successes = success; + } + tuple = dict_find(iter, MESSAGE_KEY_ITEM_1_FAIL); + if (tuple) { + int fail = atoi(tuple->value->cstring); + if (fail < 0) { fail = 0; } + item_buffer.items[0].fails = fail; + } + tuple = dict_find(iter, MESSAGE_KEY_ITEM_1_PERCENTAGE); + if (tuple) { + bool show_percentage = tuple->value->int32 != 0; + item_buffer.items[0].show_percentage = show_percentage; + } + + // Get the item 2 + tuple = dict_find(iter, MESSAGE_KEY_ITEM_2); + if (tuple) { + strncpy(item_buffer.items[1].name, tuple->value->cstring, MAX_CHARACTERS); + } + tuple = dict_find(iter, MESSAGE_KEY_ITEM_2_SUCCESS); + if (tuple) { + int success = atoi(tuple->value->cstring); + if (success < 0) { success = 0; } + item_buffer.items[1].successes = success; + } + tuple = dict_find(iter, MESSAGE_KEY_ITEM_2_FAIL); + if (tuple) { + int fail = atoi(tuple->value->cstring); + if (fail < 0) { fail = 0; } + item_buffer.items[1].fails = fail; + } + tuple = dict_find(iter, MESSAGE_KEY_ITEM_2_PERCENTAGE); + if (tuple) { + bool show_percentage = tuple->value->int32 != 0; + item_buffer.items[1].show_percentage = show_percentage; + } + + // Get the item 3 + tuple = dict_find(iter, MESSAGE_KEY_ITEM_3); + if (tuple) { + strncpy(item_buffer.items[2].name, tuple->value->cstring, MAX_CHARACTERS); + } + tuple = dict_find(iter, MESSAGE_KEY_ITEM_3_SUCCESS); + if (tuple) { + int success = atoi(tuple->value->cstring); + if (success < 0) { success = 0; } + item_buffer.items[2].successes = success; + } + tuple = dict_find(iter, MESSAGE_KEY_ITEM_3_FAIL); + if (tuple) { + int fail = atoi(tuple->value->cstring); + if (fail < 0) { fail = 0; } + item_buffer.items[2].fails = fail; + } + tuple = dict_find(iter, MESSAGE_KEY_ITEM_3_PERCENTAGE); + if (tuple) { + bool show_percentage = tuple->value->int32 != 0; + item_buffer.items[2].show_percentage = show_percentage; + } + + // Get the item 4 + tuple = dict_find(iter, MESSAGE_KEY_ITEM_4); + if (tuple) { + strncpy(item_buffer.items[3].name, tuple->value->cstring, MAX_CHARACTERS); + } + tuple = dict_find(iter, MESSAGE_KEY_ITEM_4_SUCCESS); + if (tuple) { + int success = atoi(tuple->value->cstring); + if (success < 0) { success = 0; } + item_buffer.items[3].successes = success; + } + tuple = dict_find(iter, MESSAGE_KEY_ITEM_4_FAIL); + if (tuple) { + int fail = atoi(tuple->value->cstring); + if (fail < 0) { fail = 0; } + item_buffer.items[3].fails = fail; + } + tuple = dict_find(iter, MESSAGE_KEY_ITEM_4_PERCENTAGE); + if (tuple) { + bool show_percentage = tuple->value->int32 != 0; + item_buffer.items[3].show_percentage = show_percentage; + } + + // Get the item 5 + tuple = dict_find(iter, MESSAGE_KEY_ITEM_5); + if (tuple) { + strncpy(item_buffer.items[4].name, tuple->value->cstring, MAX_CHARACTERS); + } + tuple = dict_find(iter, MESSAGE_KEY_ITEM_5_SUCCESS); + if (tuple) { + int success = atoi(tuple->value->cstring); + if (success < 0) { success = 0; } + item_buffer.items[4].successes = success; + } + tuple = dict_find(iter, MESSAGE_KEY_ITEM_5_FAIL); + if (tuple) { + int fail = atoi(tuple->value->cstring); + if (fail < 0) { fail = 0; } + item_buffer.items[4].fails = fail; + } + tuple = dict_find(iter, MESSAGE_KEY_ITEM_5_PERCENTAGE); + if (tuple) { + bool show_percentage = tuple->value->int32 != 0; + item_buffer.items[4].show_percentage = show_percentage; + } + + // Get the item 6 + tuple = dict_find(iter, MESSAGE_KEY_ITEM_6); + if (tuple) { + strncpy(item_buffer.items[5].name, tuple->value->cstring, MAX_CHARACTERS); + } + tuple = dict_find(iter, MESSAGE_KEY_ITEM_6_SUCCESS); + if (tuple) { + int success = atoi(tuple->value->cstring); + if (success < 0) { success = 0; } + item_buffer.items[5].successes = success; + } + tuple = dict_find(iter, MESSAGE_KEY_ITEM_6_FAIL); + if (tuple) { + int fail = atoi(tuple->value->cstring); + if (fail < 0) { fail = 0; } + item_buffer.items[5].fails = fail; + } + tuple = dict_find(iter, MESSAGE_KEY_ITEM_6_PERCENTAGE); + if (tuple) { + bool show_percentage = tuple->value->int32 != 0; + item_buffer.items[5].show_percentage = show_percentage; + } + + // Get the item 7 + tuple = dict_find(iter, MESSAGE_KEY_ITEM_7); + if (tuple) { + strncpy(item_buffer.items[6].name, tuple->value->cstring, MAX_CHARACTERS); + } + tuple = dict_find(iter, MESSAGE_KEY_ITEM_7_SUCCESS); + if (tuple) { + int success = atoi(tuple->value->cstring); + if (success < 0) { success = 0; } + item_buffer.items[6].successes = success; + } + tuple = dict_find(iter, MESSAGE_KEY_ITEM_7_FAIL); + if (tuple) { + int fail = atoi(tuple->value->cstring); + if (fail < 0) { fail = 0; } + item_buffer.items[6].fails = fail; + } + tuple = dict_find(iter, MESSAGE_KEY_ITEM_7_PERCENTAGE); + if (tuple) { + bool show_percentage = tuple->value->int32 != 0; + item_buffer.items[6].show_percentage = show_percentage; + } + + // Get the item 8 + tuple = dict_find(iter, MESSAGE_KEY_ITEM_8); + if (tuple) { + strncpy(item_buffer.items[7].name, tuple->value->cstring, MAX_CHARACTERS); + } + tuple = dict_find(iter, MESSAGE_KEY_ITEM_8_SUCCESS); + if (tuple) { + int success = atoi(tuple->value->cstring); + if (success < 0) { success = 0; } + item_buffer.items[7].successes = success; + } + tuple = dict_find(iter, MESSAGE_KEY_ITEM_8_FAIL); + if (tuple) { + int fail = atoi(tuple->value->cstring); + if (fail < 0) { fail = 0; } + item_buffer.items[7].fails = fail; + } + tuple = dict_find(iter, MESSAGE_KEY_ITEM_8_PERCENTAGE); + if (tuple) { + bool show_percentage = tuple->value->int32 != 0; + item_buffer.items[7].show_percentage = show_percentage; + } + + // Get the item 9 + tuple = dict_find(iter, MESSAGE_KEY_ITEM_9); + if (tuple) { + strncpy(item_buffer.items[8].name, tuple->value->cstring, MAX_CHARACTERS); + } + tuple = dict_find(iter, MESSAGE_KEY_ITEM_9_SUCCESS); + if (tuple) { + int success = atoi(tuple->value->cstring); + if (success < 0) { success = 0; } + item_buffer.items[8].successes = success; + } + tuple = dict_find(iter, MESSAGE_KEY_ITEM_9_FAIL); + if (tuple) { + int fail = atoi(tuple->value->cstring); + if (fail < 0) { fail = 0; } + item_buffer.items[8].fails = fail; + } + tuple = dict_find(iter, MESSAGE_KEY_ITEM_9_PERCENTAGE); + if (tuple) { + bool show_percentage = tuple->value->int32 != 0; + item_buffer.items[8].show_percentage = show_percentage; + } + + // Organize buffer + items_organize(&item_buffer); + + // Update list from buffer + update_items(&s_items, &item_buffer); + + // Save the items to NVM + items_organize(&s_items); + items_nvm_save(); + + // Update the ScrollLayer + scroll_layer_add_items(); + scroll_layer_update_scrollbar(); +} + +// ------------ INITS ------------ +void items_init() { + // Read items from NVM + items_nvm_load(); + items_organize(&s_items); + + // Register received handler + app_message_register_inbox_received(inbox_received_handler); + app_message_open(app_message_inbox_size_maximum(), app_message_outbox_size_maximum()); +} + +void items_deinit() { + items_nvm_save(); +} \ No newline at end of file diff --git a/src/c/items.h b/src/c/items.h new file mode 100644 index 0000000..88e799e --- /dev/null +++ b/src/c/items.h @@ -0,0 +1,25 @@ +#pragma once + + +// NVM +#define ITEMS_NVM_KEY 1 +#define MAX_ITEMS 9 +#define MAX_CHARACTERS 10 + + +typedef struct { + char name[MAX_CHARACTERS+1]; + int successes; + int fails; + bool show_percentage; +} item_t; + +typedef struct { + int num_items; + item_t items[MAX_ITEMS]; +} item_list_t; + + +item_list_t *items_get(); +void items_init(); +void items_deinit(); \ No newline at end of file diff --git a/src/c/percentage.c b/src/c/percentage.c new file mode 100644 index 0000000..7e33d59 --- /dev/null +++ b/src/c/percentage.c @@ -0,0 +1,81 @@ +#include + +#include "src/c/battery.h" +#include "src/c/time.h" +#include "src/c/scrolllayer.h" + + +static Window *s_main_window; +static Layer *s_background_layer; +static GBitmap *s_background_bitmap; + + +static void update_background(Layer *layer, GContext *ctx) { + GRect bounds = gbitmap_get_bounds(s_background_bitmap); + graphics_draw_bitmap_in_rect( + ctx, + s_background_bitmap, + GRect(0, 0, bounds.size.w, bounds.size.h) + ); +} + +static void main_window_load(Window *window) { + Layer *window_layer = window_get_root_layer(window); + GRect bounds = layer_get_bounds(window_layer); + + // Create background layer + s_background_layer = layer_create(bounds); + layer_set_update_proc(s_background_layer, update_background); + layer_add_child(window_layer, s_background_layer); + + // Load background bitmap + s_background_bitmap = gbitmap_create_with_resource(RESOURCE_ID_IMAGE_BACKGROUND); + + // Init items + items_init(); + + // Init and draw the battery + battery_init(window_layer); + + // Init and draw the time + time_init(window_layer); + + // Init and draw the ScrollLayer + scrolllayer_init(window_layer, window); +} + +static void main_window_unload(Window *window) { + // Deinit the items + items_deinit(); + + // Deinit the battery + battery_deinit(); + + // Deinit the time + time_deinit(); + + // Deinit the ScrollLayer + scrolllayer_deinit(); +} + +static void init() { + // MAIN WINDOW CREATE + s_main_window = window_create(); + window_set_background_color(s_main_window, GColorBlack); + window_set_window_handlers(s_main_window, (WindowHandlers) { + .load = main_window_load, + .unload = main_window_unload + }); + window_stack_push(s_main_window, true); +} + +static void deinit() { + // MAIN WINDOW DESTROY + window_destroy(s_main_window); +} + +int main() { + init(); + app_event_loop(); + deinit(); +} \ No newline at end of file diff --git a/src/c/scrolllayer.c b/src/c/scrolllayer.c new file mode 100644 index 0000000..d85ab5a --- /dev/null +++ b/src/c/scrolllayer.c @@ -0,0 +1,560 @@ +#include +#include "scrolllayer.h" + +#include "src/c/colors.h" + +static ScrollLayer *s_scroll_layer; +static Layer *s_scroll_bar_layer; +static GFont s_scrolllayer_font; +static GBitmap *s_background_bitmap; +static GBitmap *s_plus_bitmap; +static GBitmap *s_minus_bitmap; +static GBitmap *s_background_plus_pressed_bitmap; +static GBitmap *s_background_minus_pressed_bitmap; +static GBitmap *s_scroll_bar_current_bitmap; +static GBitmap *s_scroll_bar_full_bitmap; +static GBitmap *s_scroll_bar_half_bitmap; +static GBitmap *s_scroll_bar_third_bitmap; + +// TOUCH BUTTONS +static GRect item_1_plus_button; +static GRect item_1_minus_button; +static GRect item_2_plus_button; +static GRect item_2_minus_button; +static GRect item_3_plus_button; +static GRect item_3_minus_button; + +// SCROLL +static int s_scroll_y; +static int s_scroll_page; +static scroll_items_t s_scroll_items; + + +// ------------ SCROLL ------------ +static void scroll_up(ClickRecognizerRef recognizer, void *context) { + s_scroll_y -= MAX_UI_ITEMS * ITEM_UI_HEIGHT; + s_scroll_page -= 1; + + if (s_scroll_y < 0) { + s_scroll_y = 0; + } + + if (s_scroll_page < 0) { + s_scroll_page = 0; + } + + // Move scroll bar + GRect scroll_frame = layer_get_frame(s_scroll_bar_layer); + int scroll_frame_y = scroll_frame.origin.y - scroll_frame.size.h; + if (scroll_frame_y < SCROLL_BAR_ORIGIN_HEIGHT) { + scroll_frame_y = SCROLL_BAR_ORIGIN_HEIGHT; + } + scroll_frame.origin.y = scroll_frame_y; + layer_set_frame(s_scroll_bar_layer, scroll_frame); + layer_mark_dirty(s_scroll_bar_layer); + + scroll_layer_set_content_offset( + s_scroll_layer, + GPoint(0, -s_scroll_y), + true + ); +} + +static void scroll_down(ClickRecognizerRef recognizer, void *context) { + s_scroll_y += MAX_UI_ITEMS * ITEM_UI_HEIGHT; + s_scroll_page += 1; + int max_scroll = MAX_UI_ITEMS * ITEM_UI_HEIGHT * (s_scroll_items.page_count - 1); + + if (s_scroll_y > max_scroll) { + s_scroll_y = max_scroll; + } + + if (s_scroll_page > ((MAX_ITEMS / MAX_UI_ITEMS) - 1)) { + s_scroll_page = ((MAX_ITEMS / MAX_UI_ITEMS) - 1); + } + + // Move scroll bar + GRect scroll_frame = layer_get_frame(s_scroll_bar_layer); + int scroll_frame_y = scroll_frame.origin.y + scroll_frame.size.h; + if (scroll_frame_y > (SCROLL_BAR_ORIGIN_HEIGHT_END - scroll_frame.size.h)) { + scroll_frame_y = (SCROLL_BAR_ORIGIN_HEIGHT_END - scroll_frame.size.h); + } + scroll_frame.origin.y = scroll_frame_y; + layer_set_frame(s_scroll_bar_layer, scroll_frame); + layer_mark_dirty(s_scroll_bar_layer); + + scroll_layer_set_content_offset( + s_scroll_layer, + GPoint(0, -s_scroll_y), + true + ); +} + +// ------------ BUTTONS ------------ +static void click_config_provider(void *context) { + window_single_click_subscribe(BUTTON_ID_UP, scroll_up); + window_single_click_subscribe(BUTTON_ID_DOWN, scroll_down); +} + +// ------------ HELPERS ------------ +static GColor get_color_float(float val) { + GColor color; + + if (val < RED_LIMIT) { + color = RED; + } + else if (val < ORANGE_LIMIT) { + color = ORANGE; + } + else { + color = GREEN; + } + + return color; +} + +static void bar_draw(Layer *layer, GContext *ctx) { + GColor *color = layer_get_data(layer); + + graphics_context_set_fill_color(ctx, *color); + graphics_fill_rect(ctx, layer_get_bounds(layer), 0, GCornerNone); +} + +static void scroll_bar_draw(Layer *layer, GContext *ctx) { + GRect bounds = gbitmap_get_bounds(s_scroll_bar_current_bitmap); + graphics_draw_bitmap_in_rect(ctx, s_scroll_bar_current_bitmap, GRect(0, 0, bounds.size.w, bounds.size.h)); +} + +static void fill_percentage_buffer(scroll_items_t *items, int element, int total, float percentage) { + if (total > 1000) { items->items->items[element].show_percentage = true; } + + if (items->items->items[element].show_percentage) { + int percentage_int = (percentage * 100); + snprintf( + items->numbers_buffer[element], + sizeof(items->numbers_buffer[element]), + "%d%%", + percentage_int + ); + } + else { + snprintf( + items->numbers_buffer[element], + sizeof(items->numbers_buffer[element]), + "%d/%d", + items->items->items[element].successes, + total + ); + } +} + +void scroll_layer_add_items() { + GRect bounds = SCROLLLAYER_FRAME_RECT; + int n_items = s_scroll_items.items->num_items-1; + if (n_items < 0) { n_items = 0; } + s_scroll_items.page_count = (n_items / MAX_UI_ITEMS) + 1; + + // Add entries + for (int i = 0; i < s_scroll_items.items->num_items; i++) { + // Add background + s_scroll_items.backgrounds[i] = bitmap_layer_create(GRect(0, i * ITEM_UI_HEIGHT, bounds.size.w, ITEM_UI_HEIGHT)); + bitmap_layer_set_bitmap(s_scroll_items.backgrounds[i], s_background_bitmap); + bitmap_layer_set_compositing_mode(s_scroll_items.backgrounds[i], GCompOpSet); // Set transparency + scroll_layer_add_child(s_scroll_layer, bitmap_layer_get_layer(s_scroll_items.backgrounds[i])); + + // Add text + s_scroll_items.texts[i] = text_layer_create(GRect(26, (i * ITEM_UI_HEIGHT) + 4, 124, 16)); + text_layer_set_text(s_scroll_items.texts[i], s_scroll_items.items->items[i].name); + text_layer_set_font(s_scroll_items.texts[i], s_scrolllayer_font); + text_layer_set_text_alignment(s_scroll_items.texts[i], GTextAlignmentCenter); + text_layer_set_background_color(s_scroll_items.texts[i], GColorClear); + text_layer_set_text_color(s_scroll_items.texts[i], WHITE); + scroll_layer_add_child(s_scroll_layer, text_layer_get_layer(s_scroll_items.texts[i])); + + // Add percentage + int total = s_scroll_items.items->items[i].successes + s_scroll_items.items->items[i].fails; + float percentage = (float)s_scroll_items.items->items[i].successes / total; + GColor color = get_color_float(percentage); + s_scroll_items.numbers[i] = text_layer_create(GRect(26, (i * ITEM_UI_HEIGHT) + 18, 124, 16)); + fill_percentage_buffer(&s_scroll_items, i, total, percentage); + text_layer_set_text(s_scroll_items.numbers[i], s_scroll_items.numbers_buffer[i]); + text_layer_set_font(s_scroll_items.numbers[i], s_scrolllayer_font); + text_layer_set_text_alignment(s_scroll_items.numbers[i], GTextAlignmentCenter); + text_layer_set_background_color(s_scroll_items.numbers[i], GColorClear); + text_layer_set_text_color(s_scroll_items.numbers[i], color); + scroll_layer_add_child(s_scroll_layer, text_layer_get_layer(s_scroll_items.numbers[i])); + + // Add plus button + s_scroll_items.plus_buttons[i] = bitmap_layer_create(GRect(146, (i * ITEM_UI_HEIGHT) + 18, 20, 20)); + bitmap_layer_set_bitmap(s_scroll_items.plus_buttons[i], s_plus_bitmap); + bitmap_layer_set_compositing_mode(s_scroll_items.plus_buttons[i], GCompOpSet); // Set transparency + scroll_layer_add_child(s_scroll_layer, bitmap_layer_get_layer(s_scroll_items.plus_buttons[i])); + + // Add minus button + s_scroll_items.minus_buttons[i] = bitmap_layer_create(GRect(7, (i * ITEM_UI_HEIGHT) + 18, 20, 20)); + bitmap_layer_set_bitmap(s_scroll_items.minus_buttons[i], s_minus_bitmap); + bitmap_layer_set_compositing_mode(s_scroll_items.minus_buttons[i], GCompOpSet); // Set transparency + scroll_layer_add_child(s_scroll_layer, bitmap_layer_get_layer(s_scroll_items.minus_buttons[i])); + + // Add bar + int width = (PERCENTAGE_BAR_WIDTH) * percentage; + GRect bar = GRect(31, (i * ITEM_UI_HEIGHT) + 40, width, 4); + s_scroll_items.success_bars[i] = layer_create_with_data(bar, sizeof(GColor)); + GColor *color_ptr = layer_get_data(s_scroll_items.success_bars[i]); + *color_ptr = color; + layer_set_update_proc(s_scroll_items.success_bars[i], bar_draw); + scroll_layer_add_child(s_scroll_layer, s_scroll_items.success_bars[i]); + } +} + +void scroll_layer_remove_items() { + // Destroy items + for (int i = 0; i < MAX_ITEMS; i++) { + // TextLayers + text_layer_destroy(s_scroll_items.texts[i]); + text_layer_destroy(s_scroll_items.numbers[i]); + + // BitmapLayers + bitmap_layer_destroy(s_scroll_items.backgrounds[i]); + bitmap_layer_destroy(s_scroll_items.plus_buttons[i]); + bitmap_layer_destroy(s_scroll_items.minus_buttons[i]); + + // Layers + layer_destroy(s_scroll_items.success_bars[i]); + + // Destroy the pointers + s_scroll_items.texts[i] = NULL; + s_scroll_items.numbers[i] = NULL; + s_scroll_items.backgrounds[i] = NULL; + s_scroll_items.plus_buttons[i] = NULL; + s_scroll_items.minus_buttons[i] = NULL; + s_scroll_items.success_bars[i] = NULL; + } +} + +void scroll_layer_update_scrollbar() { + GRect scroll_box = layer_get_bounds(s_scroll_bar_layer); + GRect scroll_frame = layer_get_frame(s_scroll_bar_layer); + + if (s_scroll_items.page_count == 2) { + scroll_box.size.h = 82; + scroll_frame.size.h = 82; + s_scroll_bar_current_bitmap = s_scroll_bar_half_bitmap; + } + else if (s_scroll_items.page_count == 3) { + scroll_box.size.h = 52; + scroll_frame.size.h = 52; + s_scroll_bar_current_bitmap = s_scroll_bar_third_bitmap; + } + else { + scroll_box.size.h = 160; + scroll_frame.size.h = 160; + s_scroll_bar_current_bitmap = s_scroll_bar_full_bitmap; + } + + layer_set_bounds(s_scroll_bar_layer, scroll_box); + layer_set_frame(s_scroll_bar_layer, scroll_frame); + layer_mark_dirty(s_scroll_bar_layer); +} + +// ------------ TOUCH ------------ +static void touch_handler(const TouchEvent *event, void *context) { + // Get touch point + GPoint point = GPoint(event->x, event->y); + + // First item buttons + if (grect_contains_point(&item_1_plus_button, &point)) { + int element = 0 + (s_scroll_page * MAX_UI_ITEMS); + + if (element >= s_scroll_items.items->num_items) { + return; + } + + if (event->type == TouchEvent_Touchdown) { + // Change background to pressed + bitmap_layer_set_bitmap(s_scroll_items.backgrounds[element], s_background_plus_pressed_bitmap); + + // Update numbers + s_scroll_items.items->items[element].successes++; + int total = s_scroll_items.items->items[element].successes + s_scroll_items.items->items[element].fails; + float percentage = (float)s_scroll_items.items->items[element].successes / total; + GColor color = get_color_float(percentage); + fill_percentage_buffer(&s_scroll_items, element, total, percentage); + text_layer_set_text_color(s_scroll_items.numbers[element], color); + text_layer_set_text(s_scroll_items.numbers[element], s_scroll_items.numbers_buffer[element]); + + // Update the percentage bar + int width = (PERCENTAGE_BAR_WIDTH) * percentage; + GRect bar = GRect(31, (element * ITEM_UI_HEIGHT) + 40, width, 4); + GColor *color_ptr = layer_get_data(s_scroll_items.success_bars[element]); + *color_ptr = color; + layer_set_frame(s_scroll_items.success_bars[element], bar); + } + else if (event->type == TouchEvent_Liftoff) { + // Reset the background + bitmap_layer_set_bitmap(s_scroll_items.backgrounds[element], s_background_bitmap); + } + } + + if (grect_contains_point(&item_1_minus_button, &point)) { + int element = 0 + (s_scroll_page * MAX_UI_ITEMS); + + if (element >= s_scroll_items.items->num_items) { + return; + } + + if (event->type == TouchEvent_Touchdown) { + // Change background to pressed + bitmap_layer_set_bitmap(s_scroll_items.backgrounds[element], s_background_minus_pressed_bitmap); + + s_scroll_items.items->items[element].fails++; + int total = s_scroll_items.items->items[element].successes + s_scroll_items.items->items[element].fails; + float percentage = (float)s_scroll_items.items->items[element].successes / total; + GColor color = get_color_float(percentage); + fill_percentage_buffer(&s_scroll_items, element, total, percentage); + text_layer_set_text_color(s_scroll_items.numbers[element], color); + text_layer_set_text(s_scroll_items.numbers[element], s_scroll_items.numbers_buffer[element]); + + // Update the percentage bar + int width = (PERCENTAGE_BAR_WIDTH) * percentage; + GRect bar = GRect(31, (element * ITEM_UI_HEIGHT) + 40, width, 4); + GColor *color_ptr = layer_get_data(s_scroll_items.success_bars[element]); + *color_ptr = color; + layer_set_frame(s_scroll_items.success_bars[element], bar); + } + else if (event->type == TouchEvent_Liftoff) { + // Reset the background + bitmap_layer_set_bitmap(s_scroll_items.backgrounds[element], s_background_bitmap); + } + } + + // Second item buttons + if (grect_contains_point(&item_2_plus_button, &point)) { + int element = 1 + (s_scroll_page * MAX_UI_ITEMS); + + if (element >= s_scroll_items.items->num_items) { + return; + } + + if (event->type == TouchEvent_Touchdown) { + // Change background to pressed + bitmap_layer_set_bitmap(s_scroll_items.backgrounds[element], s_background_plus_pressed_bitmap); + + s_scroll_items.items->items[element].successes++; + int total = s_scroll_items.items->items[element].successes + s_scroll_items.items->items[element].fails; + float percentage = (float)s_scroll_items.items->items[element].successes / total; + GColor color = get_color_float(percentage); + fill_percentage_buffer(&s_scroll_items, element, total, percentage); + text_layer_set_text_color(s_scroll_items.numbers[element], color); + text_layer_set_text(s_scroll_items.numbers[element], s_scroll_items.numbers_buffer[element]); + + // Update the percentage bar + int width = (PERCENTAGE_BAR_WIDTH) * percentage; + GRect bar = GRect(31, (element * ITEM_UI_HEIGHT) + 40, width, 4); + GColor *color_ptr = layer_get_data(s_scroll_items.success_bars[element]); + *color_ptr = color; + layer_set_frame(s_scroll_items.success_bars[element], bar); + } + else if (event->type == TouchEvent_Liftoff) { + // Reset the background + bitmap_layer_set_bitmap(s_scroll_items.backgrounds[element], s_background_bitmap); + } + } + + if (grect_contains_point(&item_2_minus_button, &point)) { + int element = 1 + (s_scroll_page * MAX_UI_ITEMS); + + if (element >= s_scroll_items.items->num_items) { + return; + } + + if (event->type == TouchEvent_Touchdown) { + // Change background to pressed + bitmap_layer_set_bitmap(s_scroll_items.backgrounds[element], s_background_minus_pressed_bitmap); + + s_scroll_items.items->items[element].fails++; + int total = s_scroll_items.items->items[element].successes + s_scroll_items.items->items[element].fails; + float percentage = (float)s_scroll_items.items->items[element].successes / total; + GColor color = get_color_float(percentage); + fill_percentage_buffer(&s_scroll_items, element, total, percentage); + text_layer_set_text_color(s_scroll_items.numbers[element], color); + text_layer_set_text(s_scroll_items.numbers[element], s_scroll_items.numbers_buffer[element]); + + // Update the percentage bar + int width = (PERCENTAGE_BAR_WIDTH) * percentage; + GRect bar = GRect(31, (element * ITEM_UI_HEIGHT) + 40, width, 4); + GColor *color_ptr = layer_get_data(s_scroll_items.success_bars[element]); + *color_ptr = color; + layer_set_frame(s_scroll_items.success_bars[element], bar); + } + else if (event->type == TouchEvent_Liftoff) { + // Reset the background + bitmap_layer_set_bitmap(s_scroll_items.backgrounds[element], s_background_bitmap); + } + } + + // Thisrd item buttons + if (grect_contains_point(&item_3_plus_button, &point)) { + int element = 2 + (s_scroll_page * MAX_UI_ITEMS); + + if (element >= s_scroll_items.items->num_items) { + return; + } + + if (event->type == TouchEvent_Touchdown) { + // Change background to pressed + bitmap_layer_set_bitmap(s_scroll_items.backgrounds[element], s_background_plus_pressed_bitmap); + + s_scroll_items.items->items[element].successes++; + int total = s_scroll_items.items->items[element].successes + s_scroll_items.items->items[element].fails; + float percentage = (float)s_scroll_items.items->items[element].successes / total; + GColor color = get_color_float(percentage); + fill_percentage_buffer(&s_scroll_items, element, total, percentage); + text_layer_set_text_color(s_scroll_items.numbers[element], color); + text_layer_set_text(s_scroll_items.numbers[element], s_scroll_items.numbers_buffer[element]); + + // Update the percentage bar + int width = (PERCENTAGE_BAR_WIDTH) * percentage; + GRect bar = GRect(31, (element * ITEM_UI_HEIGHT) + 40, width, 4); + GColor *color_ptr = layer_get_data(s_scroll_items.success_bars[element]); + *color_ptr = color; + layer_set_frame(s_scroll_items.success_bars[element], bar); + } + else if (event->type == TouchEvent_Liftoff) { + // Reset the background + bitmap_layer_set_bitmap(s_scroll_items.backgrounds[element], s_background_bitmap); + } + } + + if (grect_contains_point(&item_3_minus_button, &point)) { + int element = 2 + (s_scroll_page * MAX_UI_ITEMS); + + if (element >= s_scroll_items.items->num_items) { + return; + } + + if (event->type == TouchEvent_Touchdown) { + // Change background to pressed + bitmap_layer_set_bitmap(s_scroll_items.backgrounds[element], s_background_minus_pressed_bitmap); + + s_scroll_items.items->items[element].fails++; + int total = s_scroll_items.items->items[element].successes + s_scroll_items.items->items[element].fails; + float percentage = (float)s_scroll_items.items->items[element].successes / total; + GColor color = get_color_float(percentage); + fill_percentage_buffer(&s_scroll_items, element, total, percentage); + text_layer_set_text_color(s_scroll_items.numbers[element], color); + text_layer_set_text(s_scroll_items.numbers[element], s_scroll_items.numbers_buffer[element]); + + // Update the percentage bar + int width = (PERCENTAGE_BAR_WIDTH) * percentage; + GRect bar = GRect(31, (element * ITEM_UI_HEIGHT) + 40, width, 4); + GColor *color_ptr = layer_get_data(s_scroll_items.success_bars[element]); + *color_ptr = color; + layer_set_frame(s_scroll_items.success_bars[element], bar); + } + else if (event->type == TouchEvent_Liftoff) { + // Reset the background + bitmap_layer_set_bitmap(s_scroll_items.backgrounds[element], s_background_bitmap); + } + } +} + +// ------------ INITS ------------ +void scrolllayer_init(Layer *parent, Window *window) { + s_scroll_y = 0; + s_scroll_page = 0; + GRect bounds = SCROLLLAYER_FRAME_RECT; + + // Load custom font + s_scrolllayer_font = fonts_load_custom_font(resource_get_handle(RESOURCE_ID_FONT_SLKSCR_16)); + + // Add touch areas + item_1_plus_button = ITEM_1_PLUS_BUTTON_RECT; + item_1_minus_button = ITEM_1_MINUS_BUTTON_RECT; + item_2_plus_button = ITEM_2_PLUS_BUTTON_RECT; + item_2_minus_button = ITEM_2_MINUS_BUTTON_RECT; + item_3_plus_button = ITEM_3_PLUS_BUTTON_RECT; + item_3_minus_button = ITEM_3_MINUS_BUTTON_RECT; + + // Load bitmaps + s_background_bitmap = gbitmap_create_with_resource(RESOURCE_ID_IMAGE_ITEM_BACKGROUND); + s_plus_bitmap = gbitmap_create_with_resource(RESOURCE_ID_IMAGE_PLUS_BUTTON); + s_minus_bitmap = gbitmap_create_with_resource(RESOURCE_ID_IMAGE_MINUS_BUTTON); + s_background_plus_pressed_bitmap = gbitmap_create_with_resource(RESOURCE_ID_IMAGE_BACKGROUND_PLUS_PRESSED); + s_background_minus_pressed_bitmap = gbitmap_create_with_resource(RESOURCE_ID_IMAGE_BACKGROUND_MINUS_PRESSED); + s_scroll_bar_full_bitmap = gbitmap_create_with_resource(RESOURCE_ID_IMAGE_SCROLL_BAR_FULL); + s_scroll_bar_half_bitmap = gbitmap_create_with_resource(RESOURCE_ID_IMAGE_SCROLL_BAR_HALF); + s_scroll_bar_third_bitmap = gbitmap_create_with_resource(RESOURCE_ID_IMAGE_SCROLL_BAR_THIRD); + + // Create ScrollLayer + s_scroll_layer = scroll_layer_create(bounds); + scroll_layer_set_content_size( + s_scroll_layer, + GSize(bounds.size.w, MAX_ITEMS * ITEM_UI_HEIGHT) + ); + + // Remove shadow + scroll_layer_set_shadow_hidden(s_scroll_layer, true); + // Button scroll + window_set_click_config_provider(window, click_config_provider); + + // Get entries + s_scroll_items.items = items_get(); + + // Add items + scroll_layer_add_items(); + + // Add ScrollLayer + layer_add_child(parent, scroll_layer_get_layer(s_scroll_layer)); + + // Add scroll bar + GRect scroll_box = SCROLL_BAR_FRAME_RECT; + if (s_scroll_items.page_count == 2) { + scroll_box.size.h = 82; + s_scroll_bar_layer = layer_create(scroll_box); + s_scroll_bar_current_bitmap = s_scroll_bar_half_bitmap; + + } + else if (s_scroll_items.page_count == 3) { + scroll_box.size.h = 52; + s_scroll_bar_layer = layer_create(scroll_box); + s_scroll_bar_current_bitmap = s_scroll_bar_third_bitmap; + } + else { + scroll_box.size.h = 160; + s_scroll_bar_layer = layer_create(scroll_box); + s_scroll_bar_current_bitmap = s_scroll_bar_full_bitmap; + } + layer_set_update_proc(s_scroll_bar_layer, scroll_bar_draw); + layer_add_child(parent, s_scroll_bar_layer); + + // Register touch handler + touch_service_subscribe(touch_handler, NULL); +} + +void scrolllayer_deinit() { + // Unload font + fonts_unload_custom_font(s_scrolllayer_font); + + // Unsubscribe touch + touch_service_unsubscribe(); + + // Destroy ScrollLayer + scroll_layer_destroy(s_scroll_layer); + + // Destroy bitmaps + gbitmap_destroy(s_background_bitmap); + gbitmap_destroy(s_plus_bitmap); + gbitmap_destroy(s_minus_bitmap); + gbitmap_destroy(s_background_plus_pressed_bitmap); + gbitmap_destroy(s_background_minus_pressed_bitmap); + gbitmap_destroy(s_scroll_bar_full_bitmap); + gbitmap_destroy(s_scroll_bar_half_bitmap); + gbitmap_destroy(s_scroll_bar_third_bitmap); + + // Destroy scroll bar + layer_destroy(s_scroll_bar_layer); + + // Destroy items + scroll_layer_remove_items(); +} \ No newline at end of file diff --git a/src/c/scrolllayer.h b/src/c/scrolllayer.h new file mode 100644 index 0000000..0ebe812 --- /dev/null +++ b/src/c/scrolllayer.h @@ -0,0 +1,49 @@ +#pragma once + + +#include + +#include "src/c/items.h" + + +// SCROLLLAYER POSITION +#define SCROLLLAYER_FRAME_RECT GRect(5, 30, 174, 168) +#define SCROLL_BAR_FRAME_RECT GRect(180, 34, 10, 160) +#define SCROLL_BAR_ORIGIN_HEIGHT 34 +#define SCROLL_BAR_ORIGIN_HEIGHT_END 34 + 160 +#define PERCENTAGE_BAR_WIDTH 112 + +// TOUCH BUTTON RECTS +#define ITEM_1_PLUS_BUTTON_RECT GRect(110, 32, 70, 56) +#define ITEM_1_MINUS_BUTTON_RECT GRect(8, 32, 70, 56) +#define ITEM_2_PLUS_BUTTON_RECT GRect(110, 88, 70, 56) +#define ITEM_2_MINUS_BUTTON_RECT GRect(8, 88, 70, 56) +#define ITEM_3_PLUS_BUTTON_RECT GRect(110, 144, 70, 56) +#define ITEM_3_MINUS_BUTTON_RECT GRect(8, 144, 70, 56) + +// UI +#define MAX_UI_ITEMS 3 +#define MAX_UI_PAGES (MAX_ITEMS / MAX_UI_ITEMS) +#define ITEM_UI_HEIGHT 56 + +// LIMITS +#define RED_LIMIT 0.1 +#define ORANGE_LIMIT 0.4 + +typedef struct { + item_list_t *items; + int page_count; + TextLayer *texts[MAX_ITEMS]; + TextLayer *numbers[MAX_ITEMS]; + char numbers_buffer[MAX_ITEMS][MAX_CHARACTERS]; + BitmapLayer *backgrounds[MAX_ITEMS]; + BitmapLayer *plus_buttons[MAX_ITEMS]; + BitmapLayer *minus_buttons[MAX_ITEMS]; + Layer *success_bars[MAX_ITEMS]; +} scroll_items_t; + +void scroll_layer_add_items(); +void scroll_layer_remove_items(); +void scroll_layer_update_scrollbar(); +void scrolllayer_init(Layer *parent, Window *window); +void scrolllayer_deinit(); \ No newline at end of file diff --git a/src/c/time.c b/src/c/time.c new file mode 100644 index 0000000..d15cbe2 --- /dev/null +++ b/src/c/time.c @@ -0,0 +1,58 @@ +#include "time.h" + +#include "src/c/colors.h" + +static TextLayer *s_time_layer; +static GFont s_time_font; + + +static void update_time() { + // Get tm structure + time_t temp = time(NULL); + struct tm *tick_time = localtime(&temp); + + // Write the current time + static char s_time_buffer[8]; + strftime(s_time_buffer, sizeof(s_time_buffer), + clock_is_24h_style() ? "%H:%M" : "%I:%M", + tick_time); + + // Display the time + text_layer_set_text(s_time_layer, s_time_buffer); +} + +static void tick_handler(struct tm *tick_time, TimeUnits units_changed) { + update_time(); +} + +void time_init(Layer *parent) { + // Load custom font + s_time_font = fonts_load_custom_font(resource_get_handle(RESOURCE_ID_FONT_SLKSCR_16)); + + // Create the time TextLayer + s_time_layer = text_layer_create(TIME_RECT); + text_layer_set_background_color(s_time_layer, GColorClear); + text_layer_set_text_color(s_time_layer, WHITE); + text_layer_set_font(s_time_layer, s_time_font); + text_layer_set_text_alignment(s_time_layer, GTextAlignmentCenter); + + // Add child + layer_add_child(parent, text_layer_get_layer(s_time_layer)); + + // Display the time on start + update_time(); + + // Register with TickTimerService + tick_timer_service_subscribe(MINUTE_UNIT, tick_handler); +} + +void time_deinit() { + // Destroy time layer + text_layer_destroy(s_time_layer); + + // Unload font + fonts_unload_custom_font(s_time_font); + + // Unsubscribe timer + tick_timer_service_unsubscribe(); +} \ No newline at end of file diff --git a/src/c/time.h b/src/c/time.h new file mode 100644 index 0000000..9b4bc77 --- /dev/null +++ b/src/c/time.h @@ -0,0 +1,12 @@ +#pragma once + + +#include + + +// TIME POSITION +#define TIME_RECT GRect(8, 6, 186, 20) + + +void time_init(Layer *parent); +void time_deinit(); \ No newline at end of file diff --git a/src/pkjs/config.js b/src/pkjs/config.js new file mode 100644 index 0000000..6be4d1a --- /dev/null +++ b/src/pkjs/config.js @@ -0,0 +1,312 @@ +module.exports = [ + { + "type": "heading", + "defaultValue": "App configuration" + }, + { + "type": "text", + "defaultValue": "Character limit is 10. If numbers go higher than 1000, use percentage." + }, + + { + "type": "section", + "items": [ + { + "type": "heading", + "defaultValue": "Item 1" + }, + { + "type": "input", + "messageKey": "ITEM_1", + "defaultValue": "" + }, + { + "type": "input", + "messageKey": "ITEM_1_SUCCESS", + "label": "Success", + "defaultValue": "0" + }, + { + "type": "input", + "messageKey": "ITEM_1_FAIL", + "label": "Fail", + "defaultValue": "0" + }, + { + "type": "toggle", + "messageKey": "ITEM_1_PERCENTAGE", + "label": "Show percentage", + "defaultValue": false + } + ] + }, + + { + "type": "section", + "items": [ + { + "type": "heading", + "defaultValue": "Item 2" + }, + { + "type": "input", + "messageKey": "ITEM_2", + "defaultValue": "" + }, + { + "type": "input", + "messageKey": "ITEM_2_SUCCESS", + "label": "Success", + "defaultValue": "0" + }, + { + "type": "input", + "messageKey": "ITEM_2_FAIL", + "label": "Fail", + "defaultValue": "0" + }, + { + "type": "toggle", + "messageKey": "ITEM_2_PERCENTAGE", + "label": "Show percentage", + "defaultValue": false + } + ] + }, + + { + "type": "section", + "items": [ + { + "type": "heading", + "defaultValue": "Item 3" + }, + { + "type": "input", + "messageKey": "ITEM_3", + "defaultValue": "" + }, + { + "type": "input", + "messageKey": "ITEM_3_SUCCESS", + "label": "Success", + "defaultValue": "0" + }, + { + "type": "input", + "messageKey": "ITEM_3_FAIL", + "label": "Fail", + "defaultValue": "0" + }, + { + "type": "toggle", + "messageKey": "ITEM_3_PERCENTAGE", + "label": "Show percentage", + "defaultValue": false + } + ] + }, + + { + "type": "section", + "items": [ + { + "type": "heading", + "defaultValue": "Item 4" + }, + { + "type": "input", + "messageKey": "ITEM_4", + "defaultValue": "" + }, + { + "type": "input", + "messageKey": "ITEM_4_SUCCESS", + "label": "Success", + "defaultValue": "0" + }, + { + "type": "input", + "messageKey": "ITEM_4_FAIL", + "label": "Fail", + "defaultValue": "0" + }, + { + "type": "toggle", + "messageKey": "ITEM_4_PERCENTAGE", + "label": "Show percentage", + "defaultValue": false + } + ] + }, + + { + "type": "section", + "items": [ + { + "type": "heading", + "defaultValue": "Item 5" + }, + { + "type": "input", + "messageKey": "ITEM_5", + "defaultValue": "" + }, + { + "type": "input", + "messageKey": "ITEM_5_SUCCESS", + "label": "Success", + "defaultValue": "0" + }, + { + "type": "input", + "messageKey": "ITEM_5_FAIL", + "label": "Fail", + "defaultValue": "0" + }, + { + "type": "toggle", + "messageKey": "ITEM_5_PERCENTAGE", + "label": "Show percentage", + "defaultValue": false + } + ] + }, + + { + "type": "section", + "items": [ + { + "type": "heading", + "defaultValue": "Item 6" + }, + { + "type": "input", + "messageKey": "ITEM_6", + "defaultValue": "" + }, + { + "type": "input", + "messageKey": "ITEM_6_SUCCESS", + "label": "Success", + "defaultValue": "0" + }, + { + "type": "input", + "messageKey": "ITEM_6_FAIL", + "label": "Fail", + "defaultValue": "0" + }, + { + "type": "toggle", + "messageKey": "ITEM_6_PERCENTAGE", + "label": "Show percentage", + "defaultValue": false + } + ] + }, + + { + "type": "section", + "items": [ + { + "type": "heading", + "defaultValue": "Item 7" + }, + { + "type": "input", + "messageKey": "ITEM_7", + "defaultValue": "" + }, + { + "type": "input", + "messageKey": "ITEM_7_SUCCESS", + "label": "Success", + "defaultValue": "0" + }, + { + "type": "input", + "messageKey": "ITEM_7_FAIL", + "label": "Fail", + "defaultValue": "0" + }, + { + "type": "toggle", + "messageKey": "ITEM_7_PERCENTAGE", + "label": "Show percentage", + "defaultValue": false + } + ] + }, + + { + "type": "section", + "items": [ + { + "type": "heading", + "defaultValue": "Item 8" + }, + { + "type": "input", + "messageKey": "ITEM_8", + "defaultValue": "" + }, + { + "type": "input", + "messageKey": "ITEM_8_SUCCESS", + "label": "Success", + "defaultValue": "0" + }, + { + "type": "input", + "messageKey": "ITEM_8_FAIL", + "label": "Fail", + "defaultValue": "0" + }, + { + "type": "toggle", + "messageKey": "ITEM_8_PERCENTAGE", + "label": "Show percentage", + "defaultValue": false + } + ] + }, + + { + "type": "section", + "items": [ + { + "type": "heading", + "defaultValue": "Item 9" + }, + { + "type": "input", + "messageKey": "ITEM_9", + "defaultValue": "" + }, + { + "type": "input", + "messageKey": "ITEM_9_SUCCESS", + "label": "Success", + "defaultValue": "0" + }, + { + "type": "input", + "messageKey": "ITEM_9_FAIL", + "label": "Fail", + "defaultValue": "0" + }, + { + "type": "toggle", + "messageKey": "ITEM_9_PERCENTAGE", + "label": "Show percentage", + "defaultValue": false + } + ] + }, + + { + "type": "submit", + "defaultValue": "Save Settings" + } +]; \ No newline at end of file diff --git a/src/pkjs/index.js b/src/pkjs/index.js new file mode 100644 index 0000000..db8cf38 --- /dev/null +++ b/src/pkjs/index.js @@ -0,0 +1,3 @@ +var Clay = require('@rebble/clay'); +var clayConfig = require('./config'); +var clay = new Clay(clayConfig); diff --git a/wscript b/wscript new file mode 100644 index 0000000..c18af6c --- /dev/null +++ b/wscript @@ -0,0 +1,57 @@ +# +# This file is the default set of rules to compile a Pebble project. +# +# Feel free to customize this to your needs. +# + +import os.path +try: + from sh import CommandNotFound, jshint, cat, ErrorReturnCode_2 + hint = jshint +except (ImportError, Exception): + hint = None + +top = '.' +out = 'build' + + +def options(ctx): + ctx.load('pebble_sdk') + + +def configure(ctx): + ctx.load('pebble_sdk') + + +def build(ctx): + if False and hint is not None: + try: + hint(['--config', 'pebble-jshintrc'] + [node.abspath() for node in ctx.path.ant_glob("src/**/*.js")], _tty_out=False) # no tty because there are none in the cloudpebble sandbox. + except ErrorReturnCode_2 as e: + ctx.fatal("\nJavaScript linting failed (you can disable this in Project Settings):\n" + e.stdout) + + ctx.load('pebble_sdk') + + build_worker = os.path.exists('worker_src') + binaries = [] + + for p in ctx.env.TARGET_PLATFORMS: + ctx.set_env(ctx.all_envs[p]) + ctx.set_group(ctx.env.PLATFORM_NAME) + app_elf = '{}/pebble-app.elf'.format(ctx.env.BUILD_DIR) + ctx.pbl_program(source=ctx.path.ant_glob('src/c/**/*.c'), target=app_elf) + + if build_worker: + worker_elf = '{}/pebble-worker.elf'.format(ctx.env.BUILD_DIR) + binaries.append({'platform': p, 'app_elf': app_elf, 'worker_elf': worker_elf}) + ctx.pbl_worker(source=ctx.path.ant_glob('worker_src/c/**/*.c'), target=worker_elf) + else: + binaries.append({'platform': p, 'app_elf': app_elf}) + + ctx(features='subst', + source='package.json', + target='js/package.json', + is_copy=True) + + ctx.set_group('bundle') + ctx.pbl_bundle(binaries=binaries, js=ctx.path.ant_glob(['src/pkjs/**/*.js', 'src/pkjs/**/*.json']), js_entry_file='src/pkjs/index.js')