From 9707e8b25a2b0042d53489e41d81eb5e495b16c2 Mon Sep 17 00:00:00 2001 From: Kurt Grieb Date: Wed, 13 May 2026 23:45:47 -0400 Subject: [PATCH 1/3] Add replay button to timer completion popup Adds a circular-arrow icon to the SELECT (middle) button on the "Time's Up!" popup, which restarts the timer with its original duration. The existing UP=snooze and DOWN=dismiss buttons are unchanged. Also registers the IMAGE_REPLAY resource and fixes pbi8 -> bitmap type for CloudPebble compatibility. Co-Authored-By: Claude Sonnet 4.6 --- package.json | 7 ++++++- resources/images/action_bar_icon_replay.png | Bin 0 -> 141 bytes src/main.c | 22 ++++++++++++++++++++ src/popup_window.c | 9 +++++--- src/popup_window.h | 2 +- 5 files changed, 35 insertions(+), 5 deletions(-) create mode 100644 resources/images/action_bar_icon_replay.png diff --git a/package.json b/package.json index 09810e3..9f8bea5 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "media": [ { "menuIcon": true, - "type": "pbi8", + "type": "bitmap", "name": "IMAGE_ICON", "file": "images/image_system_icon.png" }, @@ -75,6 +75,11 @@ "name": "IMAGE_SNOOZE", "file": "images/action_bar_icon_snooze.png" }, + { + "type": "bitmap", + "name": "IMAGE_REPLAY", + "file": "images/action_bar_icon_replay.png" + }, { "type": "bitmap", "name": "IMAGE_DELETE", diff --git a/resources/images/action_bar_icon_replay.png b/resources/images/action_bar_icon_replay.png new file mode 100644 index 0000000000000000000000000000000000000000..abea00f28b862f894dc464f903fbf099056b28ec GIT binary patch literal 141 zcmeAS@N?(olHy`uVBq!ia0vp^LLkfmBp8a9UhD=^A)YRdAr*5{`yL80D6lja?)|TQ z{cK6^ACCAlXZ95=)9tk{xSeq%z)iU=Ewr=uSFxDLy_ub_^q4=zW-Snr4CcS8yG-DJ o#gXYhdLJHGsoE2ky8djo%s)?I$qtp=TOfNpUHx3vIVCg!0JrHhn*aa+ literal 0 HcmV?d00001 diff --git a/src/main.c b/src/main.c index a38fb21..52d2ab7 100644 --- a/src/main.c +++ b/src/main.c @@ -143,6 +143,27 @@ static void popup_window_snooze_timer_callback(CountdownTimer *countdown_timer, +/* + * PopupWindow replay timer callback + * restarts the timer with its original duration + */ + +static void popup_window_replay_timer_callback(CountdownTimer *countdown_timer, void *context) { + countdown_timer_update(countdown_timer, countdown_timer_get_duration(countdown_timer), false); + countdown_timer_start(countdown_timer); + popup_window_pop(s_popup_window, true); + // show detail if not on top + if (!detail_window_get_topmost_window(s_detail_window)) { + detail_window_set_countdown_timer(s_detail_window, countdown_timer); + detail_window_push(s_detail_window, false); + } + detail_window_deep_refresh(s_detail_window); + // log activity + s_last_activity = countdown_timer_get_epoch_ms(); +} + + + /* * PopupWindow stop timer callback * cancels the current timer vibration sequence @@ -418,6 +439,7 @@ static void initialize(void) { // create pop-up window PopupWindowCallbacks popup_callbacks = { .up_click = popup_window_snooze_timer_callback, + .select_click = popup_window_replay_timer_callback, .down_click = popup_window_stop_timer_callback, }; s_popup_window = popup_window_create(); diff --git a/src/popup_window.c b/src/popup_window.c index 8757e93..6b63f71 100644 --- a/src/popup_window.c +++ b/src/popup_window.c @@ -73,7 +73,7 @@ struct PopupWindow { TextLayer *text; //< displays title text ActionBarLayer *action; //< optional action bar for dialogs PopupWindowCallbacks callbacks; //< callbacks for optional ActionBar - GBitmap *snooze_icon, *stop_icon; //< icons for ActionBar + GBitmap *snooze_icon, *stop_icon, *replay_icon; //< icons for ActionBar #ifndef PBL_PLATFORM_APLITE GDrawCommandSequence *draw_sequence; //< draw command sequence @@ -220,7 +220,7 @@ static void up_click_handler(ClickRecognizerRef recognizer, void *context) { /* * SELECT click handler callback * - * nothing yet... here for completeness + * replays the completed timer with its original duration */ static void select_click_handler(ClickRecognizerRef recognizer, void *context) { @@ -228,7 +228,7 @@ static void select_click_handler(ClickRecognizerRef recognizer, void *context) { if (popup_window->callbacks.select_click == NULL) { return; } - return popup_window->callbacks.select_click(context); + return popup_window->callbacks.select_click(popup_window->countdown_timer, context); } @@ -267,6 +267,7 @@ static void prv_window_load(Window* window){ // get window parameters popup_window->stop_icon = gbitmap_create_with_resource(RESOURCE_ID_IMAGE_DISMISS); popup_window->snooze_icon = gbitmap_create_with_resource(RESOURCE_ID_IMAGE_SNOOZE); + popup_window->replay_icon = gbitmap_create_with_resource(RESOURCE_ID_IMAGE_REPLAY); // get window parameters Layer *root = window_get_root_layer(popup_window->window); @@ -293,6 +294,7 @@ static void prv_window_load(Window* window){ action_bar_layer_set_context(popup_window->action, popup_window); action_bar_layer_set_click_config_provider(popup_window->action, click_config_provider); action_bar_layer_set_icon(popup_window->action, BUTTON_ID_UP, popup_window->snooze_icon); + action_bar_layer_set_icon(popup_window->action, BUTTON_ID_SELECT, popup_window->replay_icon); action_bar_layer_set_icon(popup_window->action, BUTTON_ID_DOWN, popup_window->stop_icon); if (popup_window->action_visible) { @@ -313,6 +315,7 @@ static void prv_window_unload(Window* window){ window_destroy(popup_window->window); gbitmap_destroy(popup_window->snooze_icon); gbitmap_destroy(popup_window->stop_icon); + gbitmap_destroy(popup_window->replay_icon); #ifndef PBL_PLATFORM_APLITE if (popup_window->draw_sequence != NULL) { gdraw_command_sequence_destroy(popup_window->draw_sequence); diff --git a/src/popup_window.h b/src/popup_window.h index 639c516..35b96a4 100644 --- a/src/popup_window.h +++ b/src/popup_window.h @@ -69,7 +69,7 @@ typedef void (*PopupWindowUpClick)(CountdownTimer *countdown_timer, void *contex * called when the SELECT button is pressed */ -typedef void (*PopupWindowSelectClick)(void *context); +typedef void (*PopupWindowSelectClick)(CountdownTimer *countdown_timer, void *context); From 49b5809464cbcdf5223009fcb23cea7c6931f119 Mon Sep 17 00:00:00 2001 From: Kurt Grieb Date: Wed, 13 May 2026 23:50:02 -0400 Subject: [PATCH 2/3] Add index.js entry point for CloudPebble compatibility --- src/js/index.js | 146 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 146 insertions(+) create mode 100644 src/js/index.js diff --git a/src/js/index.js b/src/js/index.js new file mode 100644 index 0000000..f4662eb --- /dev/null +++ b/src/js/index.js @@ -0,0 +1,146 @@ +// ********** Timeline ********** // +// "Exersized" pin +var timerPIN = { + "id": "", + "time": 0, + "layout": { + "type": "weatherPin", + "title": "Timer Complete", + "subtitle": "50:00", + "tinyIcon": "system://images/ALARM_CLOCK", + "largeIcon": "system://images/ALARM_CLOCK", + "locationName": " ", + "backgroundColor": "#55AAFF" + }, + "actions": [ + { + "title": "Open Timer", + "type": "openWatchApp", + "launchCode": 10 + } + // { + // "title": "Restart Timer", + // "type": "openWatchApp", + // "launchCode": 11 + // }, + // { + // "title": "Delete Timer", + // "type": "openWatchApp", + // "launchCode": 12 + // } + ] +}; + +// ***** Timeline Lib ***** // +// The Timeline public URL root +var API_URL_ROOT = 'https://timeline-api.getpebble.com/'; +/** + * Send a request to the Pebble public web timeline API. + * @param pin The JSON pin to insert. Must contain 'id' field. + * @param type The type of request, either PUT or DELETE. + * @param callback The callback to receive the responseText after the request has completed. + */ +function timelineRequest(pin, type, callback) { + // User or shared? + var url = API_URL_ROOT + 'v1/user/pins/' + pin.id; + + // Create XHR + var xhr = new XMLHttpRequest(); + xhr.onload = function () { + console.log('timeline: response received: ' + this.responseText); + callback(this.responseText); + }; + xhr.open(type, url); + + // Get token + Pebble.getTimelineToken(function (token) { + // Add headers + xhr.setRequestHeader('Content-Type', 'application/json'); + xhr.setRequestHeader('X-User-Token', '' + token); + + // Send + xhr.send(JSON.stringify(pin)); + console.log('timeline: request sent.'); + }, function (error) { console.log('timeline: error getting timeline token: ' + error); }); +} +/** + * Insert a pin into the timeline for this user. + * @param pin The JSON pin to insert. + * @param callback The callback to receive the responseText after the request has completed. + */ +function insertUserPin(pin, callback) { + timelineRequest(pin, 'PUT', callback); +} +/** + * Delete a pin from the timeline for this user. + * @param pin The JSON pin to delete. + * @param callback The callback to receive the responseText after the request has completed. + */ +function deleteUserPin(pin, callback) { + timelineRequest(pin, 'DELETE', callback); +} + + + +// ********** AppMessage ********** // +// send message to phone +function send_to_phone(){ + // create dictionary + var dict = { + 'KEY_DURATION':0 + }; + + // send to pebble + Pebble.sendAppMessage(dict, + function(e) { + console.log('Send successful.'); + }, + function(e) { + console.log('Send failed!'); + } + ); +} + +// message received +Pebble.addEventListener('appmessage', function(e) { + // check for key + if (e.payload.hasOwnProperty('KEY_DURATION')){ + // check that it is valid to send pins i.e. its SDK 3.0 or greater + if (typeof Pebble.getTimelineToken == 'function') { + // update pin time + timerPIN.id = e.payload.KEY_UNIQUEID.toString(); + // show total time + var tot = e.payload.KEY_TOTAL_TIME / 60; + var hr = Math.floor(tot / 60); + var min = Math.floor(tot % 60); + if (hr < 10) hr = "0" + hr; + if (min < 10) min = "0" + min; + timerPIN.layout.subtitle = hr + ":" + min; + // zero two least significant digits + timerPIN.actions[0].launchCode = timerPIN.id * 100 + 10; + // timerPIN.actions[1].launchCode = timerPIN.id * 100 + 11; + // timerPIN.actions[2].launchCode = timerPIN.id * 100 + 12; + // check if deleting + if (e.payload.KEY_DURATION > 0){ + // update date + var tDate = new Date(); + tDate.setSeconds(tDate.getSeconds() + e.payload.KEY_DURATION); + timerPIN.time = tDate.toISOString(); + // insert pin + insertUserPin(timerPIN, function (responseText) { + console.log('Pin Sent Result (' + timerPIN.id + '): ' + responseText); + }); + } + else{ + deleteUserPin(timerPIN, function (responseText) { + console.log('Pin Deleted Result (' + timerPIN.id + '): ' + responseText); + }); + } + } + } +}); + +// loaded and ready +Pebble.addEventListener('ready', function(e) { + console.log("JS ready!"); +}); \ No newline at end of file From d62d99aa7f037f3ba7058ae32a046c3486eda76a Mon Sep 17 00:00:00 2001 From: Kurt Grieb Date: Thu, 14 May 2026 00:05:36 -0400 Subject: [PATCH 3/3] Revert CloudPebble workarounds (pbi8, index.js) --- package.json | 2 +- src/js/index.js | 146 ------------------------------------------------ 2 files changed, 1 insertion(+), 147 deletions(-) delete mode 100644 src/js/index.js diff --git a/package.json b/package.json index 9f8bea5..228e254 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "media": [ { "menuIcon": true, - "type": "bitmap", + "type": "pbi8", "name": "IMAGE_ICON", "file": "images/image_system_icon.png" }, diff --git a/src/js/index.js b/src/js/index.js deleted file mode 100644 index f4662eb..0000000 --- a/src/js/index.js +++ /dev/null @@ -1,146 +0,0 @@ -// ********** Timeline ********** // -// "Exersized" pin -var timerPIN = { - "id": "", - "time": 0, - "layout": { - "type": "weatherPin", - "title": "Timer Complete", - "subtitle": "50:00", - "tinyIcon": "system://images/ALARM_CLOCK", - "largeIcon": "system://images/ALARM_CLOCK", - "locationName": " ", - "backgroundColor": "#55AAFF" - }, - "actions": [ - { - "title": "Open Timer", - "type": "openWatchApp", - "launchCode": 10 - } - // { - // "title": "Restart Timer", - // "type": "openWatchApp", - // "launchCode": 11 - // }, - // { - // "title": "Delete Timer", - // "type": "openWatchApp", - // "launchCode": 12 - // } - ] -}; - -// ***** Timeline Lib ***** // -// The Timeline public URL root -var API_URL_ROOT = 'https://timeline-api.getpebble.com/'; -/** - * Send a request to the Pebble public web timeline API. - * @param pin The JSON pin to insert. Must contain 'id' field. - * @param type The type of request, either PUT or DELETE. - * @param callback The callback to receive the responseText after the request has completed. - */ -function timelineRequest(pin, type, callback) { - // User or shared? - var url = API_URL_ROOT + 'v1/user/pins/' + pin.id; - - // Create XHR - var xhr = new XMLHttpRequest(); - xhr.onload = function () { - console.log('timeline: response received: ' + this.responseText); - callback(this.responseText); - }; - xhr.open(type, url); - - // Get token - Pebble.getTimelineToken(function (token) { - // Add headers - xhr.setRequestHeader('Content-Type', 'application/json'); - xhr.setRequestHeader('X-User-Token', '' + token); - - // Send - xhr.send(JSON.stringify(pin)); - console.log('timeline: request sent.'); - }, function (error) { console.log('timeline: error getting timeline token: ' + error); }); -} -/** - * Insert a pin into the timeline for this user. - * @param pin The JSON pin to insert. - * @param callback The callback to receive the responseText after the request has completed. - */ -function insertUserPin(pin, callback) { - timelineRequest(pin, 'PUT', callback); -} -/** - * Delete a pin from the timeline for this user. - * @param pin The JSON pin to delete. - * @param callback The callback to receive the responseText after the request has completed. - */ -function deleteUserPin(pin, callback) { - timelineRequest(pin, 'DELETE', callback); -} - - - -// ********** AppMessage ********** // -// send message to phone -function send_to_phone(){ - // create dictionary - var dict = { - 'KEY_DURATION':0 - }; - - // send to pebble - Pebble.sendAppMessage(dict, - function(e) { - console.log('Send successful.'); - }, - function(e) { - console.log('Send failed!'); - } - ); -} - -// message received -Pebble.addEventListener('appmessage', function(e) { - // check for key - if (e.payload.hasOwnProperty('KEY_DURATION')){ - // check that it is valid to send pins i.e. its SDK 3.0 or greater - if (typeof Pebble.getTimelineToken == 'function') { - // update pin time - timerPIN.id = e.payload.KEY_UNIQUEID.toString(); - // show total time - var tot = e.payload.KEY_TOTAL_TIME / 60; - var hr = Math.floor(tot / 60); - var min = Math.floor(tot % 60); - if (hr < 10) hr = "0" + hr; - if (min < 10) min = "0" + min; - timerPIN.layout.subtitle = hr + ":" + min; - // zero two least significant digits - timerPIN.actions[0].launchCode = timerPIN.id * 100 + 10; - // timerPIN.actions[1].launchCode = timerPIN.id * 100 + 11; - // timerPIN.actions[2].launchCode = timerPIN.id * 100 + 12; - // check if deleting - if (e.payload.KEY_DURATION > 0){ - // update date - var tDate = new Date(); - tDate.setSeconds(tDate.getSeconds() + e.payload.KEY_DURATION); - timerPIN.time = tDate.toISOString(); - // insert pin - insertUserPin(timerPIN, function (responseText) { - console.log('Pin Sent Result (' + timerPIN.id + '): ' + responseText); - }); - } - else{ - deleteUserPin(timerPIN, function (responseText) { - console.log('Pin Deleted Result (' + timerPIN.id + '): ' + responseText); - }); - } - } - } -}); - -// loaded and ready -Pebble.addEventListener('ready', function(e) { - console.log("JS ready!"); -}); \ No newline at end of file