Files
1-More-Episode/src/c/main.c

391 lines
15 KiB
C

#include "gdraw_anim.h"
#include <pebble.h>
#include <stdio.h>
#include <string.h>
#define DEBUG_MODE 2
// declare settings-derived statics
static uint8_t s_is_jellyfin;
static GColor s_color_primary = GColorWhite;
static GColor s_color_secondary = GColorWhite;
// 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 Layer *s_button_bar_layer;
static Layer *s_sleep_bar_layer;
static Layer *s_sleep_icon_layer;
static Layer *s_pin_icon_layer;
static GDrawCommandImage *s_sleep_icon;
static GDrawCommandImage *s_pin_icon;
static const uint8_t s_icon_width = 25;
#if PBL_ROUND
#if PBL_DISPLAY_WIDTH >= 200
static const uint8_t s_sleep_bar_drop = 53;
#else
static const uint8_t s_sleep_bar_drop = 49;
#endif
#else
static const uint8_t s_sleep_bar_drop = 31;
#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_sleep_icon_start = GRect((PBL_DISPLAY_WIDTH / 2) - 13, (PBL_DISPLAY_HEIGHT / 2) - 12, s_icon_width, s_icon_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 uint8_t s_margin = PBL_DISPLAY_WIDTH / 35;
static const uint16_t s_available_height = PBL_DISPLAY_HEIGHT - s_sleep_bar_drop - s_margin + 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
if (y < s_sleep_bar_drop) {
y = s_sleep_bar_drop - s_wasted_top_text_pixels + s_margin;
}
layer_set_frame(text_layer_get_layer(s_last_watched_layer), GRect(PBL_IF_ROUND_ELSE(s_icon_width, 0) + s_margin,
y,
PBL_DISPLAY_WIDTH - (PBL_IF_ROUND_ELSE(s_icon_width * 2, s_icon_width) + (s_margin) * 2),
s_available_height));
// APP_LOG(APP_LOG_LEVEL_DEBUG, "height=%d rows=%d/%d", 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 button_bar_update_proc(Layer *layer, GContext *ctx) {
graphics_context_set_fill_color(ctx, s_color_secondary);
graphics_fill_rect(ctx, layer_get_bounds(layer), 0, GCornerNone);
}
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), 0, GCornerNone);
}
static void sleep_icon_update_proc(Layer *layer, GContext *ctx) {
graphics_context_set_antialiased(ctx, false);
gdraw_command_image_draw(ctx, s_sleep_icon, GPoint(0, 0));
}
static void draw_pin() {
GDrawCommandList *command_list = gdraw_command_image_get_command_list(s_pin_icon);
const uint32_t command_count = gdraw_command_list_get_num_commands(command_list);
for (uint32_t i = 0; i < command_count; ++i) {
GDrawCommand *command = gdraw_command_list_get_command(command_list, i);
if (command) {
#if PBL_ROUND
gdraw_command_set_fill_color(command, GColorClear);
gdraw_command_set_stroke_color(command, s_color_secondary);
#endif
gdraw_command_set_stroke_width(command, pin_animation_get_stroke_width());
}
}
}
static void pin_icon_update_proc(Layer *layer, GContext *ctx) {
graphics_context_set_antialiased(ctx, false);
draw_pin();
gdraw_command_image_draw(ctx, s_pin_icon, GPoint(0, 0));
}
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(4, (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(4, (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;
}
// sleep bar and icon
s_sleep_bar_layer = layer_create(s_sleep_bar_start);
s_sleep_icon_layer = layer_create(s_sleep_icon_start);
layer_set_update_proc(s_sleep_icon_layer, sleep_icon_update_proc);
// pin icon
s_pin_icon_layer = layer_create(GRect(PBL_DISPLAY_WIDTH - PBL_IF_ROUND_ELSE(s_icon_width + 3, s_icon_width), (PBL_DISPLAY_HEIGHT / 2) - 13, s_icon_width, s_icon_width));
layer_set_update_proc(s_pin_icon_layer, pin_icon_update_proc);
pin_animation_init(s_pin_icon_layer);
s_last_watched_layer = text_layer_create(GRect(0, 0, 0, 0)); // placeholder, updated when layer is written to
#if PBL_DISPLAY_WIDTH >= 200
s_sleep_time_layer = text_layer_create(GRect(0, PBL_IF_ROUND_ELSE(21, -2), 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(24, -2), 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);
text_layer_set_text_color(s_sleep_time_layer, GColorWhite);
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
#if PBL_RECT
// button bar
s_button_bar_layer = layer_create(GRect(PBL_DISPLAY_WIDTH - s_icon_width, 0, s_icon_width, PBL_DISPLAY_HEIGHT));
layer_set_update_proc(s_button_bar_layer, button_bar_update_proc);
layer_add_child(window_layer, s_button_bar_layer);
#endif
layer_add_child(window_layer, s_pin_icon_layer);
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, s_sleep_icon_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 {
pin_animation_deinit();
text_layer_destroy(s_last_watched_layer);
text_layer_destroy(s_sleep_time_layer);
layer_destroy(s_sleep_icon_layer);
layer_destroy(s_sleep_bar_layer);
layer_destroy(s_pin_icon_layer);
layer_destroy(s_button_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 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_single_click_handler(ClickRecognizerRef recognizer, void *context) {
pin_animation_start();
send_pin_to_pkjs();
}
static void click_config_provider(Window *window) {
window_single_click_subscribe(BUTTON_ID_SELECT, select_single_click_handler);
}
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);
window_set_click_config_provider(s_main_window, (ClickConfigProvider)click_config_provider);
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);
#if PBL_ROUND
layer_mark_dirty(s_pin_icon_layer);
#else
layer_mark_dirty(s_button_bar_layer);
#endif
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);
PropertyAnimation *sleep_bar_prop = property_animation_create_layer_frame(s_sleep_bar_layer, &s_sleep_bar_start, &GRect(0, -PBL_DISPLAY_HEIGHT + s_sleep_bar_drop, PBL_DISPLAY_WIDTH, PBL_DISPLAY_HEIGHT));
Animation *sleep_bar_anim = property_animation_get_animation(sleep_bar_prop);
animation_set_duration(sleep_bar_anim, 512);
PropertyAnimation *sleep_icon_prop = property_animation_create_layer_frame(s_sleep_icon_layer, &s_sleep_icon_start, &GRect(PBL_IF_ROUND_ELSE((PBL_DISPLAY_WIDTH / 2) - 13, 4), 4, s_icon_width, s_icon_width));
Animation *sleep_icon_anim = property_animation_get_animation(sleep_icon_prop);
animation_set_duration(sleep_icon_anim, 512);
Animation *load_spawn_anim = animation_spawn_create(sleep_bar_anim, sleep_icon_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
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;
}
s_pin_icon = gdraw_command_image_create_with_resource(RESOURCE_ID_PIN_ICON);
s_sleep_icon = gdraw_command_image_create_with_resource(RESOURCE_ID_SLEEP_ICON);
// initialize pkjs
app_message_register_inbox_received(inbox_received_handler);
app_message_open(1024, 1536);
// get sleep time (UNIX timestamp)
time_t end = time(NULL);
time_t start = end - (SECONDS_PER_DAY * 1.5);
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 deinit() { window_destroy(s_main_window); }
int main(void) {
init();
app_event_loop();
deinit();
}