Skip to content

Commit 4282e53

Browse files
authored
Merge pull request #41 from Bruno-366/copilot/fix-40
Add LiSS workout duration timer functionality with integrated countdown display and enhanced control buttons
2 parents cd236ea + 323e179 commit 4282e53

File tree

5 files changed

+209
-1
lines changed

5 files changed

+209
-1
lines changed

β€Žsrc/App.svelteβ€Ž

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,14 @@
9797
workoutType: null,
9898
phase: 'initial',
9999
startTime: 0
100+
},
101+
lissTimer: {
102+
isActive: false,
103+
isPaused: false,
104+
timeLeft: 0,
105+
totalTime: 0,
106+
startTime: 0,
107+
pausedTime: 0
100108
}
101109
}))
102110
}

β€Žsrc/components/CardioWorkouts.svelteβ€Ž

Lines changed: 169 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
<script lang="ts">
22
import type { Workout, CardioWorkout } from '../types'
3+
import { Play, Pause, Square } from 'lucide-svelte'
4+
import { showRestCompleteNotification } from '../utils'
5+
import { uiStore } from '../stores'
36
47
interface CardioWorkoutsProps {
58
workout: Workout
@@ -28,6 +31,140 @@
2831
const type = cardioWorkout().type
2932
return CARDIO_WORKOUT_CONFIGS[type as keyof typeof CARDIO_WORKOUT_CONFIGS]
3033
})
34+
35+
// LiSS Timer functionality
36+
const lissTimer = $derived($uiStore.lissTimer)
37+
38+
// Update store function
39+
const updateLissTimer = (updates: Partial<typeof lissTimer>) => {
40+
uiStore.update(state => ({
41+
...state,
42+
lissTimer: { ...state.lissTimer, ...updates }
43+
}))
44+
}
45+
46+
// Start LiSS timer function
47+
const startLissTimer = () => {
48+
const duration = cardioWorkout().duration as number
49+
const durationInSeconds = duration * 60
50+
const now = Date.now()
51+
52+
if (lissTimer.isPaused) {
53+
// Resume from pause
54+
updateLissTimer({
55+
isActive: true,
56+
isPaused: false,
57+
startTime: now - (lissTimer.totalTime - lissTimer.timeLeft) * 1000
58+
})
59+
} else {
60+
// Start fresh
61+
updateLissTimer({
62+
isActive: true,
63+
isPaused: false,
64+
timeLeft: durationInSeconds,
65+
totalTime: durationInSeconds,
66+
startTime: now,
67+
pausedTime: 0
68+
})
69+
}
70+
}
71+
72+
// Pause LiSS timer function
73+
const pauseLissTimer = () => {
74+
updateLissTimer({
75+
isActive: false,
76+
isPaused: true,
77+
pausedTime: Date.now()
78+
})
79+
}
80+
81+
// Stop LiSS timer function
82+
const stopLissTimer = () => {
83+
updateLissTimer({
84+
isActive: false,
85+
isPaused: false,
86+
timeLeft: 0,
87+
totalTime: 0,
88+
startTime: 0,
89+
pausedTime: 0
90+
})
91+
}
92+
93+
// Timer interval effect - timestamp-based to prevent background throttling
94+
$effect(() => {
95+
let interval: number | undefined
96+
97+
if (lissTimer.isActive && lissTimer.timeLeft > 0) {
98+
interval = setInterval(() => {
99+
const now = Date.now()
100+
const elapsedSeconds = Math.floor((now - lissTimer.startTime) / 1000)
101+
const newTimeLeft = Math.max(0, lissTimer.totalTime - elapsedSeconds)
102+
103+
updateLissTimer({ timeLeft: newTimeLeft })
104+
}, 100) // Check more frequently for smoother updates
105+
}
106+
107+
return () => {
108+
if (interval) clearInterval(interval)
109+
}
110+
})
111+
112+
// Separate effect to handle notification when timer reaches 0
113+
$effect(() => {
114+
if (lissTimer.isActive && lissTimer.timeLeft === 0) {
115+
showRestCompleteNotification()
116+
// Auto-pause when complete
117+
updateLissTimer({ isActive: false, isPaused: false })
118+
}
119+
})
120+
121+
// Derived timer display values
122+
const displayDuration = $derived(() => {
123+
if (workout.type === 'liss' && typeof cardioWorkout().duration === 'number') {
124+
if (lissTimer.isActive || lissTimer.isPaused || lissTimer.timeLeft > 0) {
125+
// Show countdown timer
126+
const minutes = Math.floor(lissTimer.timeLeft / 60)
127+
const seconds = lissTimer.timeLeft % 60
128+
return `${minutes}:${seconds.toString().padStart(2, '0')}`
129+
} else {
130+
// Show initial duration
131+
return cardioWorkout().duration
132+
}
133+
}
134+
return cardioWorkout().duration
135+
})
136+
137+
const showTimerControls = $derived(() => {
138+
return workout.type === 'liss' && typeof cardioWorkout().duration === 'number'
139+
})
140+
141+
// Derived button states for conditional styling
142+
const buttonStates = $derived(() => {
143+
const isStartDisabled = lissTimer.isActive
144+
const isPauseDisabled = !lissTimer.isActive
145+
const isStopDisabled = !lissTimer.isActive && !lissTimer.isPaused
146+
147+
return {
148+
start: {
149+
disabled: isStartDisabled,
150+
class: isStartDisabled
151+
? 'flex-1 bg-green-500 text-white font-semibold py-3 rounded-lg transition-all flex items-center justify-center gap-2 opacity-40 cursor-not-allowed'
152+
: 'flex-1 bg-green-500 hover:bg-green-600 text-white font-semibold py-3 rounded-lg transition-all flex items-center justify-center gap-2 opacity-100'
153+
},
154+
pause: {
155+
disabled: isPauseDisabled,
156+
class: isPauseDisabled
157+
? 'flex-1 bg-yellow-500 text-white font-semibold py-3 rounded-lg transition-all flex items-center justify-center gap-2 opacity-40 cursor-not-allowed'
158+
: 'flex-1 bg-yellow-500 hover:bg-yellow-600 text-white font-semibold py-3 rounded-lg transition-all flex items-center justify-center gap-2 opacity-100'
159+
},
160+
stop: {
161+
disabled: isStopDisabled,
162+
class: isStopDisabled
163+
? 'flex-1 bg-red-500 text-white font-semibold py-3 rounded-lg transition-all flex items-center justify-center gap-2 opacity-40 cursor-not-allowed'
164+
: 'flex-1 bg-red-500 hover:bg-red-600 text-white font-semibold py-3 rounded-lg transition-all flex items-center justify-center gap-2 opacity-100'
165+
}
166+
}
167+
})
31168
</script>
32169

33170
{#if workout.type === 'liss' || workout.type === 'hiit'}
@@ -37,12 +174,43 @@
37174
{cardioWorkout().activity}
38175
</h3>
39176
{#if cardioWorkout().duration}
40-
<div class="text-4xl font-bold">{cardioWorkout().duration}</div>
177+
<div class="text-4xl font-bold">{displayDuration()}</div>
41178
{#if typeof cardioWorkout().duration === 'number'}
42179
<div class="text-sm opacity-90 mt-1">minutes</div>
43180
{/if}
44181
{/if}
45182
</div>
183+
184+
{#if showTimerControls()}
185+
<!-- Timer control buttons -->
186+
<div class="flex gap-2 mt-4">
187+
<button
188+
onclick={buttonStates().start.disabled ? undefined : startLissTimer}
189+
disabled={buttonStates().start.disabled}
190+
class={buttonStates().start.class}
191+
>
192+
<Play class="w-5 h-5" stroke="none" fill="currentColor" />
193+
Start
194+
</button>
195+
<button
196+
onclick={buttonStates().pause.disabled ? undefined : pauseLissTimer}
197+
disabled={buttonStates().pause.disabled}
198+
class={buttonStates().pause.class}
199+
>
200+
<Pause class="w-5 h-5" stroke="none" fill="currentColor" />
201+
Pause
202+
</button>
203+
<button
204+
onclick={buttonStates().stop.disabled ? undefined : stopLissTimer}
205+
disabled={buttonStates().stop.disabled}
206+
class={buttonStates().stop.class}
207+
>
208+
<Square class="w-5 h-5" stroke="none" fill="currentColor" />
209+
Stop
210+
</button>
211+
</div>
212+
{/if}
213+
46214
<button
47215
onclick={onCompleteWorkout}
48216
class="w-full bg-green-500 hover:bg-green-600 text-white font-semibold py-3 rounded-lg transition-colors mt-4"

β€Žsrc/components/ResetProgress.svelteβ€Ž

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,14 @@
3535
workoutType: null,
3636
phase: 'initial',
3737
startTime: 0
38+
},
39+
lissTimer: {
40+
isActive: false,
41+
isPaused: false,
42+
timeLeft: 0,
43+
totalTime: 0,
44+
startTime: 0,
45+
pausedTime: 0
3846
}
3947
})
4048

β€Žsrc/stores.tsβ€Ž

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,14 @@ interface UIState {
1414
phase: 'initial' | 'extended'
1515
startTime: number
1616
}
17+
lissTimer: {
18+
isActive: boolean
19+
isPaused: boolean
20+
timeLeft: number
21+
totalTime: number
22+
startTime: number
23+
pausedTime: number
24+
}
1725
}
1826

1927
interface WorkoutState {
@@ -167,6 +175,14 @@ const defaultUIState: UIState = {
167175
workoutType: null,
168176
phase: 'initial',
169177
startTime: 0
178+
},
179+
lissTimer: {
180+
isActive: false,
181+
isPaused: false,
182+
timeLeft: 0,
183+
totalTime: 0,
184+
startTime: 0,
185+
pausedTime: 0
170186
}
171187
}
172188

β€Žsrc/types.tsβ€Ž

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,14 @@ export interface AppState {
6767
phase: 'initial' | 'extended';
6868
startTime: number;
6969
};
70+
lissTimer: {
71+
isActive: boolean;
72+
isPaused: boolean;
73+
timeLeft: number;
74+
totalTime: number;
75+
startTime: number;
76+
pausedTime: number;
77+
};
7078
}
7179

7280
export interface WarmupSet {

0 commit comments

Comments
Β (0)