1+ // === Utility Functions ===
2+ function formatTime ( seconds ) {
3+ let mins = Math . floor ( seconds / 60 ) ;
4+ let secs = ( seconds % 60 ) . toString ( ) . padStart ( 2 , '0' ) ;
5+ return `${ mins } :${ secs } ` ;
6+ }
7+
8+ function getTimeStr ( ) {
9+ let d = new Date ( ) ;
10+ return `${ d . getHours ( ) . toString ( ) . padStart ( 2 , '0' ) } :${ d . getMinutes ( ) . toString ( ) . padStart ( 2 , '0' ) } ` ;
11+ }
12+
13+ function updateCachedLeftTime ( ) {
14+ cachedLeftTime = "Left: " + formatTime ( state . remainingTotal ) ;
15+ }
16+
17+ // === Constants ===
18+ const FILE = "jwalk.json" ;
19+ const DEFAULTS = {
20+ totalDuration : 30 ,
21+ intervalDuration : 3 ,
22+ startMode : 0 ,
23+ modeBuzzerDuration : 1000 ,
24+ finishBuzzerDuration : 1500 ,
25+ showClock : 1 ,
26+ updateWhileLocked : 0
27+ } ;
28+
29+ // === Settings and State ===
30+ let settings = require ( "Storage" ) . readJSON ( FILE , 1 ) || DEFAULTS ;
31+
32+ let state = {
33+ remainingTotal : settings . totalDuration * 60 ,
34+ intervalDuration : settings . intervalDuration * 60 ,
35+ remainingInterval : 0 ,
36+ intervalEnd : 0 ,
37+ paused : false ,
38+ currentMode : settings . startMode === 1 ? "Intense" : "Relax" ,
39+ finished : false ,
40+ forceDraw : false ,
41+ } ;
42+
43+ let cachedLeftTime = "" ;
44+ let lastMinuteStr = getTimeStr ( ) ;
45+ let drawTimerInterval ;
46+
47+ // === UI Rendering ===
48+ function drawUI ( ) {
49+ let y = Bangle . appRect . y + 8 ;
50+ g . reset ( ) . setBgColor ( g . theme . bg ) . clearRect ( Bangle . appRect ) ;
51+ g . setColor ( g . theme . fg ) ;
52+
53+ let displayInterval = state . paused
54+ ? state . remainingInterval
55+ : Math . max ( 0 , Math . floor ( ( state . intervalEnd - Date . now ( ) ) / 1000 ) ) ;
56+
57+ g . setFont ( "Vector" , 40 ) ;
58+ g . setFontAlign ( 0 , 0 ) ;
59+ g . drawString ( formatTime ( displayInterval ) , g . getWidth ( ) / 2 , y + 70 ) ;
60+
61+ let cy = y + 100 ;
62+ if ( state . paused ) {
63+ g . setFont ( "Vector" , 15 ) ;
64+ g . drawString ( "PAUSED" , g . getWidth ( ) / 2 , cy ) ;
65+ } else {
66+ let cx = g . getWidth ( ) / 2 ;
67+ g . setColor ( g . theme . accent || g . theme . fg2 || g . theme . fg ) ;
68+ if ( state . currentMode === "Relax" ) {
69+ g . fillCircle ( cx , cy , 5 ) ;
70+ } else {
71+ g . fillPoly ( [
72+ cx , cy - 6 ,
73+ cx - 6 , cy + 6 ,
74+ cx + 6 , cy + 6
75+ ] ) ;
76+ }
77+ g . setColor ( g . theme . fg ) ;
78+ }
79+
80+ g . setFont ( "6x8" , 2 ) ;
81+ g . setFontAlign ( 0 , - 1 ) ;
82+ g . drawString ( state . currentMode , g . getWidth ( ) / 2 , y + 15 ) ;
83+ g . drawString ( cachedLeftTime , g . getWidth ( ) / 2 , cy + 15 ) ;
84+
85+ if ( settings . showClock ) {
86+ g . setFontAlign ( 1 , 0 ) ;
87+ g . drawString ( lastMinuteStr , g . getWidth ( ) - 4 , y ) ;
88+ }
89+ g . flip ( ) ;
90+ }
91+
92+ // === Workout Logic ===
93+ function toggleMode ( ) {
94+ state . currentMode = state . currentMode === "Relax" ? "Intense" : "Relax" ;
95+ Bangle . buzz ( settings . modeBuzzerDuration ) ;
96+ state . forceDraw = true ;
97+ }
98+
99+ function startNextInterval ( ) {
100+ if ( state . remainingTotal <= 0 ) {
101+ finishWorkout ( ) ;
102+ return ;
103+ }
104+
105+ state . remainingInterval = Math . min ( state . intervalDuration , state . remainingTotal ) ;
106+ state . remainingTotal -= state . remainingInterval ;
107+ updateCachedLeftTime ( ) ;
108+ state . intervalEnd = Date . now ( ) + state . remainingInterval * 1000 ;
109+ state . forceDraw = true ;
110+ }
111+
112+ function togglePause ( ) {
113+ if ( state . finished ) return ;
114+
115+ if ( ! state . paused ) {
116+ state . remainingInterval = Math . max ( 0 , Math . floor ( ( state . intervalEnd - Date . now ( ) ) / 1000 ) ) ;
117+ state . paused = true ;
118+ } else {
119+ state . intervalEnd = Date . now ( ) + state . remainingInterval * 1000 ;
120+ state . paused = false ;
121+ }
122+ drawUI ( ) ;
123+ }
124+
125+ function finishWorkout ( ) {
126+ clearInterval ( drawTimerInterval ) ;
127+ Bangle . buzz ( settings . finishBuzzerDuration ) ;
128+ state . finished = true ;
129+
130+ setTimeout ( ( ) => {
131+ g . clear ( ) ;
132+ g . setFont ( "Vector" , 30 ) ;
133+ g . setFontAlign ( 0 , 0 ) ;
134+ g . drawString ( "Well done!" , g . getWidth ( ) / 2 , g . getHeight ( ) / 2 ) ;
135+ g . flip ( ) ;
136+
137+ const exitHandler = ( ) => {
138+ Bangle . removeListener ( "touch" , exitHandler ) ;
139+ Bangle . removeListener ( "btn1" , exitHandler ) ;
140+ load ( ) ; // Exit app
141+ } ;
142+
143+ Bangle . on ( "touch" , exitHandler ) ;
144+ setWatch ( exitHandler , BTN1 , { repeat : false } ) ;
145+ } , 500 ) ;
146+ }
147+
148+ // === Timer Tick ===
149+ function tick ( ) {
150+ if ( state . finished ) return ;
151+
152+ const currentMinuteStr = getTimeStr ( ) ;
153+ if ( currentMinuteStr !== lastMinuteStr ) {
154+ lastMinuteStr = currentMinuteStr ;
155+ state . forceDraw = true ;
156+ }
157+
158+ if ( ! state . paused && ( state . intervalEnd - Date . now ( ) ) / 1000 <= 0 ) {
159+ toggleMode ( ) ;
160+ startNextInterval ( ) ;
161+ return ;
162+ }
163+
164+ if ( state . forceDraw || settings . updateWhileLocked || ! Bangle . isLocked ( ) ) {
165+ drawUI ( ) ;
166+ state . forceDraw = false ;
167+ }
168+ }
169+
170+ // === Initialization ===
171+ Bangle . on ( "touch" , togglePause ) ;
172+ Bangle . loadWidgets ( ) ;
173+ Bangle . drawWidgets ( ) ;
174+
175+ updateCachedLeftTime ( ) ;
176+ startNextInterval ( ) ;
177+ drawUI ( ) ;
178+ drawTimerInterval = setInterval ( tick , 1000 ) ;
0 commit comments