Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7669d76589 | ||
|
|
dfe551d64c | ||
|
|
cc141961c6 | ||
|
|
7d556b025b | ||
|
|
bcb7abfe7b | ||
|
|
03dcf93f41 | ||
|
|
5fb3ed65c1 | ||
|
|
3b84319f83 | ||
|
|
11ea312572 | ||
|
|
1c026c5986 | ||
|
|
ff2c5c1acc | ||
|
|
d8feaa2bb8 | ||
|
|
d84edb5429 | ||
|
|
0ab885d7ae | ||
|
|
b6ad2e5831 | ||
|
|
8f9c79f641 | ||
|
|
d821f343b4 | ||
|
|
9ea92c70a6 | ||
|
|
5c1f093201 | ||
|
|
838bf7cea6 |
@@ -0,0 +1,3 @@
|
||||
[submodule "neat_bar"]
|
||||
path = neat_bar
|
||||
url = https://git.wetfence.xyz/RandyTheSilly/neat_bar.git
|
||||
@@ -1,12 +1,11 @@
|
||||
# 1 More Episode?
|
||||
"#^%$! I fell asleep and woke up watching a different season! What was the last episode I saw?!" - You, probably
|
||||
|
||||
Well now your Pebble smartwatch can tell you! This app uses your Pebble Health
|
||||
data to match the time you fell asleep with your Jellyfin/Plex watch history.
|
||||
You can even track what you fell asleep to each night on your timeline!
|
||||
Well now your Pebble smartwatch can tell you! This app uses your Pebble Health data to match the time you fell asleep with your Jellyfin/Plex watch history. You can even track what you fell asleep to each night on your past timeline!
|
||||
|
||||
Simply provide your Jellyfin/Plex ([Tautulli](https://tautulli.com/)) API key+URL
|
||||
in settings and let your Pebble keep track!
|
||||
Simply provide your Jellyfin/Plex ([Tautulli](https://tautulli.com/)) API key+URL in settings and let your Pebble keep track! The app changes themes automatically to match your streaming platform of choice.
|
||||
|
||||
Due to limitations of Pebble sleep data retention, it can only show data for your most recent sleep session, and it has to be within the last day, so be sure to launch the app the day you wake up from your binge!
|
||||
|
||||
>[!WARNING]
|
||||
>Jellyfin is strongly encouraged! Plex support requires installing [Tautulli](https://tautulli.com/)
|
||||
@@ -16,8 +15,8 @@ in settings and let your Pebble keep track!
|
||||
- v1.1+
|
||||
- [ ] Use larger sleep icon on loading screen & animate shrink
|
||||
- [ ] Make last watched text scrollable (if too long)
|
||||
- [ ] Allow setting manual sleep offsets (if your sleep tracking is typically a little off)
|
||||
- [ ] Allow on-the-fly sleep time adjustments with top/bottom buttons
|
||||
- [ ] Allow using up/down buttons to scroll between adjacent episodes
|
||||
- [ ] Keep 7-point watch history saved on phone (save with timeline pin button)
|
||||
- [ ] Make scaling less ugly on Gabbro
|
||||
- [ ] General optimization and cleanup
|
||||
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
#!/usr/bin/env zsh
|
||||
echo "CompileFlags:
|
||||
Add:
|
||||
[
|
||||
-DPBL_DISPLAY_WIDTH=200,
|
||||
-DPBL_DISPLAY_HEIGHT=228,
|
||||
-xc,
|
||||
-nostdinc,
|
||||
-DPBL_COLOR,
|
||||
-I${HOME}/.pebble-sdk/SDKs/current/sdk-core/pebble/emery/include,
|
||||
-include$(pwd)/build/include/message_keys.auto.h,
|
||||
-I$(pwd)/build/emery,
|
||||
-include${HOME}/.pebble-sdk/SDKs/current/toolchain/arm-none-eabi/arm-none-eabi/include/stdint.h,
|
||||
-include${HOME}/.pebble-sdk/SDKs/current/toolchain/arm-none-eabi/arm-none-eabi/include/stdlib.h,
|
||||
-include${HOME}/.pebble-sdk/SDKs/current/toolchain/arm-none-eabi/arm-none-eabi/include/time.h,
|
||||
-include${HOME}/.pebble-sdk/SDKs/current/toolchain/arm-none-eabi/arm-none-eabi/include/string.h,
|
||||
-include${HOME}/.pebble-sdk/SDKs/current/toolchain/arm-none-eabi/lib/gcc/arm-none-eabi/14.2.1/include/stddef.h,
|
||||
-include${HOME}/.pebble-sdk/SDKs/current/toolchain/arm-none-eabi/lib/gcc/arm-none-eabi/14.2.1/include/stdbool.h,
|
||||
]" > ./.clangd
|
||||
@@ -1,4 +0,0 @@
|
||||
<svg width="25" height="25" viewBox="0 0 25 25" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M22 18L16 21L11 20L5 18L3 14L9 13L12 2H16L20 5L17 15L22 18Z" fill="white"/>
|
||||
<path d="M12 17L10 24M9 13L3 14L5 18L16 21L22 18L17 15M9 13L13 13L17 15M9 13L12 2H16L20 5L17 15" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 366 B |
@@ -1,9 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 20.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.2" baseProfile="tiny" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
x="0px" y="0px" viewBox="0 0 25 23" xml:space="preserve">
|
||||
<polygon fill="#FFFFFF" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" points="
|
||||
18,1 24,7 24,15 17,22 9,22 7,20 1,22 4,17 2,15 2,7 8,1 "/>
|
||||
<polyline fill="#FFFFFF" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" points="
|
||||
9,8 16,8 9,15 16,15 "/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 690 B |
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"author": "RandyTheSilly",
|
||||
"dependencies": {
|
||||
"@rebble/clay": "^1.0.8"
|
||||
"@rebble/clay": "^1.0.10"
|
||||
},
|
||||
"keywords": [
|
||||
"jellyfin",
|
||||
@@ -38,39 +38,38 @@
|
||||
"name": "LOGO_25",
|
||||
"spaceOptimization": "storage",
|
||||
"type": "bitmap",
|
||||
"menuIcon": true,
|
||||
"targetPlatforms": [
|
||||
"basalt",
|
||||
"chalk",
|
||||
"emery",
|
||||
"gabbro"
|
||||
]
|
||||
"menuIcon": true
|
||||
},
|
||||
{
|
||||
"file": "logo_25_no_aa.png",
|
||||
"file": "pin.png",
|
||||
"memoryFormat": "Smallest",
|
||||
"name": "LOGO_25",
|
||||
"name": "PIN",
|
||||
"spaceOptimization": "storage",
|
||||
"type": "bitmap",
|
||||
"targetPlatforms": [
|
||||
"diorite",
|
||||
"flint"
|
||||
]
|
||||
"type": "bitmap"
|
||||
},
|
||||
{
|
||||
"type": "raw",
|
||||
"name": "PIN_ICON",
|
||||
"file": "pin.pdc"
|
||||
"file": "prev.png",
|
||||
"memoryFormat": "Smallest",
|
||||
"name": "PREV",
|
||||
"spaceOptimization": "storage",
|
||||
"type": "bitmap"
|
||||
},
|
||||
{
|
||||
"file": "next.png",
|
||||
"memoryFormat": "Smallest",
|
||||
"name": "NEXT",
|
||||
"spaceOptimization": "storage",
|
||||
"type": "bitmap"
|
||||
}
|
||||
]
|
||||
},
|
||||
"sdkVersion": "3",
|
||||
"targetPlatforms": [
|
||||
"basalt",
|
||||
"diorite",
|
||||
"chalk",
|
||||
"flint",
|
||||
"diorite",
|
||||
"emery",
|
||||
"flint",
|
||||
"gabbro"
|
||||
],
|
||||
"uuid": "3097b48e-552c-4a78-92b2-566b9128640e",
|
||||
|
||||
|
Before Width: | Height: | Size: 154 B |
|
After Width: | Height: | Size: 144 B |
|
Before Width: | Height: | Size: 173 B After Width: | Height: | Size: 173 B |
|
After Width: | Height: | Size: 84 B |
|
After Width: | Height: | Size: 96 B |
|
After Width: | Height: | Size: 105 B |
|
After Width: | Height: | Size: 140 B |
|
After Width: | Height: | Size: 80 B |
|
After Width: | Height: | Size: 92 B |
@@ -1,66 +0,0 @@
|
||||
#include "gdraw_anim.h"
|
||||
|
||||
static uint8_t s_pin_stroke_width = 1;
|
||||
static Animation *s_pin_stroke_anim;
|
||||
static Layer *s_pin_layer;
|
||||
|
||||
static uint8_t get_stroke_width_for_progress(AnimationProgress progress) {
|
||||
const uint8_t min_width = PBL_IF_ROUND_ELSE(1, 2);
|
||||
const uint8_t max_width = PBL_IF_ROUND_ELSE(3, 4);
|
||||
const uint8_t delta = max_width - min_width;
|
||||
const AnimationProgress half = ANIMATION_NORMALIZED_MAX / 2;
|
||||
|
||||
if (progress <= half) {
|
||||
return min_width + (uint8_t)(delta * progress * 2 / ANIMATION_NORMALIZED_MAX);
|
||||
}
|
||||
return max_width - (uint8_t)(delta * (progress - half) * 2 / ANIMATION_NORMALIZED_MAX);
|
||||
}
|
||||
|
||||
static void pin_stroke_anim_update(Animation *animation, const AnimationProgress progress) {
|
||||
s_pin_stroke_width = get_stroke_width_for_progress(progress);
|
||||
if (s_pin_layer) {
|
||||
layer_mark_dirty(s_pin_layer);
|
||||
}
|
||||
}
|
||||
|
||||
static void pin_stroke_anim_stopped(Animation *animation, bool finished, void *context) {
|
||||
s_pin_stroke_anim = NULL;
|
||||
animation_destroy(animation);
|
||||
}
|
||||
|
||||
void pin_animation_init(Layer *pin_layer) {
|
||||
s_pin_layer = pin_layer;
|
||||
s_pin_stroke_width = PBL_IF_ROUND_ELSE(1, 2);
|
||||
}
|
||||
|
||||
void pin_animation_start(void) {
|
||||
if (s_pin_stroke_anim) {
|
||||
animation_unschedule(s_pin_stroke_anim);
|
||||
animation_destroy(s_pin_stroke_anim);
|
||||
s_pin_stroke_anim = NULL;
|
||||
}
|
||||
|
||||
static const AnimationImplementation s_pin_stroke_anim_impl = {
|
||||
.update = pin_stroke_anim_update,
|
||||
};
|
||||
|
||||
s_pin_stroke_anim = animation_create();
|
||||
animation_set_implementation(s_pin_stroke_anim, &s_pin_stroke_anim_impl);
|
||||
animation_set_duration(s_pin_stroke_anim, 400);
|
||||
animation_set_curve(s_pin_stroke_anim, AnimationCurveEaseInOut);
|
||||
animation_set_handlers(s_pin_stroke_anim, (AnimationHandlers){.stopped = pin_stroke_anim_stopped}, NULL);
|
||||
animation_schedule(s_pin_stroke_anim);
|
||||
}
|
||||
|
||||
void pin_animation_deinit(void) {
|
||||
if (s_pin_stroke_anim) {
|
||||
animation_unschedule(s_pin_stroke_anim);
|
||||
animation_destroy(s_pin_stroke_anim);
|
||||
s_pin_stroke_anim = NULL;
|
||||
}
|
||||
s_pin_layer = NULL;
|
||||
}
|
||||
|
||||
uint8_t pin_animation_get_stroke_width(void) {
|
||||
return s_pin_stroke_width;
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <pebble.h>
|
||||
|
||||
void pin_animation_init(Layer *pin_layer);
|
||||
void pin_animation_deinit(void);
|
||||
void pin_animation_start(void);
|
||||
uint8_t pin_animation_get_stroke_width(void);
|
||||
@@ -1,45 +1,59 @@
|
||||
#include "gdraw_anim.h"
|
||||
#include "../../neat_bar/src/c/neat_bar.h"
|
||||
#include <pebble.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
// #define DEBUG_MODE 1
|
||||
#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 Layer *s_button_bar_layer;
|
||||
static NeatBarLayer *s_neat_bar_layer;
|
||||
static Layer *s_sleep_bar_layer;
|
||||
static BitmapLayer *s_logo_layer;
|
||||
static Layer *s_pin_icon_layer;
|
||||
static GBitmap *s_logo;
|
||||
static GDrawCommandImage *s_pin_icon;
|
||||
static const uint8_t s_icon_width = 25;
|
||||
static const uint8_t s_logo_width = 25;
|
||||
|
||||
// defines
|
||||
#define MARGIN 8
|
||||
////
|
||||
#if PBL_ROUND
|
||||
#if PBL_DISPLAY_WIDTH >= 200
|
||||
static const uint8_t s_sleep_bar_drop = 54;
|
||||
//
|
||||
#define NEAT_BAR_WIDTH 30
|
||||
#define SLEEP_BAR_HEIGHT (PBL_DISPLAY_HEIGHT >= 260 ? 54 : 50)
|
||||
//
|
||||
#else
|
||||
static const uint8_t s_sleep_bar_drop = 50;
|
||||
#endif
|
||||
#else
|
||||
static const uint8_t s_sleep_bar_drop = 34;
|
||||
//
|
||||
#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_icon_width, s_icon_width);
|
||||
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;
|
||||
@@ -47,8 +61,7 @@ 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 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
|
||||
@@ -63,15 +76,17 @@ static void write_last_watched(const char *text) {
|
||||
|
||||
// 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) {
|
||||
y = s_sleep_bar_drop - s_wasted_top_text_pixels + s_margin;
|
||||
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
|
||||
}
|
||||
|
||||
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);
|
||||
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) {
|
||||
@@ -85,35 +100,52 @@ static void update_sleep_time(time_t start, time_t end) {
|
||||
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_antialiased(ctx, true);
|
||||
graphics_context_set_fill_color(ctx, s_color_primary);
|
||||
graphics_fill_rect(ctx, layer_get_bounds(layer), 0, GCornerNone);
|
||||
graphics_fill_rect(ctx, layer_get_bounds(layer), PBL_IF_ROUND_ELSE(15, 3), PBL_IF_ROUND_ELSE(GCornersBottom, GCornersAll));
|
||||
}
|
||||
|
||||
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 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 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 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) {
|
||||
@@ -121,10 +153,10 @@ static void main_window_load(Window *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(s_margin, (PBL_DISPLAY_HEIGHT / 2) - PBL_IF_ROUND_ELSE(33, 49), PBL_DISPLAY_WIDTH - 8, PBL_DISPLAY_HEIGHT));
|
||||
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(s_margin, (PBL_DISPLAY_HEIGHT / 2) - PBL_IF_ROUND_ELSE(43, 32), PBL_DISPLAY_WIDTH - 8, PBL_DISPLAY_HEIGHT));
|
||||
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);
|
||||
@@ -134,24 +166,46 @@ static void main_window_load(Window *window) {
|
||||
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, NEAT_BAR_STYLE_RECT);
|
||||
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);
|
||||
bitmap_layer_set_bitmap(s_logo_layer, s_logo);
|
||||
#endif
|
||||
bitmap_layer_set_bitmap(s_logo_layer, s_logo_icon);
|
||||
|
||||
// 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
|
||||
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, 2), PBL_DISPLAY_WIDTH, 30));
|
||||
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, 2), PBL_DISPLAY_WIDTH, 30));
|
||||
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
|
||||
@@ -173,9 +227,9 @@ static void main_window_load(Window *window) {
|
||||
} else {
|
||||
#if PBL_RECT
|
||||
#if PBL_DISPLAY_WIDTH >= 200
|
||||
text_layer_set_text(s_sleep_time_layer, " No sleep time found");
|
||||
text_layer_set_text(s_sleep_time_layer, " No sleep time found");
|
||||
#else
|
||||
text_layer_set_text(s_sleep_time_layer, " No sleep time");
|
||||
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");
|
||||
@@ -195,13 +249,6 @@ static void main_window_load(Window *window) {
|
||||
}
|
||||
|
||||
// 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, bitmap_layer_get_layer(s_logo_layer));
|
||||
@@ -212,13 +259,11 @@ 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);
|
||||
bitmap_layer_destroy(s_logo_layer);
|
||||
layer_destroy(s_sleep_bar_layer);
|
||||
layer_destroy(s_pin_icon_layer);
|
||||
layer_destroy(s_button_bar_layer);
|
||||
neat_bar_layer_destroy(s_neat_bar_layer);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -245,41 +290,6 @@ static void send_sleep_time_to_pkjs() {
|
||||
}
|
||||
}
|
||||
|
||||
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 const uint32_t segments[] = {60};
|
||||
VibePattern pattern = {
|
||||
.durations = segments,
|
||||
.num_segments = ARRAY_LENGTH(segments),
|
||||
};
|
||||
vibes_enqueue_custom_pattern(pattern);
|
||||
}
|
||||
|
||||
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);
|
||||
@@ -291,7 +301,6 @@ static void soft_reload(bool first_load) {
|
||||
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();
|
||||
}
|
||||
|
||||
@@ -322,11 +331,6 @@ static void inbox_received_handler(DictionaryIterator *iter, void *context) {
|
||||
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;
|
||||
}
|
||||
@@ -334,10 +338,21 @@ static void inbox_received_handler(DictionaryIterator *iter, void *context) {
|
||||
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));
|
||||
#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, s_margin), 3, s_icon_width, s_icon_width));
|
||||
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);
|
||||
@@ -370,8 +385,10 @@ static void init() {
|
||||
s_color_primary = GColorWhite;
|
||||
s_color_secondary = GColorWhite;
|
||||
#endif
|
||||
s_pin_icon = gdraw_command_image_create_with_resource(RESOURCE_ID_PIN_ICON);
|
||||
s_logo = gbitmap_create_with_resource(RESOURCE_ID_LOGO_25);
|
||||
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);
|
||||
@@ -379,7 +396,7 @@ static void init() {
|
||||
|
||||
// get sleep time (UNIX timestamp)
|
||||
time_t end = time(NULL);
|
||||
time_t start = end - (SECONDS_PER_DAY * 1.5);
|
||||
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);
|
||||
|
||||
@@ -8,11 +8,11 @@ module.exports = function (minified) {
|
||||
if (this.get()) {
|
||||
clayConfig.getItemById('helptext_host').set("Examples:<br>https://jellyfin.example.com<br>http://192.168.1.10:8096");
|
||||
clayConfig.getItemById('helptext_api_key').set("1. Log into Jellyfin web UI as an admin<br>2. Go to your \"Dashboard\"<br>3. Select \"API Keys\" from the left menu<br>4. Create a key with \"New API Key\"; name it whatever you like and enter the key (not the name) in this box<br><br>The key will look like this:<br>d7f4c2e8a4b3444bb5d6b685fa5918b6");
|
||||
clayConfig.getItemById('helptext_user').set("Enter Jellyfin username to track;<br>this is used in case multiple users were active when you fell asleep")
|
||||
clayConfig.getItemById('helptext_user').set("Enter Jellyfin username to track;<br>this is used in case multiple users were active when you fell asleep");
|
||||
} else {
|
||||
clayConfig.getItemById('helptext_host').set("IMPORTANT:<br>Plex support requires installing Tautulli alongside Plex.<br>Enter the URL to your Tautulli instance, not Plex itself.<br><br>Examples:<br>https://tautulli.example.com<br>http://192.168.1.10:8181");
|
||||
clayConfig.getItemById('helptext_api_key').set("1. Log into your Tautulli web UI (after initial configuration)<br>2. Go to settings<br>3. Select \"Web Interface\" from the left menu<br>4. At the bottom, ensure \"Enable API\" is checked<br>5. Reveal your API key; this is what goes in this box<br><br>The key will look like this:<br>9cb7053181cf4c8d807640f8c6a93b20");
|
||||
clayConfig.getItemById('helptext_user').set("Enter Plex username to track;<br>this is used in case multiple users were active when you fell asleep")
|
||||
clayConfig.getItemById('helptext_user').set("Enter Plex username to track;<br>this is used in case multiple users were active when you fell asleep");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -34,7 +34,8 @@ def build(ctx):
|
||||
ctx.env = ctx.all_envs[platform]
|
||||
ctx.set_group(ctx.env.PLATFORM_NAME)
|
||||
app_elf = '{}/pebble-app.elf'.format(ctx.env.BUILD_DIR)
|
||||
ctx.pbl_build(source=ctx.path.ant_glob('src/c/**/*.c'), target=app_elf, bin_type='app')
|
||||
ctx.pbl_build(source=ctx.path.ant_glob(['src/c/**/*.c', 'neat_bar/src/c/**/*.c']), target=app_elf, bin_type='app')
|
||||
|
||||
|
||||
if build_worker:
|
||||
worker_elf = '{}/pebble-worker.elf'.format(ctx.env.BUILD_DIR)
|
||||
|
||||