Compare commits
10 Commits
bfd88af248
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 2d242a48e1 | |||
| d5b9c443a0 | |||
| 6c239163de | |||
| 8d509af3f8 | |||
| 0becfa0243 | |||
| 6bb04801bc | |||
| f3365a8dac | |||
| ebca07666e | |||
| 92dfcfabcd | |||
| eac496be0e |
@@ -0,0 +1,52 @@
|
|||||||
|
# Countdown Watchface
|
||||||
|
|
||||||
|
A clean Pebble watchface with a top progress bar that counts down to your next half-hour mark or upcoming calendar appointment.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **Time** — large 24-hour format (`HH:MM`) using the Roboto Bold 49 font
|
||||||
|
- **Date** — day/month number (`DD/MM`) in Gothic 28 Bold below the time
|
||||||
|
- **Countdown bar** — 8px bar across the top of the screen:
|
||||||
|
- Starts full at the top of each half hour and drains to empty as it approaches `:00` or `:30`
|
||||||
|
- If a calendar appointment is within the next 30 minutes, the bar counts down to that instead
|
||||||
|
- Requires Google Calendar API setup (see below); falls back to half-hour countdown without it
|
||||||
|
- **Battery bar** — thin 4px indicator along the bottom edge
|
||||||
|
|
||||||
|
## Supported platforms
|
||||||
|
|
||||||
|
`aplite`, `basalt`, `diorite`, `emery`, `flint`
|
||||||
|
|
||||||
|
## Building
|
||||||
|
|
||||||
|
Requires the [Pebble SDK](https://developer.rebble.io/developer.pebble.com/sdk/index.html) (version 3).
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd countdown_watchface
|
||||||
|
pebble build
|
||||||
|
```
|
||||||
|
|
||||||
|
Install on a connected watch or emulator:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pebble install --phone <watch-ip>
|
||||||
|
# or
|
||||||
|
pebble install --emulator basalt
|
||||||
|
```
|
||||||
|
|
||||||
|
## Calendar integration (optional)
|
||||||
|
|
||||||
|
The watchface uses Google Calendar API to detect appointments within the next 30 minutes. Without this, the bar simply counts down to the next `:00` or `:30`.
|
||||||
|
|
||||||
|
**Setup:**
|
||||||
|
|
||||||
|
1. Create a Google Cloud project and enable the **Google Calendar API**.
|
||||||
|
2. Generate an **API key** (restrict it to the Calendar API and your IP/app if desired).
|
||||||
|
3. In the Pebble phone app's JS console (or via a config page), run:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
localStorage.setItem('gcal_api_key', 'YOUR_API_KEY_HERE');
|
||||||
|
```
|
||||||
|
|
||||||
|
The calendar ID defaults to `primary`. To use a different calendar, edit `src/pkjs/index.js` and change the `CALENDAR_ID` variable.
|
||||||
|
|
||||||
|
The phone-side JS polls for events every minute and sends the minutes-to-appointment to the watch via AppMessage. If the API call fails or no key is set, the watch falls back to the half-hour countdown automatically.
|
||||||
|
|||||||
@@ -6,9 +6,7 @@
|
|||||||
"pebble-app"
|
"pebble-app"
|
||||||
],
|
],
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {},
|
||||||
"pebble-effect-layer": "^1.2.0"
|
|
||||||
},
|
|
||||||
"pebble": {
|
"pebble": {
|
||||||
"displayName": "countdown_watchface",
|
"displayName": "countdown_watchface",
|
||||||
"uuid": "769d0058-ae41-4e80-b41a-64d58e2ae3e0",
|
"uuid": "769d0058-ae41-4e80-b41a-64d58e2ae3e0",
|
||||||
@@ -25,10 +23,19 @@
|
|||||||
"watchface": true
|
"watchface": true
|
||||||
},
|
},
|
||||||
"messageKeys": [
|
"messageKeys": [
|
||||||
"dummy"
|
"BAR_CHUNK_MINS",
|
||||||
|
"DAY_START_HOUR",
|
||||||
|
"DAY_END_HOUR"
|
||||||
],
|
],
|
||||||
"resources": {
|
"resources": {
|
||||||
"media": []
|
"media": [
|
||||||
|
{
|
||||||
|
"type": "font",
|
||||||
|
"name": "FONT_ROBOTO_BOLD_36",
|
||||||
|
"file": "fonts/Roboto-Bold.ttf",
|
||||||
|
"maxHeight": 36
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
Binary file not shown.
@@ -1,120 +1,240 @@
|
|||||||
#include <pebble.h>
|
#include <pebble.h>
|
||||||
#include <pebble-effect-layer/pebble-effect-layer.h>
|
|
||||||
|
|
||||||
|
#define PERSIST_KEY_DAY_START 1
|
||||||
|
#define PERSIST_KEY_DAY_END 2
|
||||||
|
#define PERSIST_KEY_CHUNK_MINS 3
|
||||||
|
|
||||||
static Window *s_main_window;
|
static Window *s_main_window;
|
||||||
static TextLayer *s_time_layer;
|
static TextLayer *s_time_layer;
|
||||||
static TextLayer *s_date_layer;
|
static TextLayer *s_date_layer;
|
||||||
static EffectLayer *s_effect_layer;
|
static Layer *s_bar_layer;
|
||||||
|
static Layer *s_day_bar_layer;
|
||||||
static Layer *s_battery_layer;
|
static Layer *s_battery_layer;
|
||||||
static Layer *s_countdown_layer;
|
static GFont s_date_font;
|
||||||
static int s_battery_level;
|
|
||||||
|
|
||||||
static void battery_update_proc(Layer *layer, GContext *ctx) {
|
static const char * const MONTHS[] = {
|
||||||
|
"JAN","FEB","MAR","APR","MAY","JUN",
|
||||||
|
"JUL","AUG","SEP","OCT","NOV","DEC"
|
||||||
|
};
|
||||||
|
|
||||||
|
static int s_battery_level = 100;
|
||||||
|
static int s_bar_fill = 30;
|
||||||
|
static int s_day_bar_fill = 16;
|
||||||
|
|
||||||
|
static int s_day_start_hour = 6;
|
||||||
|
static int s_day_end_hour = 22;
|
||||||
|
static int s_bar_chunk_mins = 30;
|
||||||
|
|
||||||
|
static int mins_to_next_chunk(void) {
|
||||||
|
time_t now = time(NULL);
|
||||||
|
struct tm *t = localtime(&now);
|
||||||
|
return s_bar_chunk_mins - (t->tm_min % s_bar_chunk_mins);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void draw_progress_bar(GContext *ctx, GRect bounds, int num_blocks, int fill) {
|
||||||
|
graphics_context_set_fill_color(ctx, GColorBlack);
|
||||||
|
graphics_fill_rect(ctx, bounds, 0, GCornerNone);
|
||||||
|
|
||||||
|
if (num_blocks <= 0) return;
|
||||||
|
|
||||||
|
int gap = 1;
|
||||||
|
int block_w = (bounds.size.w - (num_blocks - 1) * gap) / num_blocks;
|
||||||
|
if (block_w < 1) block_w = 1;
|
||||||
|
int total_w = num_blocks * block_w + (num_blocks - 1) * gap;
|
||||||
|
int start_x = (bounds.size.w - total_w) / 2;
|
||||||
|
|
||||||
|
for (int i = 0; i < num_blocks; i++) {
|
||||||
|
int x = start_x + i * (block_w + gap);
|
||||||
|
if (i >= num_blocks - fill) {
|
||||||
|
graphics_context_set_fill_color(ctx, GColorWhite);
|
||||||
|
graphics_fill_rect(ctx, GRect(x, 0, block_w, bounds.size.h), 0, GCornerNone);
|
||||||
|
} else {
|
||||||
|
graphics_context_set_fill_color(ctx, GColorWhite);
|
||||||
|
graphics_fill_rect(ctx, GRect(x, bounds.size.h - 1, block_w, 1), 0, GCornerNone);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void bar_draw(Layer *layer, GContext *ctx) {
|
||||||
|
draw_progress_bar(ctx, layer_get_bounds(layer), s_bar_chunk_mins, s_bar_fill);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void day_bar_draw(Layer *layer, GContext *ctx) {
|
||||||
|
draw_progress_bar(ctx, layer_get_bounds(layer),
|
||||||
|
s_day_end_hour - s_day_start_hour, s_day_bar_fill);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void update_bar(void) {
|
||||||
|
time_t now = time(NULL);
|
||||||
|
struct tm *t = localtime(&now);
|
||||||
|
|
||||||
|
// Top bar: countdown to next chunk boundary
|
||||||
|
int chunk_mins = mins_to_next_chunk();
|
||||||
|
s_bar_fill = chunk_mins < 0 ? 0 : (chunk_mins > s_bar_chunk_mins ? s_bar_chunk_mins : chunk_mins);
|
||||||
|
|
||||||
|
// Day bar: whole hours remaining (ceiling) within the configured window
|
||||||
|
int total_blocks = s_day_end_hour - s_day_start_hour;
|
||||||
|
int current_mins = t->tm_hour * 60 + t->tm_min;
|
||||||
|
int mins_left = s_day_end_hour * 60 - current_mins;
|
||||||
|
|
||||||
|
if (mins_left <= 0) {
|
||||||
|
s_day_bar_fill = 0;
|
||||||
|
} else if (current_mins < s_day_start_hour * 60) {
|
||||||
|
s_day_bar_fill = total_blocks;
|
||||||
|
} else {
|
||||||
|
s_day_bar_fill = (mins_left + 59) / 60;
|
||||||
|
if (s_day_bar_fill > total_blocks) s_day_bar_fill = total_blocks;
|
||||||
|
}
|
||||||
|
|
||||||
|
layer_mark_dirty(s_bar_layer);
|
||||||
|
layer_mark_dirty(s_day_bar_layer);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void battery_draw(Layer *layer, GContext *ctx) {
|
||||||
GRect bounds = layer_get_bounds(layer);
|
GRect bounds = layer_get_bounds(layer);
|
||||||
|
graphics_context_set_fill_color(ctx, GColorBlack);
|
||||||
|
graphics_fill_rect(ctx, bounds, 0, GCornerNone);
|
||||||
|
|
||||||
int width = (s_battery_level * 114)/ 100;
|
if (s_battery_level >= 20) return;
|
||||||
|
|
||||||
|
graphics_context_set_stroke_color(ctx, GColorWhite);
|
||||||
|
graphics_draw_rect(ctx, GRect(0, 1, 19, 10));
|
||||||
graphics_context_set_fill_color(ctx, GColorWhite);
|
graphics_context_set_fill_color(ctx, GColorWhite);
|
||||||
graphics_fill_rect(ctx,bounds,0,GCornerNone);
|
graphics_fill_rect(ctx, GRect(19, 4, 3, 4), 0, GCornerNone);
|
||||||
|
int fill_w = (s_battery_level * 15) / 100;
|
||||||
|
if (fill_w > 0) {
|
||||||
|
graphics_fill_rect(ctx, GRect(2, 3, fill_w, 6), 0, GCornerNone);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
graphics_context_set_fill_color(ctx,GColorWhite);
|
static void inbox_received(DictionaryIterator *iter, void *context) {
|
||||||
graphics_fill_rect(ctx, GRect(0,0,width,bounds.size.h), 0, GCornerNone);
|
Tuple *t;
|
||||||
|
|
||||||
|
t = dict_find(iter, MESSAGE_KEY_DAY_START_HOUR);
|
||||||
|
if (t) {
|
||||||
|
s_day_start_hour = (int)t->value->int32;
|
||||||
|
persist_write_int(PERSIST_KEY_DAY_START, s_day_start_hour);
|
||||||
|
}
|
||||||
|
|
||||||
|
t = dict_find(iter, MESSAGE_KEY_DAY_END_HOUR);
|
||||||
|
if (t) {
|
||||||
|
s_day_end_hour = (int)t->value->int32;
|
||||||
|
persist_write_int(PERSIST_KEY_DAY_END, s_day_end_hour);
|
||||||
|
}
|
||||||
|
|
||||||
|
t = dict_find(iter, MESSAGE_KEY_BAR_CHUNK_MINS);
|
||||||
|
if (t) {
|
||||||
|
s_bar_chunk_mins = (int)t->value->int32;
|
||||||
|
persist_write_int(PERSIST_KEY_CHUNK_MINS, s_bar_chunk_mins);
|
||||||
|
}
|
||||||
|
|
||||||
|
update_bar();
|
||||||
|
}
|
||||||
|
|
||||||
|
static void update_display(struct tm *tick_time) {
|
||||||
|
static char time_buf[6];
|
||||||
|
strftime(time_buf, sizeof(time_buf), "%H:%M", tick_time);
|
||||||
|
text_layer_set_text(s_time_layer, time_buf);
|
||||||
|
|
||||||
|
static char date_buf[8];
|
||||||
|
snprintf(date_buf, sizeof(date_buf), "%d %s", tick_time->tm_mday, MONTHS[tick_time->tm_mon]);
|
||||||
|
text_layer_set_text(s_date_layer, date_buf);
|
||||||
|
|
||||||
|
update_bar();
|
||||||
|
}
|
||||||
|
|
||||||
|
static void tick_handler(struct tm *tick_time, TimeUnits units_changed) {
|
||||||
|
update_display(tick_time);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void battery_callback(BatteryChargeState state) {
|
||||||
|
s_battery_level = state.charge_percent;
|
||||||
|
layer_mark_dirty(s_battery_layer);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void main_window_load(Window *window) {
|
static void main_window_load(Window *window) {
|
||||||
Layer *window_layer = window_get_root_layer(window);
|
Layer *root = window_get_root_layer(window);
|
||||||
GRect bounds = layer_get_bounds(window_layer);
|
GRect bounds = layer_get_bounds(root);
|
||||||
|
|
||||||
s_effect_layer = effect_layer_create(bounds);
|
// Text area aligned to 30-block bar width (middle setting)
|
||||||
effect_layer_add_effect(s_effect_layer, effect_invert, NULL);
|
int block_w = (bounds.size.w - 29) / 30;
|
||||||
layer_add_child(window_layer,effect_layer_get_layer(s_effect_layer));
|
int bar_w = 30 * block_w + 29;
|
||||||
|
int bar_x = (bounds.size.w - bar_w) / 2;
|
||||||
|
|
||||||
s_time_layer = text_layer_create(
|
#ifdef PBL_PLATFORM_EMERY
|
||||||
GRect(0, PBL_IF_ROUND_ELSE(58,52), bounds.size.w, 50)
|
s_time_layer = text_layer_create(GRect(bar_x, 18, bar_w, 68));
|
||||||
);
|
text_layer_set_font(s_time_layer, fonts_get_system_font(FONT_KEY_LECO_60_BOLD_NUMBERS_AM_PM));
|
||||||
|
#else
|
||||||
// Improve the layout to be more like a watchface
|
s_time_layer = text_layer_create(GRect(bar_x, 44, bar_w, 44));
|
||||||
|
text_layer_set_font(s_time_layer, fonts_get_system_font(FONT_KEY_LECO_42_NUMBERS));
|
||||||
|
#endif
|
||||||
text_layer_set_background_color(s_time_layer, GColorClear);
|
text_layer_set_background_color(s_time_layer, GColorClear);
|
||||||
text_layer_set_text_color(s_time_layer, GColorWhite);
|
text_layer_set_text_color(s_time_layer, GColorWhite);
|
||||||
|
text_layer_set_text_alignment(s_time_layer, GTextAlignmentRight);
|
||||||
text_layer_set_text(s_time_layer, "00:00");
|
text_layer_set_text(s_time_layer, "00:00");
|
||||||
text_layer_set_font(s_time_layer, fonts_get_system_font(FONT_KEY_ROBOTO_BOLD_SUBSET_49));
|
layer_add_child(root, text_layer_get_layer(s_time_layer));
|
||||||
text_layer_set_text_alignment(s_time_layer, GTextAlignmentCenter);
|
|
||||||
|
|
||||||
// Add it as a child layer to the Window's root layer
|
// Chunk countdown bar
|
||||||
layer_add_child(window_layer, text_layer_get_layer(s_time_layer));
|
s_bar_layer = layer_create(GRect(0, 90, bounds.size.w, 10));
|
||||||
|
layer_set_update_proc(s_bar_layer, bar_draw);
|
||||||
|
layer_add_child(root, s_bar_layer);
|
||||||
|
|
||||||
s_date_layer = text_layer_create(
|
// Day progress bar
|
||||||
GRect(0, PBL_IF_ROUND_ELSE(108,102), bounds.size.w, 50)
|
s_day_bar_layer = layer_create(GRect(0, 102, bounds.size.w, 10));
|
||||||
);
|
layer_set_update_proc(s_day_bar_layer, day_bar_draw);
|
||||||
|
layer_add_child(root, s_day_bar_layer);
|
||||||
|
|
||||||
// Improve the layout to be more like a watchface
|
// Date
|
||||||
|
s_date_font = fonts_load_custom_font(resource_get_handle(RESOURCE_ID_FONT_ROBOTO_BOLD_36));
|
||||||
|
s_date_layer = text_layer_create(GRect(bar_x, 114, bar_w, 40));
|
||||||
text_layer_set_background_color(s_date_layer, GColorClear);
|
text_layer_set_background_color(s_date_layer, GColorClear);
|
||||||
text_layer_set_text_color(s_date_layer, GColorWhite);
|
text_layer_set_text_color(s_date_layer, GColorWhite);
|
||||||
text_layer_set_text(s_date_layer, "Wed 1st Jan");
|
text_layer_set_font(s_date_layer, s_date_font);
|
||||||
text_layer_set_font(s_date_layer, fonts_get_system_font(FONT_KEY_ROBOTO_CONDENSED_21));
|
text_layer_set_text_alignment(s_date_layer, GTextAlignmentRight);
|
||||||
text_layer_set_text_alignment(s_date_layer, GTextAlignmentCenter);
|
text_layer_set_text(s_date_layer, "1 JAN");
|
||||||
|
layer_add_child(root, text_layer_get_layer(s_date_layer));
|
||||||
|
|
||||||
// Add it as a child layer to the Window's root layer
|
// Battery icon — bottom right, only visible below 20%
|
||||||
layer_add_child(window_layer, text_layer_get_layer(s_date_layer));
|
s_battery_layer = layer_create(GRect(bounds.size.w - 26, bounds.size.h - 14, 23, 12));
|
||||||
|
layer_set_update_proc(s_battery_layer, battery_draw);
|
||||||
// Battery
|
layer_add_child(root, s_battery_layer);
|
||||||
s_battery_layer = layer_create(GRect(14,54,115,2));
|
|
||||||
layer_set_update_proc(s_battery_layer, battery_update_proc);
|
|
||||||
|
|
||||||
layer_add_child(window_layer, s_battery_layer);
|
|
||||||
layer_mark_dirty(s_battery_layer);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static void main_window_unload(Window *window) {
|
static void main_window_unload(Window *window) {
|
||||||
text_layer_destroy(s_time_layer);
|
text_layer_destroy(s_time_layer);
|
||||||
text_layer_destroy(s_date_layer);
|
text_layer_destroy(s_date_layer);
|
||||||
effect_layer_destroy(s_effect_layer);
|
layer_destroy(s_bar_layer);
|
||||||
|
layer_destroy(s_day_bar_layer);
|
||||||
layer_destroy(s_battery_layer);
|
layer_destroy(s_battery_layer);
|
||||||
|
fonts_unload_custom_font(s_date_font);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void update_time() {
|
static void init(void) {
|
||||||
time_t temp = time(NULL);
|
s_day_start_hour = persist_exists(PERSIST_KEY_DAY_START) ? persist_read_int(PERSIST_KEY_DAY_START) : 6;
|
||||||
struct tm *tick_time = localtime(&temp);
|
s_day_end_hour = persist_exists(PERSIST_KEY_DAY_END) ? persist_read_int(PERSIST_KEY_DAY_END) : 22;
|
||||||
|
s_bar_chunk_mins = persist_exists(PERSIST_KEY_CHUNK_MINS) ? persist_read_int(PERSIST_KEY_CHUNK_MINS) : 30;
|
||||||
|
|
||||||
static char s_buffer[8];
|
|
||||||
strftime(s_buffer, sizeof(s_buffer), clock_is_24h_style() ? "%H:%M" : "%I:%M", tick_time);
|
|
||||||
|
|
||||||
text_layer_set_text(s_time_layer, s_buffer);
|
|
||||||
|
|
||||||
static char d_buffer[12];
|
|
||||||
strftime(d_buffer, sizeof(d_buffer), "%a %d %b", tick_time);
|
|
||||||
|
|
||||||
text_layer_set_text(s_date_layer, d_buffer);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
static void tick_handler(struct tm *tick_time, TimeUnits units_changed) {
|
|
||||||
update_time();
|
|
||||||
}
|
|
||||||
|
|
||||||
static void battery_callback(BatteryChargeState state) {
|
|
||||||
s_battery_level = state.charge_percent;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
static void init() {
|
|
||||||
s_main_window = window_create();
|
s_main_window = window_create();
|
||||||
|
window_set_background_color(s_main_window, GColorBlack);
|
||||||
|
|
||||||
window_set_window_handlers(s_main_window, (WindowHandlers) {
|
window_set_window_handlers(s_main_window, (WindowHandlers) {
|
||||||
.load = main_window_load,
|
.load = main_window_load,
|
||||||
.unload = main_window_unload
|
.unload = main_window_unload,
|
||||||
});
|
});
|
||||||
|
|
||||||
window_stack_push(s_main_window, true);
|
window_stack_push(s_main_window, true);
|
||||||
|
|
||||||
tick_timer_service_subscribe(MINUTE_UNIT, tick_handler);
|
tick_timer_service_subscribe(MINUTE_UNIT, tick_handler);
|
||||||
battery_state_service_subscribe(battery_callback);
|
battery_state_service_subscribe(battery_callback);
|
||||||
update_time();
|
|
||||||
battery_callback(battery_state_service_peek());
|
battery_callback(battery_state_service_peek());
|
||||||
|
|
||||||
|
app_message_register_inbox_received(inbox_received);
|
||||||
|
app_message_open(128, 0);
|
||||||
|
|
||||||
|
time_t now = time(NULL);
|
||||||
|
update_display(localtime(&now));
|
||||||
}
|
}
|
||||||
|
|
||||||
static void deinit() {
|
static void deinit(void) {
|
||||||
window_destroy(s_main_window);
|
window_destroy(s_main_window);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,94 @@
|
|||||||
|
// --- Configuration ---
|
||||||
|
|
||||||
|
function getStoredConfig() {
|
||||||
|
return {
|
||||||
|
chunk: parseInt(localStorage.getItem('chunk') || '30', 10),
|
||||||
|
start: parseInt(localStorage.getItem('start') || '6', 10),
|
||||||
|
end: parseInt(localStorage.getItem('end') || '22', 10)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendConfig(cfg) {
|
||||||
|
Pebble.sendAppMessage(
|
||||||
|
{
|
||||||
|
'BAR_CHUNK_MINS': cfg.chunk,
|
||||||
|
'DAY_START_HOUR': cfg.start,
|
||||||
|
'DAY_END_HOUR': cfg.end
|
||||||
|
},
|
||||||
|
function() {},
|
||||||
|
function(e) { console.log('Config send failed: ' + JSON.stringify(e)); }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildConfigPage(chunk, start, end) {
|
||||||
|
return '<!DOCTYPE html>' +
|
||||||
|
'<html><head>' +
|
||||||
|
'<meta charset="utf-8">' +
|
||||||
|
'<meta name="viewport" content="width=device-width,initial-scale=1">' +
|
||||||
|
'<title>Watchface Settings</title>' +
|
||||||
|
'<style>' +
|
||||||
|
'body{background:#111;color:#eee;font-family:-apple-system,sans-serif;padding:20px;margin:0}' +
|
||||||
|
'h2{margin:0 0 24px;font-size:20px}' +
|
||||||
|
'label{display:block;margin-top:20px;font-size:13px;color:#999;text-transform:uppercase;letter-spacing:.05em}' +
|
||||||
|
'select,input{display:block;width:100%;box-sizing:border-box;background:#222;color:#eee;' +
|
||||||
|
'border:1px solid #444;border-radius:6px;padding:10px;margin-top:6px;font-size:16px;-webkit-appearance:none}' +
|
||||||
|
'.btns{display:flex;gap:10px;margin-top:32px}' +
|
||||||
|
'button{flex:1;padding:14px;border:none;border-radius:6px;font-size:16px;font-weight:600;cursor:pointer}' +
|
||||||
|
'#sv{background:#0A84FF;color:#fff}' +
|
||||||
|
'#cn{background:#2a2a2a;color:#eee}' +
|
||||||
|
'</style></head><body>' +
|
||||||
|
'<h2>Watchface Settings</h2>' +
|
||||||
|
'<label>Top bar interval' +
|
||||||
|
'<select id="q">' +
|
||||||
|
'<option value="15">15 min — quarter hour</option>' +
|
||||||
|
'<option value="30">30 min — half hour</option>' +
|
||||||
|
'<option value="60">60 min — full hour</option>' +
|
||||||
|
'</select></label>' +
|
||||||
|
'<label>Day start (hour, 0–23)<input type="number" id="a" min="0" max="23"></label>' +
|
||||||
|
'<label>Day end (hour, 0–23)<input type="number" id="b" min="0" max="23"></label>' +
|
||||||
|
'<div class="btns"><button id="sv">Save</button><button id="cn">Cancel</button></div>' +
|
||||||
|
'<script>' +
|
||||||
|
'document.getElementById("q").value=' + chunk + ';' +
|
||||||
|
'document.getElementById("a").value=' + start + ';' +
|
||||||
|
'document.getElementById("b").value=' + end + ';' +
|
||||||
|
'document.getElementById("sv").addEventListener("click",function(){' +
|
||||||
|
'var d={chunk:+document.getElementById("q").value,' +
|
||||||
|
'start:+document.getElementById("a").value,' +
|
||||||
|
'end:+document.getElementById("b").value};' +
|
||||||
|
'location.href="pebblekit-js://"+encodeURIComponent(JSON.stringify(d));' +
|
||||||
|
'});' +
|
||||||
|
'document.getElementById("cn").addEventListener("click",function(){' +
|
||||||
|
'location.href="pebblekit-js://cancel";' +
|
||||||
|
'});' +
|
||||||
|
'<\/script></body></html>';
|
||||||
|
}
|
||||||
|
|
||||||
|
Pebble.addEventListener('ready', function() {
|
||||||
|
sendConfig(getStoredConfig());
|
||||||
|
});
|
||||||
|
|
||||||
|
Pebble.addEventListener('showConfiguration', function() {
|
||||||
|
var cfg = getStoredConfig();
|
||||||
|
var html = buildConfigPage(cfg.chunk, cfg.start, cfg.end);
|
||||||
|
Pebble.openURL('data:text/html,' + encodeURIComponent(html));
|
||||||
|
});
|
||||||
|
|
||||||
|
Pebble.addEventListener('webviewclosed', function(e) {
|
||||||
|
if (!e.response || e.response === 'cancel') return;
|
||||||
|
try {
|
||||||
|
var cfg = JSON.parse(decodeURIComponent(e.response));
|
||||||
|
var chunk = parseInt(cfg.chunk, 10);
|
||||||
|
var start = parseInt(cfg.start, 10);
|
||||||
|
var end = parseInt(cfg.end, 10);
|
||||||
|
if (chunk <= 0 || chunk > 60) return;
|
||||||
|
if (start < 0 || start > 22) return;
|
||||||
|
if (end < 1 || end > 23) return;
|
||||||
|
if (start >= end) return;
|
||||||
|
localStorage.setItem('chunk', chunk);
|
||||||
|
localStorage.setItem('start', start);
|
||||||
|
localStorage.setItem('end', end);
|
||||||
|
sendConfig({ chunk: chunk, start: start, end: end });
|
||||||
|
} catch (err) {
|
||||||
|
console.log('Failed to parse config: ' + err);
|
||||||
|
}
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user