428 lines
16 KiB
C
428 lines
16 KiB
C
#include "../../neat_bar/src/c/neat_bar.h"
|
|
#include <pebble.h>
|
|
#include <stdio.h>
|
|
#include <string.h>
|
|
|
|
// #define DEBUG_MODE 1
|
|
|
|
// declare settings-derived statics
|
|
static uint8_t s_is_jellyfin;
|
|
static GColor s_color_primary = GColorWhite;
|
|
static GColor s_color_secondary = GColorWhite;
|
|
|
|
// declare bitmaps
|
|
static GBitmap *s_logo_icon;
|
|
static GBitmap *s_pin_icon;
|
|
static GBitmap *s_next_icon;
|
|
static GBitmap *s_prev_icon;
|
|
|
|
// declare window/layer statics
|
|
static Window *s_main_window;
|
|
static TextLayer *s_config_app_layer;
|
|
static TextLayer *s_sleep_time_layer;
|
|
static TextLayer *s_last_watched_layer;
|
|
static NeatBarLayer *s_neat_bar_layer;
|
|
static Layer *s_sleep_bar_layer;
|
|
static BitmapLayer *s_logo_layer;
|
|
static const uint8_t s_logo_width = 25;
|
|
#define MARGIN 8
|
|
#if PBL_ROUND
|
|
#define NEAT_BAR_WIDTH 30
|
|
#if PBL_DISPLAY_WIDTH >= 260
|
|
static const uint8_t s_sleep_bar_drop = 54;
|
|
#else
|
|
static const uint8_t s_sleep_bar_drop = 50;
|
|
#endif
|
|
#else
|
|
#define NEAT_BAR_WIDTH 25
|
|
static const uint8_t s_sleep_bar_drop = 37;
|
|
#endif
|
|
#if PBL_DISPLAY_WIDTH >= 200
|
|
#define LW_FONT_SIZE 28
|
|
#else
|
|
#define LW_FONT_SIZE 24
|
|
#endif
|
|
|
|
// declare animation statics
|
|
static GRect s_sleep_bar_start = GRect(0, 0, PBL_DISPLAY_WIDTH, PBL_DISPLAY_HEIGHT);
|
|
static GRect s_logo_start = GRect((PBL_DISPLAY_WIDTH / 2) - 13, (PBL_DISPLAY_HEIGHT / 2) - 17, s_logo_width, s_logo_width);
|
|
|
|
// declare time tracking statics
|
|
static time_t s_sleep_timestamp;
|
|
static bool s_sleep_session_found;
|
|
static bool s_sleep_data_accessible;
|
|
|
|
static const uint8_t s_wasted_top_text_pixels = 10; // 10 blank pixels above each line of text
|
|
static const uint16_t s_available_height = PBL_DISPLAY_HEIGHT - s_sleep_bar_drop - MARGIN / 2 + s_wasted_top_text_pixels;
|
|
// static const uint8_t s_max_rows = s_available_height / LW_FONT_SIZE;
|
|
static const uint16_t s_screen_center_y = PBL_DISPLAY_HEIGHT / 2;
|
|
static const uint16_t s_base_shift = s_screen_center_y - (LW_FONT_SIZE / 2) - (s_wasted_top_text_pixels / 2); // base - perfect if there is one row
|
|
static void write_last_watched(const char *text) {
|
|
text_layer_set_text(s_last_watched_layer, text);
|
|
|
|
GSize text_size = text_layer_get_content_size(s_last_watched_layer);
|
|
uint8_t current_rows = text_size.h / LW_FONT_SIZE;
|
|
|
|
// for each additional row beyond 1, shift up by half a row's height
|
|
uint16_t y = s_base_shift - ((current_rows - 1) * (LW_FONT_SIZE / 2));
|
|
|
|
// if the top extends above the sleep bar, force it below it;
|
|
// account for blank pixels above text to avoid forcing position when not visually necessary
|
|
if (y + s_wasted_top_text_pixels < s_sleep_bar_drop + MARGIN / 2) {
|
|
y = s_sleep_bar_drop + MARGIN / 2 - s_wasted_top_text_pixels;
|
|
#if PBL_DISPLAY_WIDTH >= 260 && PBL_ROUND
|
|
y += 3;
|
|
#endif
|
|
}
|
|
|
|
GRect last_watched_frame = layer_get_frame(text_layer_get_layer(s_last_watched_layer));
|
|
last_watched_frame.origin.y = y;
|
|
layer_set_frame(text_layer_get_layer(s_last_watched_layer), last_watched_frame);
|
|
// APP_LOG(APP_LOG_LEVEL_DEBUG, "total height=%d, text height=%d rows=%d/%d", s_available_height, text_size.h, current_rows, s_max_rows);
|
|
}
|
|
|
|
static bool cb_update_sleep_time(HealthActivity activity, time_t start_time, time_t end_time, void *context) {
|
|
s_sleep_timestamp = start_time;
|
|
return false;
|
|
}
|
|
|
|
static void update_sleep_time(time_t start, time_t end) {
|
|
health_service_activities_iterate(HealthActivitySleep, start, end,
|
|
HealthIterationDirectionPast,
|
|
cb_update_sleep_time, NULL);
|
|
}
|
|
|
|
static void sleep_bar_update_proc(Layer *layer, GContext *ctx) {
|
|
graphics_context_set_fill_color(ctx, s_color_primary);
|
|
graphics_fill_rect(ctx, layer_get_bounds(layer), PBL_IF_ROUND_ELSE(15, 3), PBL_IF_ROUND_ELSE(GCornersBottom, GCornersAll));
|
|
}
|
|
|
|
static void send_pin_to_pkjs() {
|
|
DictionaryIterator *out;
|
|
AppMessageResult result = app_message_outbox_begin(&out);
|
|
if (result != APP_MSG_OK) {
|
|
write_last_watched("outbox_begin failure");
|
|
return;
|
|
}
|
|
if (s_sleep_session_found) {
|
|
dict_write_uint32(out, MESSAGE_KEY_PKJS_PIN_TIMESTAMP, s_sleep_timestamp);
|
|
} else {
|
|
dict_write_uint32(out, MESSAGE_KEY_PKJS_PIN_TIMESTAMP, time(NULL));
|
|
}
|
|
dict_write_cstring(out, MESSAGE_KEY_PKJS_PIN_TITLE, text_layer_get_text(s_last_watched_layer));
|
|
result = app_message_outbox_send();
|
|
if (result != APP_MSG_OK) {
|
|
write_last_watched("outbox_send failure");
|
|
return;
|
|
}
|
|
}
|
|
|
|
static void select_click_handler(ClickRecognizerRef recognizer, void *context) {
|
|
send_pin_to_pkjs();
|
|
static const uint32_t segments[] = {60};
|
|
VibePattern pattern = {
|
|
.durations = segments,
|
|
.num_segments = ARRAY_LENGTH(segments),
|
|
};
|
|
vibes_enqueue_custom_pattern(pattern);
|
|
}
|
|
|
|
void up_click_handler(ClickRecognizerRef recognizer, void *context) {
|
|
}
|
|
|
|
void down_click_handler(ClickRecognizerRef recognizer, void *context) {
|
|
}
|
|
|
|
void click_config_provider(void *context) {
|
|
window_single_click_subscribe(BUTTON_ID_SELECT, select_click_handler);
|
|
window_single_click_subscribe(BUTTON_ID_UP, up_click_handler);
|
|
window_single_click_subscribe(BUTTON_ID_DOWN, down_click_handler);
|
|
}
|
|
|
|
static void main_window_load(Window *window) {
|
|
Layer *window_layer = window_get_root_layer(window);
|
|
if (s_is_jellyfin == 0) {
|
|
window_set_background_color(window, GColorWhite);
|
|
#if PBL_DISPLAY_WIDTH >= 200
|
|
s_config_app_layer = text_layer_create(GRect(MARGIN, (PBL_DISPLAY_HEIGHT / 2) - PBL_IF_ROUND_ELSE(33, 49), PBL_DISPLAY_WIDTH - 8, PBL_DISPLAY_HEIGHT));
|
|
text_layer_set_font(s_config_app_layer, fonts_get_system_font(FONT_KEY_GOTHIC_28_BOLD));
|
|
#else
|
|
s_config_app_layer = text_layer_create(GRect(MARGIN, (PBL_DISPLAY_HEIGHT / 2) - PBL_IF_ROUND_ELSE(43, 32), PBL_DISPLAY_WIDTH - 8, PBL_DISPLAY_HEIGHT));
|
|
text_layer_set_font(s_config_app_layer, fonts_get_system_font(PBL_IF_ROUND_ELSE(FONT_KEY_GOTHIC_24_BOLD, FONT_KEY_GOTHIC_18_BOLD)));
|
|
#endif
|
|
text_layer_set_background_color(s_config_app_layer, GColorClear);
|
|
text_layer_set_text(s_config_app_layer, "Please configure the app's settings from your phone");
|
|
text_layer_set_text_alignment(s_config_app_layer, GTextAlignmentCenter);
|
|
layer_add_child(window_layer, text_layer_get_layer(s_config_app_layer));
|
|
return;
|
|
}
|
|
|
|
// neat bar
|
|
s_neat_bar_layer = neat_bar_layer_create();
|
|
neat_bar_layer_set_icon(s_neat_bar_layer, BUTTON_ID_UP, s_prev_icon);
|
|
neat_bar_layer_set_icon(s_neat_bar_layer, BUTTON_ID_SELECT, s_pin_icon);
|
|
neat_bar_layer_set_icon(s_neat_bar_layer, BUTTON_ID_DOWN, s_next_icon);
|
|
neat_bar_layer_set_background_color(s_neat_bar_layer, s_color_secondary);
|
|
neat_bar_layer_set_width(s_neat_bar_layer, NEAT_BAR_WIDTH);
|
|
#if PBL_DISPLAY_HEIGHT >= 260 && PBL_ROUND
|
|
neat_bar_layer_set_corner_radius(s_neat_bar_layer, 15);
|
|
neat_bar_layer_set_height(s_neat_bar_layer, 140);
|
|
neat_bar_layer_set_y_offset(s_neat_bar_layer, 60);
|
|
#elif PBL_DISPLAY_HEIGHT == 228 && PBL_RECT
|
|
neat_bar_layer_set_corner_radius(s_neat_bar_layer, 3);
|
|
neat_bar_layer_set_floating(s_neat_bar_layer, 4);
|
|
neat_bar_layer_set_height(s_neat_bar_layer, 183);
|
|
neat_bar_layer_set_y_offset(s_neat_bar_layer, 41);
|
|
#elif PBL_DISPLAY_HEIGHT == 180 && PBL_ROUND
|
|
neat_bar_layer_set_corner_radius(s_neat_bar_layer, 15);
|
|
neat_bar_layer_set_height(s_neat_bar_layer, 71);
|
|
neat_bar_layer_set_y_offset(s_neat_bar_layer, 54);
|
|
#else // 144x168
|
|
neat_bar_layer_set_corner_radius(s_neat_bar_layer, 3);
|
|
neat_bar_layer_set_floating(s_neat_bar_layer, 4);
|
|
neat_bar_layer_set_height(s_neat_bar_layer, 123);
|
|
neat_bar_layer_set_y_offset(s_neat_bar_layer, 41);
|
|
neat_bar_layer_set_spread(s_neat_bar_layer, -10);
|
|
#endif
|
|
neat_bar_layer_set_click_config_provider(s_neat_bar_layer, click_config_provider);
|
|
neat_bar_layer_add_to_window(s_neat_bar_layer, window);
|
|
|
|
// sleep bar and icon
|
|
s_sleep_bar_layer = layer_create(s_sleep_bar_start);
|
|
s_logo_layer = bitmap_layer_create(s_logo_start);
|
|
#if PBL_COLOR
|
|
bitmap_layer_set_compositing_mode(s_logo_layer, GCompOpSet);
|
|
#endif
|
|
bitmap_layer_set_bitmap(s_logo_layer, s_logo_icon);
|
|
|
|
s_last_watched_layer = text_layer_create(GRect(MARGIN, 0, PBL_DISPLAY_WIDTH - (2 * MARGIN) - NEAT_BAR_WIDTH - PBL_IF_ROUND_ELSE(0, MARGIN / 2), s_available_height)); // y position updated when layer is written to
|
|
#if PBL_DISPLAY_WIDTH >= 200
|
|
s_sleep_time_layer = text_layer_create(GRect(0, PBL_IF_ROUND_ELSE(22, 5), PBL_DISPLAY_WIDTH, 30));
|
|
text_layer_set_font(s_sleep_time_layer, fonts_get_system_font(FONT_KEY_GOTHIC_24_BOLD));
|
|
text_layer_set_font(s_last_watched_layer, fonts_get_system_font(FONT_KEY_GOTHIC_28));
|
|
#else
|
|
s_sleep_time_layer = text_layer_create(GRect(0, PBL_IF_ROUND_ELSE(25, 5), PBL_DISPLAY_WIDTH, 30));
|
|
text_layer_set_font(s_sleep_time_layer, fonts_get_system_font(PBL_IF_ROUND_ELSE(FONT_KEY_GOTHIC_18_BOLD, FONT_KEY_GOTHIC_24_BOLD)));
|
|
text_layer_set_font(s_last_watched_layer, fonts_get_system_font(FONT_KEY_GOTHIC_24));
|
|
#endif
|
|
text_layer_set_text_alignment(s_sleep_time_layer, GTextAlignmentCenter);
|
|
text_layer_set_text_alignment(s_last_watched_layer, GTextAlignmentCenter);
|
|
|
|
// sleep time
|
|
text_layer_set_background_color(s_sleep_time_layer, GColorClear);
|
|
#if PBL_COLOR
|
|
text_layer_set_text_color(s_sleep_time_layer, GColorWhite);
|
|
#else
|
|
text_layer_set_text_color(s_sleep_time_layer, GColorBlack);
|
|
#endif
|
|
if (s_sleep_timestamp != 0) {
|
|
static char buffer[8];
|
|
struct tm *timeinfo = localtime(&s_sleep_timestamp);
|
|
strftime(buffer, sizeof(buffer), clock_is_24h_style() ? "%H:%M" : "%I:%M", timeinfo);
|
|
text_layer_set_text(s_sleep_time_layer, buffer);
|
|
} else {
|
|
#if PBL_RECT
|
|
#if PBL_DISPLAY_WIDTH >= 200
|
|
text_layer_set_text(s_sleep_time_layer, " No sleep time found");
|
|
#else
|
|
text_layer_set_text(s_sleep_time_layer, " No sleep time");
|
|
#endif
|
|
#else
|
|
text_layer_set_text(s_sleep_time_layer, "No sleep time found");
|
|
#endif
|
|
}
|
|
layer_set_hidden(text_layer_get_layer(s_sleep_time_layer), true);
|
|
|
|
// misc. properties
|
|
layer_set_update_proc(s_sleep_bar_layer, sleep_bar_update_proc);
|
|
text_layer_set_background_color(s_last_watched_layer, GColorClear);
|
|
text_layer_set_text_color(s_last_watched_layer, GColorWhite);
|
|
|
|
if (s_sleep_data_accessible) {
|
|
write_last_watched("N/A"); // default - will be updated before display
|
|
} else {
|
|
write_last_watched("Sleep data inaccessible!\nEnsure Pebble Health is enabled.");
|
|
}
|
|
|
|
// add layers as children to windows
|
|
layer_add_child(window_layer, text_layer_get_layer(s_last_watched_layer));
|
|
layer_add_child(window_layer, s_sleep_bar_layer);
|
|
layer_add_child(window_layer, bitmap_layer_get_layer(s_logo_layer));
|
|
layer_add_child(window_layer, text_layer_get_layer(s_sleep_time_layer));
|
|
}
|
|
|
|
static void main_window_unload(Window *window) {
|
|
if (s_is_jellyfin == 0) {
|
|
text_layer_destroy(s_config_app_layer);
|
|
} else {
|
|
text_layer_destroy(s_last_watched_layer);
|
|
text_layer_destroy(s_sleep_time_layer);
|
|
bitmap_layer_destroy(s_logo_layer);
|
|
layer_destroy(s_sleep_bar_layer);
|
|
neat_bar_layer_destroy(s_neat_bar_layer);
|
|
}
|
|
}
|
|
|
|
static void send_sleep_time_to_pkjs() {
|
|
if (s_is_jellyfin == 0) {
|
|
return;
|
|
}
|
|
DictionaryIterator *out;
|
|
AppMessageResult result = app_message_outbox_begin(&out);
|
|
if (result != APP_MSG_OK) {
|
|
write_last_watched("outbox_begin failure");
|
|
return;
|
|
}
|
|
if (s_sleep_session_found) {
|
|
dict_write_uint32(out, MESSAGE_KEY_PKJS_SLEEP_TIMESTAMP, s_sleep_timestamp);
|
|
} else {
|
|
// send current UNIX timestamp to fetch the last thing played
|
|
dict_write_uint32(out, MESSAGE_KEY_PKJS_SLEEP_TIMESTAMP, time(NULL));
|
|
}
|
|
result = app_message_outbox_send();
|
|
if (result != APP_MSG_OK) {
|
|
write_last_watched("outbox_send failure");
|
|
return;
|
|
}
|
|
}
|
|
|
|
static void soft_reload(bool first_load) {
|
|
if (!first_load) {
|
|
window_stack_remove(s_main_window, false);
|
|
window_destroy(s_main_window);
|
|
}
|
|
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, false);
|
|
send_sleep_time_to_pkjs();
|
|
}
|
|
|
|
static void load_complete_animation_handler(Animation *animation, bool finished, void *context) {
|
|
layer_set_hidden(text_layer_get_layer(s_sleep_time_layer), false);
|
|
animation_destroy(animation);
|
|
}
|
|
|
|
static void inbox_received_handler(DictionaryIterator *iter, void *context) {
|
|
Tuple *ready_tuple = dict_find(iter, MESSAGE_KEY_PKJS_READY);
|
|
if (ready_tuple) {
|
|
send_sleep_time_to_pkjs();
|
|
return;
|
|
}
|
|
|
|
Tuple *clay_tuple = dict_find(iter, MESSAGE_KEY_CLAY_API_IS_JELLYFIN);
|
|
if (clay_tuple) {
|
|
bool is_jellyfin = clay_tuple->value->int32 == 1;
|
|
if (is_jellyfin) {
|
|
s_is_jellyfin = 1;
|
|
persist_write_int(0, 1);
|
|
s_color_primary = GColorVividViolet;
|
|
s_color_secondary = GColorPictonBlue;
|
|
} else {
|
|
s_is_jellyfin = 2;
|
|
persist_write_int(0, 2);
|
|
s_color_primary = GColorChromeYellow;
|
|
s_color_secondary = GColorLightGray;
|
|
}
|
|
layer_mark_dirty(s_sleep_bar_layer);
|
|
soft_reload(false); // app needs "restarted" when settings are changed
|
|
return;
|
|
}
|
|
|
|
Tuple *watched_tuple = dict_find(iter, MESSAGE_KEY_PKJS_LAST_WATCHED);
|
|
if (watched_tuple) {
|
|
write_last_watched(watched_tuple->value->cstring);
|
|
#if PBL_ROUND
|
|
#if PBL_DISPLAY_WIDTH >= 260
|
|
#define r_x 30
|
|
#define r_w 200
|
|
#else
|
|
#define r_x 15
|
|
#define r_w 150
|
|
#endif
|
|
PropertyAnimation *sleep_bar_prop = property_animation_create_layer_frame(s_sleep_bar_layer, &s_sleep_bar_start, &GRect(r_x, 0, r_w, s_sleep_bar_drop));
|
|
#else
|
|
PropertyAnimation *sleep_bar_prop = property_animation_create_layer_frame(s_sleep_bar_layer, &s_sleep_bar_start, &GRect(4, 4, PBL_DISPLAY_WIDTH - 8, s_sleep_bar_drop - 4));
|
|
#endif
|
|
Animation *sleep_bar_anim = property_animation_get_animation(sleep_bar_prop);
|
|
animation_set_duration(sleep_bar_anim, 512);
|
|
PropertyAnimation *logo_prop = property_animation_create_layer_frame(bitmap_layer_get_layer(s_logo_layer), &s_logo_start, &GRect(PBL_IF_ROUND_ELSE((PBL_DISPLAY_WIDTH / 2) - 13, MARGIN), PBL_IF_ROUND_ELSE(3, 6), s_logo_width, s_logo_width));
|
|
Animation *logo_anim = property_animation_get_animation(logo_prop);
|
|
animation_set_duration(logo_anim, 512);
|
|
Animation *load_spawn_anim = animation_spawn_create(sleep_bar_anim, logo_anim, NULL);
|
|
animation_set_handlers(load_spawn_anim, (AnimationHandlers){.stopped = load_complete_animation_handler}, NULL);
|
|
animation_schedule(load_spawn_anim);
|
|
return;
|
|
}
|
|
}
|
|
|
|
static void init() {
|
|
// set colors if is_jellyfin is locally set;
|
|
// if it has not yet been set, it will be once
|
|
// the user saves their settings
|
|
s_is_jellyfin = persist_read_int(0);
|
|
#if DEBUG_MODE
|
|
s_is_jellyfin = DEBUG_MODE; // allow forcing configured state for emu testing
|
|
#endif
|
|
#if PBL_COLOR
|
|
switch (s_is_jellyfin) {
|
|
case 1:
|
|
s_color_primary = GColorVividViolet;
|
|
s_color_secondary = GColorPictonBlue;
|
|
break;
|
|
case 2:
|
|
s_color_primary = GColorChromeYellow;
|
|
s_color_secondary = GColorLightGray;
|
|
break;
|
|
}
|
|
#else
|
|
s_color_primary = GColorWhite;
|
|
s_color_secondary = GColorWhite;
|
|
#endif
|
|
s_logo_icon = gbitmap_create_with_resource(RESOURCE_ID_LOGO_25);
|
|
s_pin_icon = gbitmap_create_with_resource(RESOURCE_ID_PIN);
|
|
s_next_icon = gbitmap_create_with_resource(RESOURCE_ID_NEXT);
|
|
s_prev_icon = gbitmap_create_with_resource(RESOURCE_ID_PREV);
|
|
|
|
// initialize pkjs
|
|
app_message_register_inbox_received(inbox_received_handler);
|
|
app_message_open(1536, 64);
|
|
|
|
// get sleep time (UNIX timestamp)
|
|
time_t end = time(NULL);
|
|
time_t start = end - (SECONDS_PER_DAY * 2);
|
|
if (health_service_any_activity_accessible(HealthActivitySleep, start, end) == HealthServiceAccessibilityMaskAvailable) {
|
|
s_sleep_data_accessible = true;
|
|
update_sleep_time(start, end);
|
|
if (s_sleep_timestamp > 0) {
|
|
s_sleep_session_found = true;
|
|
}
|
|
} else {
|
|
s_sleep_data_accessible = false;
|
|
}
|
|
|
|
soft_reload(true);
|
|
}
|
|
|
|
static void glance_reload_callback(AppGlanceReloadSession *session, size_t limit, void *context) {
|
|
AppGlanceSlice slice = {
|
|
.expiration_time = APP_GLANCE_SLICE_NO_EXPIRATION,
|
|
.layout.icon = APP_GLANCE_SLICE_DEFAULT_ICON,
|
|
.layout.subtitle_template_string = text_layer_get_text(s_last_watched_layer),
|
|
};
|
|
app_glance_add_slice(session, slice);
|
|
}
|
|
|
|
static void deinit() {
|
|
app_glance_reload(glance_reload_callback, NULL);
|
|
window_destroy(s_main_window);
|
|
}
|
|
|
|
int main(void) {
|
|
init();
|
|
app_event_loop();
|
|
deinit();
|
|
}
|