An app for logging board climbs
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: update timer

+91 -27
+1 -1
deno.json
··· 1 1 { 2 - "version": "1.0.0", 2 + "version": "1.1.0", 3 3 "compilerOptions": { 4 4 "lib": [ 5 5 "deno.ns",
+2 -2
www/index.ts
··· 4 4 import './routes/library.ts' 5 5 import './routes/climb.ts' 6 6 import './routes/settings.ts' 7 - import { globalStopwatch } from './routes/stopwatch.ts' 7 + import { getRestTime, globalStopwatch } from './routes/stopwatch.ts' 8 8 import { formatStopwatchShort } from './utils/format.ts' 9 9 10 10 client.init() ··· 82 82 globalStopwatch.addEventListener((state) => { 83 83 const isRunning = state.isStarted && !state.isPaused 84 84 if (isRunning) { 85 - label.textContent = formatStopwatchShort(state.elapsed) 85 + label.textContent = formatStopwatchShort(getRestTime(state)) 86 86 label.classList.add('sw-running') 87 87 } else { 88 88 label.textContent = 'Stopwatch'
+3 -1
www/routes/climb.ts
··· 10 10 import { CANVAS_WIDTH, canvasHeight, drawClimb } from '../utils/draw.ts' 11 11 import { BOARD_HOLDS, MB_KEYS } from '../utils/boards/mod.ts' 12 12 import app from '../models/app.ts' 13 + import { markAttempt } from './stopwatch.ts' 13 14 14 15 export let activeClimbHeader: ClimbHeader | null = null 15 16 ··· 247 248 </a> 248 249 <div class="lb-log-section"> 249 250 <button class="action" @click="${() => this.showLogDialog(climb)}"> 250 - ${app.getClimbLog(climb.id) ? 'Log Another Session' : 'Log Attempt'} 251 + Log Attempts 251 252 </button> 252 253 </div> 253 254 </div> ··· 358 359 const climb = this.currentDialogClimb 359 360 const rating = this.dialogRating 360 361 this.hideLogDialog() 362 + markAttempt() 361 363 void app.logSession( 362 364 { 363 365 id: climb.id,
+59 -20
www/routes/stopwatch.ts
··· 3 3 import { formatStopwatchShort } from '../utils/format.ts' 4 4 import app from '../models/app.ts' 5 5 6 + const AUTO_RESET_MS = 20 * 60 * 60 * 1000 // 20 hours 7 + 6 8 // Wraps the inner Stopwatch so that index.ts subscriptions survive restoration, 7 9 // and so elapsed offsets (from a persisted running timer) are applied uniformly 8 10 // to all consumers without them needing to know about the offset. ··· 78 80 }), 79 81 ) 80 82 81 - // Restore a running timer on app load, before the user navigates to the route 82 - app.settings.addEventListener(() => { 83 + // Restore a running timer on app load, before the user navigates to the route. 84 + // Runs once immediately (so a plain page refresh restores from localStorage) 85 + // and again on any later settings update (e.g. sync pulling new state). 86 + function restoreFromSettings(): void { 83 87 if (globalStopwatch.state.isStarted) return 84 88 const runningTimer = app.settings.state.runningTimer 85 89 if (!runningTimer) return 86 90 const elapsed = Date.now() - new Date(runningTimer).getTime() 91 + if (elapsed >= AUTO_RESET_MS) { 92 + globalStopwatch.reset() 93 + app.setStopwatch(null) 94 + return 95 + } 87 96 globalStopwatch.restore(elapsed) 88 97 globalStopwatch.start() 89 - }) 98 + } 99 + 100 + restoreFromSettings() 101 + app.settings.addEventListener(restoreFromSettings) 102 + 103 + // Returns ms since the last lap, or total elapsed if no laps have been logged. 104 + export function getRestTime(state: StopwatchState): number { 105 + const last = state.laps[state.laps.length - 1] 106 + return last ? state.elapsed - last.total : state.elapsed 107 + } 108 + 109 + // Starts the stopwatch and persists the effective start time to settings. 110 + export function startStopwatch(): void { 111 + globalStopwatch.start() 112 + const effectiveStart = new Date( 113 + Date.now() - globalStopwatch.state.elapsed, 114 + ).toISOString() 115 + app.setStopwatch(effectiveStart) 116 + } 117 + 118 + // Called when logging an attempt: start from reset, lap if running, no-op if paused. 119 + export function markAttempt(): void { 120 + const { isStarted, isPaused } = globalStopwatch.state 121 + if (!isStarted) { 122 + startStopwatch() 123 + } else if (!isPaused) { 124 + globalStopwatch.lap() 125 + } 126 + } 90 127 91 128 export class StopwatchPage extends LitElement { 92 129 private timeElement: HTMLElement | null = null 130 + private restElement: HTMLElement | null = null 93 131 private startButton: HTMLButtonElement | null = null 94 132 private resetButton: HTMLButtonElement | null = null 95 - private lapButton: HTMLButtonElement | null = null 96 133 private lapsContainer: HTMLElement | null = null 97 134 private removeListener: (() => void) | null = null 98 135 ··· 115 152 116 153 protected override firstUpdated() { 117 154 this.timeElement = this.querySelector('#stopwatch-time') 155 + this.restElement = this.querySelector('#stopwatch-rest') 118 156 this.startButton = this.querySelector('#start-button') 119 157 this.resetButton = this.querySelector('#reset-button') 120 - this.lapButton = this.querySelector('#lap-button') 121 158 this.lapsContainer = this.querySelector('#laps-container') 122 159 this.bindEvents() 123 160 this.updateUI(globalStopwatch.state) ··· 127 164 return html` 128 165 <div id="stopwatch-display"> 129 166 <div id="stopwatch-time">0:00</div> 167 + <div id="stopwatch-rest" class="stopwatch-rest" hidden> 168 + <span class="stopwatch-rest-label">Rest</span> 169 + <span class="stopwatch-rest-value">0:00</span> 170 + </div> 130 171 </div> 131 172 <div id="stopwatch-controls"> 132 173 <button id="start-button" class="primary">Start</button> 133 - <button id="lap-button" disabled>Lap</button> 134 174 <button id="reset-button">Reset</button> 135 175 </div> 136 176 <div id="laps-container"></div> ··· 144 184 globalStopwatch.pause() 145 185 app.setStopwatch(null) 146 186 } else { 147 - globalStopwatch.start() 148 - const effectiveStart = new Date( 149 - Date.now() - globalStopwatch.state.elapsed, 150 - ).toISOString() 151 - app.setStopwatch(effectiveStart) 187 + startStopwatch() 152 188 } 153 189 }) 154 190 ··· 156 192 globalStopwatch.reset() 157 193 app.setStopwatch(null) 158 194 }) 159 - 160 - this.lapButton?.addEventListener('click', () => { 161 - globalStopwatch.lap() 162 - }) 163 195 } 164 196 165 197 private updateUI(state: StopwatchState): void { ··· 169 201 this.timeElement.textContent = state.display 170 202 } 171 203 204 + if (this.restElement) { 205 + const showRest = state.isStarted 206 + this.restElement.hidden = !showRest 207 + if (showRest) { 208 + const valueEl = this.restElement.querySelector('.stopwatch-rest-value') 209 + if (valueEl) { 210 + valueEl.textContent = formatStopwatchShort(getRestTime(state)) 211 + } 212 + } 213 + } 214 + 172 215 if (this.startButton) { 173 216 this.startButton.textContent = isRunning ? 'Pause' : 'Start' 174 - } 175 - 176 - if (this.lapButton) { 177 - this.lapButton.disabled = !isRunning 178 217 } 179 218 180 219 this.renderLaps(state.laps) ··· 193 232 const lapNumber = laps.length - index 194 233 return ` 195 234 <div class="lap-item"> 196 - <span class="lap-number">Lap ${lapNumber}</span> 235 + <span class="lap-number">Attempt ${lapNumber}</span> 197 236 <span class="lap-time">${lap.splitDisplay}</span> 198 237 </div> 199 238 `
+25 -2
www/static/theme.css
··· 818 818 border: none; 819 819 padding: 0; 820 820 background: transparent; 821 - max-width: calc(100vw - 2 * var(--s3)); 822 821 } 823 822 824 823 #cp-dialog::backdrop { ··· 830 829 border: 1px solid currentColor; 831 830 border-radius: var(--br-lg); 832 831 padding: var(--s4); 833 - width: min(340px, 100%); 834 832 display: flex; 835 833 flex-direction: column; 836 834 gap: var(--s3); ··· 968 966 font-variant-numeric: tabular-nums; 969 967 line-height: 1; 970 968 letter-spacing: -0.02em; 969 + } 970 + 971 + .stopwatch-rest { 972 + margin-top: var(--s3); 973 + display: inline-flex; 974 + align-items: baseline; 975 + gap: var(--s2); 976 + font-variant-numeric: tabular-nums; 977 + opacity: 0.7; 978 + } 979 + 980 + .stopwatch-rest[hidden] { 981 + display: none; 982 + } 983 + 984 + .stopwatch-rest-label { 985 + font-size: var(--f6); 986 + font-weight: var(--fw-semibold); 987 + text-transform: uppercase; 988 + letter-spacing: 0.08em; 989 + } 990 + 991 + .stopwatch-rest-value { 992 + font-size: var(--f3); 993 + font-weight: var(--fw-medium); 971 994 } 972 995 973 996 #stopwatch-controls {
+1 -1
www/utils/draw.ts
··· 77 77 ] 78 78 79 79 if (holdLookup) { 80 - const HOLD_SIZE = 47 80 + const HOLD_SIZE = 60 81 81 // Load all hold images in parallel 82 82 const allPositions = [...holdLookup.values()] 83 83 const allImages = await Promise.all(