|
1 | 1 | <script lang="ts">
|
2 | 2 | import type { Workout, CardioWorkout } from '../types'
|
| 3 | + import { Play, Pause, Square } from 'lucide-svelte' |
| 4 | + import { showRestCompleteNotification } from '../utils' |
| 5 | + import { uiStore } from '../stores' |
3 | 6 |
|
4 | 7 | interface CardioWorkoutsProps {
|
5 | 8 | workout: Workout
|
|
28 | 31 | const type = cardioWorkout().type
|
29 | 32 | return CARDIO_WORKOUT_CONFIGS[type as keyof typeof CARDIO_WORKOUT_CONFIGS]
|
30 | 33 | })
|
| 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 | + }) |
31 | 168 | </script>
|
32 | 169 |
|
33 | 170 | {#if workout.type === 'liss' || workout.type === 'hiit'}
|
|
37 | 174 | {cardioWorkout().activity}
|
38 | 175 | </h3>
|
39 | 176 | {#if cardioWorkout().duration}
|
40 |
| - <div class="text-4xl font-bold">{cardioWorkout().duration}</div> |
| 177 | + <div class="text-4xl font-bold">{displayDuration()}</div> |
41 | 178 | {#if typeof cardioWorkout().duration === 'number'}
|
42 | 179 | <div class="text-sm opacity-90 mt-1">minutes</div>
|
43 | 180 | {/if}
|
44 | 181 | {/if}
|
45 | 182 | </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 | + |
46 | 214 | <button
|
47 | 215 | onclick={onCompleteWorkout}
|
48 | 216 | class="w-full bg-green-500 hover:bg-green-600 text-white font-semibold py-3 rounded-lg transition-colors mt-4"
|
|
0 commit comments