1+ /**
2+ * Timer Widget for BangleJS 2
3+ *
4+ * A battery-optimized timer widget with gesture-based controls and accidental activation protection.
5+ * Features double-swipe unlock mechanism, visual feedback, and adaptive refresh rates.
6+ *
7+ * @author Claude AI Assistant
8+ * @version 0.03
9+ */
10+ ( ( ) => {
11+ "use strict" ;
12+
13+ // =============================================================================
14+ // CONSTANTS
15+ // =============================================================================
16+
17+ /** Timer adjustment constants (in seconds) */
18+ const ONE_MINUTE = 60 ;
19+ const TEN_MINUTES = 600 ;
20+ const DEFAULT_TIME = 300 ; // 5 minutes
21+
22+ /** Refresh rate constants for battery optimization */
23+ const COUNTDOWN_INTERVAL_NORMAL = 10000 ; // 10 seconds when > 1 minute
24+ const COUNTDOWN_INTERVAL_FINAL = 1000 ; // 1 second when <= 1 minute
25+
26+ /** Completion notification constants */
27+ const BUZZ_COUNT = 3 ;
28+ const BUZZ_TOTAL_TIME = 5000 ; // 5 seconds total
29+
30+ /** Gesture control constants */
31+ const UNLOCK_GESTURE_TIMEOUT = 1500 ; // milliseconds before unlock gesture has to be started from scratcb
32+ const UNLOCK_CONTROL_TIMEOUT = 5000 ; // milliseconds before gesture control locks again
33+ const DIRECTION_LEFT = "left" ;
34+ const DIRECTION_RIGHT = "right" ;
35+ const DIRECTION_UP = "up" ;
36+ const DIRECTION_DOWN = "down" ;
37+
38+
39+
40+
41+ // =============================================================================
42+ // STATE VARIABLES
43+ // =============================================================================
44+
45+ var settings ;
46+ var interval = 0 ;
47+ var remainingTime = 0 ; // in seconds
48+
49+ // =============================================================================
50+ // UTILITY FUNCTIONS
51+ // =============================================================================
52+
53+ /**
54+ * Format time as MM:SS (allowing MM > 59)
55+ * @param {number } seconds - Time in seconds
56+ * @returns {string } Formatted time string
57+ */
58+ function formatTime ( seconds ) {
59+ var mins = Math . floor ( seconds / 60 ) ;
60+ var secs = seconds % 60 ;
61+ return mins . toString ( ) . padStart ( 2 , '0' ) + ':' + secs . toString ( ) . padStart ( 2 , '0' ) ;
62+ }
63+
64+ // =============================================================================
65+ // SETTINGS MANAGEMENT
66+ // =============================================================================
67+
68+ /**
69+ * Save current settings to storage
70+ */
71+ function saveSettings ( ) {
72+ require ( 'Storage' ) . writeJSON ( 'widtimer.json' , settings ) ;
73+ }
74+
75+ /**
76+ * Load settings from storage and calculate current timer state
77+ */
78+ function loadSettings ( ) {
79+ settings = require ( 'Storage' ) . readJSON ( 'widtimer.json' , 1 ) || {
80+ totalTime : DEFAULT_TIME ,
81+ running : false ,
82+ startTime : 0
83+ } ;
84+
85+ // Calculate remaining time if timer was running
86+ if ( settings . running && settings . startTime ) {
87+ var elapsed = Math . floor ( ( Date . now ( ) - settings . startTime ) / 1000 ) ;
88+ remainingTime = Math . max ( 0 , settings . totalTime - elapsed ) ;
89+ if ( remainingTime === 0 ) {
90+ settings . running = false ;
91+ saveSettings ( ) ;
92+ }
93+ } else {
94+ remainingTime = settings . totalTime ;
95+ }
96+ }
97+
98+ // =============================================================================
99+ // TIMER CONTROL FUNCTIONS
100+ // =============================================================================
101+
102+ /**
103+ * Main countdown function - handles timer progression and battery optimization
104+ */
105+ function countdown ( ) {
106+ if ( ! settings . running ) return ;
107+
108+ var elapsed = Math . floor ( ( Date . now ( ) - settings . startTime ) / 1000 ) ;
109+ var oldRemainingTime = remainingTime ;
110+ remainingTime = Math . max ( 0 , settings . totalTime - elapsed ) ;
111+
112+ // Switch to faster refresh when entering final minute for better accuracy
113+ if ( oldRemainingTime > 60 && remainingTime <= 60 && interval ) {
114+ clearInterval ( interval ) ;
115+ interval = setInterval ( countdown , COUNTDOWN_INTERVAL_FINAL ) ;
116+ }
117+
118+ if ( remainingTime <= 0 ) {
119+ // Timer finished - provide completion notification
120+ buzzMultiple ( ) ;
121+ settings . running = false ;
122+ remainingTime = settings . totalTime ; // Reset to original time
123+ saveSettings ( ) ;
124+ if ( interval ) {
125+ clearInterval ( interval ) ;
126+ interval = 0 ;
127+ }
128+ }
129+
130+ WIDGETS [ "widtimer" ] . draw ( ) ;
131+ }
132+
133+ /**
134+ * Generate multiple buzzes for timer completion notification
135+ */
136+ function buzzMultiple ( ) {
137+ var buzzInterval = BUZZ_TOTAL_TIME / BUZZ_COUNT ;
138+ for ( var i = 0 ; i < BUZZ_COUNT ; i ++ ) {
139+ ( function ( delay ) {
140+ setTimeout ( function ( ) {
141+ Bangle . buzz ( 300 ) ;
142+ } , delay ) ;
143+ } ) ( i * buzzInterval ) ;
144+ }
145+ }
146+
147+ /**
148+ * Start the timer with battery-optimized refresh rate
149+ */
150+ function startTimer ( ) {
151+ if ( remainingTime > 0 && ! settings . running ) {
152+ settings . running = true ;
153+ settings . startTime = Date . now ( ) ;
154+ saveSettings ( ) ;
155+ if ( ! interval ) {
156+ // Use different intervals based on remaining time for battery optimization
157+ var intervalTime = remainingTime <= 60 ? COUNTDOWN_INTERVAL_FINAL : COUNTDOWN_INTERVAL_NORMAL ;
158+ interval = setInterval ( countdown , intervalTime ) ;
159+ }
160+ }
161+ }
162+
163+ /**
164+ * Adjust timer by specified number of seconds
165+ * @param {number } seconds - Positive or negative adjustment in seconds
166+ */
167+ function adjustTimer ( seconds ) {
168+ if ( settings . running ) {
169+ // For running timer, adjust both total time and remaining time
170+ settings . totalTime = Math . max ( 0 , settings . totalTime + seconds ) ;
171+ remainingTime = Math . max ( 0 , remainingTime + seconds ) ;
172+
173+ // If remaining time becomes 0 or negative, stop the timer
174+ if ( remainingTime <= 0 ) {
175+ settings . running = false ;
176+ remainingTime = 0 ;
177+ if ( interval ) {
178+ clearInterval ( interval ) ;
179+ interval = 0 ;
180+ }
181+ // Provide feedback if timer finished due to negative adjustment
182+ if ( remainingTime === 0 ) {
183+ buzzMultiple ( ) ;
184+ }
185+ }
186+ } else {
187+ // Adjust stopped timer
188+ settings . totalTime = Math . max ( 0 , settings . totalTime + seconds ) ;
189+ remainingTime = settings . totalTime ;
190+
191+ }
192+
193+ saveSettings ( ) ;
194+ WIDGETS [ "widtimer" ] . draw ( ) ;
195+ }
196+
197+ // =============================================================================
198+ // GESTURE CONTROL SYSTEM
199+ // =============================================================================
200+
201+ // Gesture state variables
202+ var drag = null ;
203+ var lastSwipeTime = 0 ;
204+ var lastSwipeDirection = null ;
205+ var isControlLocked = true ;
206+
207+ /**
208+ * Reset gesture controls to locked state
209+ */
210+ function resetUnlock ( ) {
211+ isControlLocked = true ;
212+ WIDGETS [ "widtimer" ] . draw ( ) ;
213+ }
214+
215+ function isHorizontal ( direction ) {
216+ return ( direction == DIRECTION_LEFT ) || ( direction == DIRECTION_RIGHT )
217+ }
218+
219+ function isVertical ( direction ) {
220+ return ( direction == DIRECTION_UP ) || ( direction == DIRECTION_DOWN )
221+ }
222+
223+ function isUnlockGesture ( first_direction , second_direction ) {
224+ return ( isHorizontal ( first_direction ) && isVertical ( second_direction )
225+ || isVertical ( first_direction ) && isHorizontal ( second_direction ) )
226+ }
227+
228+ /**
229+ * Set up gesture handlers with double-swipe protection against accidental activation
230+ */
231+ function setupGestures ( ) {
232+ Bangle . on ( "drag" , function ( e ) {
233+ if ( ! drag ) {
234+ // Start tracking drag gesture
235+ drag = { x : e . x , y : e . y } ;
236+ } else if ( ! e . b ) {
237+ // Drag gesture completed
238+ var dx = e . x - drag . x ;
239+ var dy = e . y - drag . y ;
240+ drag = null ;
241+
242+ // Only process significant gestures
243+ if ( Math . abs ( dx ) > 20 || Math . abs ( dy ) > 20 ) {
244+ var currentTime = Date . now ( ) ;
245+ var direction = null ;
246+ var adjustment = 0 ;
247+
248+ // Determine gesture direction and timer adjustment
249+ if ( Math . abs ( dx ) > Math . abs ( dy ) + 10 ) {
250+ // Horizontal swipe detected
251+ if ( dx > 0 ) {
252+ direction = 'right' ;
253+ adjustment = ONE_MINUTE ;
254+ } else {
255+ direction = 'left' ;
256+ adjustment = - ONE_MINUTE ;
257+ }
258+ } else if ( Math . abs ( dy ) > Math . abs ( dx ) + 10 ) {
259+ // Vertical swipe detected
260+ if ( dy > 0 ) {
261+ direction = 'down' ;
262+ adjustment = - TEN_MINUTES ;
263+ } else {
264+ direction = 'up' ;
265+ adjustment = TEN_MINUTES ;
266+ }
267+ }
268+
269+ if ( direction ) {
270+ // Process gesture based on lock state
271+ if ( ! isControlLocked ) {
272+ // Controls unlocked - execute adjustment immediately
273+ adjustTimer ( adjustment ) ;
274+ } else if ( isUnlockGesture ( direction , lastSwipeDirection ) &&
275+ currentTime - lastSwipeTime < UNLOCK_GESTURE_TIMEOUT ) {
276+ // Double swipe detected - unlock controls and execute
277+ isControlLocked = false ;
278+ // adjustTimer(adjustment);
279+ Bangle . buzz ( 50 ) ; // Provide unlock feedback
280+
281+ // Auto-start if time > 0
282+ if ( settings . totalTime > 0 ) {
283+ startTimer ( ) ;
284+ }
285+
286+ // Auto-lock after `UNLOCK_CONTROL_TIMEOUT` seconds of inactivity
287+ setTimeout ( resetUnlock , UNLOCK_CONTROL_TIMEOUT ) ;
288+ }
289+
290+ // Update gesture tracking state
291+ lastSwipeDirection = direction ;
292+ lastSwipeTime = currentTime ;
293+ }
294+ }
295+ }
296+ } ) ;
297+ }
298+
299+ // =============================================================================
300+ // WIDGET DEFINITION
301+ // =============================================================================
302+
303+ /**
304+ * Main widget object following BangleJS widget conventions
305+ */
306+ WIDGETS [ "widtimer" ] = {
307+ area : "tl" ,
308+ width : 58 , // Optimized width for vector font display
309+
310+ /**
311+ * Draw the widget with current timer state and visual feedback
312+ */
313+ draw : function ( ) {
314+ g . reset ( ) ;
315+ g . setFontAlign ( 0 , 0 ) ;
316+ g . clearRect ( this . x , this . y , this . x + this . width , this . y + 23 ) ;
317+
318+ // Use vector font for crisp, scalable display
319+ g . setFont ( "Vector" , 16 ) ;
320+ var timeStr = formatTime ( remainingTime ) ;
321+
322+ // Set color based on current timer state
323+ if ( settings . running && remainingTime > 0 ) {
324+ g . setColor ( "#ffff00" ) ; // Yellow when running (visible on colored backgrounds)
325+ } else if ( remainingTime === 0 ) {
326+ g . setColor ( "#ff0000" ) ; // Red when finished
327+ } else if ( ! isControlLocked ) {
328+ g . setColor ( "#00ff88" ) ; // Light green when controls unlocked
329+ } else {
330+ g . setColor ( "#ffffff" ) ; // White when stopped/locked
331+ }
332+
333+ g . drawString ( timeStr , this . x + this . width / 2 , this . y + 12 ) ;
334+ g . setColor ( "#ffffff" ) ; // Reset graphics color
335+ } ,
336+
337+ /**
338+ * Reload widget state from storage and restart timer if needed
339+ */
340+ reload : function ( ) {
341+ loadSettings ( ) ;
342+
343+ // Clear any existing countdown interval
344+ if ( interval ) {
345+ clearInterval ( interval ) ;
346+ interval = 0 ;
347+ }
348+
349+ // Restart countdown if timer was previously running
350+ if ( settings . running && remainingTime > 0 ) {
351+ var intervalTime = remainingTime <= 60 ? COUNTDOWN_INTERVAL_FINAL : COUNTDOWN_INTERVAL_NORMAL ;
352+ interval = setInterval ( countdown , intervalTime ) ;
353+ }
354+
355+ this . draw ( ) ;
356+ }
357+ } ;
358+
359+ // =============================================================================
360+ // INITIALIZATION
361+ // =============================================================================
362+
363+ // Initialize widget and set up gesture handlers
364+ WIDGETS [ "widtimer" ] . reload ( ) ;
365+ setupGestures ( ) ;
366+ } ) ( ) ;
0 commit comments