20 Commits
Author SHA1 Message Date
RandyTheSilly 7669d76589 Remove clangd.zsh 2026-08-01 22:23:23 -04:00
RandyTheSilly dfe551d64c Bump neat_bar submodule 2026-07-22 23:51:46 -04:00
RandyTheSilly cc141961c6 Tidy defines 2026-07-21 23:37:10 -04:00
RandyTheSilly 7d556b025b Various alignment fixes + neat_bar bump 2026-07-21 22:16:18 -04:00
RandyTheSilly bcb7abfe7b Bump neat_bar submodule 2026-07-19 23:01:46 -04:00
RandyTheSilly 03dcf93f41 Bump neat_bar submodule 2026-07-19 22:03:20 -04:00
RandyTheSilly 5fb3ed65c1 Destroy neat bar on unload 2026-07-19 20:17:50 -04:00
RandyTheSilly 3b84319f83 Fix misc. issues on b&w watches due to GCompOpSet 2026-07-19 19:56:13 -04:00
RandyTheSilly 11ea312572 New pin icon (again) 2026-07-19 19:25:54 -04:00
RandyTheSilly 1c026c5986 Use the same arrow icon across all platforms 2026-07-19 18:26:36 -04:00
RandyTheSilly ff2c5c1acc Fix text y snapping on rect platforms 2026-07-19 18:02:20 -04:00
RandyTheSilly d8feaa2bb8 Fix text y snapping on Gabbro 2026-07-19 17:53:39 -04:00
RandyTheSilly d84edb5429 UI overhaul w/neat_bar 2026-07-19 17:27:31 -04:00
RandyTheSilly 0ab885d7ae Disable debug mode 2026-07-15 00:11:20 -04:00
RandyTheSilly b6ad2e5831 Migrate to standard action bar; prep for prev/next buttons 2026-07-15 00:03:09 -04:00
RandyTheSilly 8f9c79f641 Check for sleep sessions a full 48 hours into the past 2026-07-14 19:56:55 -04:00
RandyTheSilly d821f343b4 Add missing semicolons in customClay.js 2026-05-25 16:30:03 -04:00
RandyTheSilly 9ea92c70a6 Bump Clay 2026-05-25 16:01:09 -04:00
RandyTheSilly 5c1f093201 Update README.md 2026-05-16 20:38:58 -04:00
RandyTheSilly 838bf7cea6 Use better method for platform-specific icon 2026-05-16 20:30:30 -04:00
22 changed files with 164 additions and 250 deletions
+3
View File
@@ -0,0 +1,3 @@
[submodule "neat_bar"]
path = neat_bar
url = https://git.wetfence.xyz/RandyTheSilly/neat_bar.git
+6 -7
View File
@@ -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
-19
View File
@@ -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
-4
View File
@@ -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

-9
View File
@@ -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

Submodule
+1
Submodule neat_bar added at 55c3d658b6
+19 -20
View File
@@ -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",
Binary file not shown.

Before

Width:  |  Height:  |  Size: 154 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 B

Before

Width:  |  Height:  |  Size: 173 B

After

Width:  |  Height:  |  Size: 173 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 B

BIN
View File
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 105 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 B

-66
View File
@@ -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;
}
-8
View File
@@ -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);
+131 -114
View File
@@ -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);
+2 -2
View File
@@ -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");
}
}
+2 -1
View File
@@ -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)