#include "../../neat_bar/src/c/neat_bar.h" #include #include #include #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; // defines #define MARGIN 8 //// #if PBL_ROUND // #define NEAT_BAR_WIDTH 30 #define SLEEP_BAR_HEIGHT (PBL_DISPLAY_HEIGHT >= 260 ? 54 : 50) // #else // #define NEAT_BAR_WIDTH 25 #define SLEEP_BAR_HEIGHT 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 - SLEEP_BAR_HEIGHT - 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 < SLEEP_BAR_HEIGHT + MARGIN / 2) { y = SLEEP_BAR_HEIGHT + 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_antialiased(ctx, false); 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_background_color(s_neat_bar_layer, s_color_secondary); 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_bar_width(s_neat_bar_layer, NEAT_BAR_WIDTH); #if PBL_ROUND neat_bar_layer_set_bar_style(s_neat_bar_layer, false); neat_bar_layer_set_rect_bar_corner_radius(s_neat_bar_layer, 15); neat_bar_layer_set_bar_y_offset(s_neat_bar_layer, PBL_DISPLAY_HEIGHT >= 260 ? 60 : 54); neat_bar_layer_set_bar_height(s_neat_bar_layer, PBL_DISPLAY_HEIGHT >= 260 ? 140 : 72); #else neat_bar_layer_set_icons_x_offset(s_neat_bar_layer, 0); neat_bar_layer_set_rect_bar_corner_radius(s_neat_bar_layer, 3); neat_bar_layer_set_rect_bar_floating(s_neat_bar_layer, 4); neat_bar_layer_set_bar_y_offset(s_neat_bar_layer, 41); neat_bar_layer_set_bar_height(s_neat_bar_layer, PBL_DISPLAY_HEIGHT >= 228 ? 183 : 123); #if PBL_DISPLAY_HEIGHT == 168 neat_bar_layer_set_icons_spread(s_neat_bar_layer, 36); #endif #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, SLEEP_BAR_HEIGHT)); #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, SLEEP_BAR_HEIGHT - 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(); }