From c7011b706fc7779a10340cfda15e369e81bc80c3 Mon Sep 17 00:00:00 2001 From: Flatlogic Bot Date: Fri, 6 Mar 2026 18:43:12 +0000 Subject: [PATCH] Autosave: 20260306-184312 --- assets/js/main.js | 173 ++++++++++++++++++++++++++++++++++++---------- index.php | 15 ++-- 2 files changed, 145 insertions(+), 43 deletions(-) diff --git a/assets/js/main.js b/assets/js/main.js index 8d097cf..45bb0c7 100644 --- a/assets/js/main.js +++ b/assets/js/main.js @@ -108,6 +108,9 @@ const app = { savedTimersDropdown: document.getElementById('saved-timers-dropdown'), btnDeleteSaved: document.getElementById('btn-delete-saved'), + displayModeContainer: document.getElementById('display-mode-container'), + toggleDisplayMode: document.getElementById('toggle-display-mode'), + // Settings / Options optionsDropdown: document.getElementById('optionsDropdown'), optCountdownContainer: document.getElementById('opt-countdown-container'), @@ -137,6 +140,31 @@ const app = { this.renderCustomBuilder(); this.resetTimer(); this.loadSavedTimers(); + this.bindKeyboardShortcuts(); + }, + + bindKeyboardShortcuts() { + document.addEventListener('keydown', (e) => { + if (this.currentView !== 'timer' || !this.currentMode) return; + + // Do not trigger if user is typing in an input or textarea + if (['INPUT', 'TEXTAREA', 'SELECT'].includes(e.target.tagName)) { + // If it's the Escape key, we still want to stop the timer even if focused on input + if (e.key !== 'Escape') return; + } + + if (e.key === ' ' || e.code === 'Space') { + e.preventDefault(); + if (!this.isRunning || this.isPaused) { + this.handleStartClick(); + } else { + this.pauseTimer(); + } + } else if (e.key === 'Escape' || e.code === 'Escape') { + e.preventDefault(); + this.stopTimer(); + } + }); }, initDarkMode() { @@ -203,6 +231,8 @@ const app = { this.sessionTitles[this.currentMode] = e.target.value; } }); + + this.el.toggleDisplayMode.addEventListener('change', () => this.updateDisplay()); }, updateLandingClock() { @@ -244,7 +274,9 @@ const app = { this.el.btnPause.classList.toggle('d-none', !mode.allowPause); this.el.btnLap.classList.toggle('d-none', !mode.hasLaps); this.el.btnNext.classList.add('d-none'); // Removed from timer entirely for all modes - this.el.btnSaveMain.classList.toggle('d-none', !mode.canSave || mode.isCustom); + + // Update Save button visibility + this.updateSaveButtonVisibility(); this.el.timerSideColumn.classList.toggle('d-none', !mode.hasLaps && !mode.hasRelay && !mode.isCustom); @@ -253,11 +285,13 @@ const app = { this.el.historySection.classList.add('d-none'); this.el.savedTimersContainer.classList.remove('d-none'); this.el.relayConfig.classList.add('d-none'); + this.el.displayModeContainer.classList.remove('d-none'); } else if (mode.hasRelay) { this.el.customBuilder.classList.add('d-none'); this.el.relayConfig.classList.remove('d-none'); this.el.historySection.classList.remove('d-none'); this.el.savedTimersContainer.classList.add('d-none'); + this.el.displayModeContainer.classList.add('d-none'); // Move history to main column immediately for Relay (separate from participants) this.el.lapResultsContainer.classList.remove('d-none'); @@ -268,6 +302,7 @@ const app = { this.el.relayConfig.classList.add('d-none'); this.el.historySection.classList.remove('d-none'); this.el.savedTimersContainer.classList.toggle('d-none', !mode.canSave); + this.el.displayModeContainer.classList.add('d-none'); } this.el.countdownInputs.classList.toggle('d-none', modeKey !== 'countdown'); @@ -314,6 +349,18 @@ const app = { this.loadSavedTimers(); this.updateDisplay(); }, + + updateSaveButtonVisibility() { + const mode = this.modes[this.currentMode]; + if (!mode) return; + const hasBeenStarted = this.elapsedTime > 0 || this.isRunning || this.isPaused; + const canSave = mode.canSave && !mode.isCustom; + + // Save button appears after start and either paused or stopped + const shouldShowSave = canSave && hasBeenStarted && (this.isPaused || !this.isRunning); + + this.el.btnSaveMain.classList.toggle('d-none', !shouldShowSave); + }, renderParticipantInputs() { const count = Math.min(12, Math.max(1, parseInt(this.el.participantCount.value) || 1)); @@ -366,8 +413,8 @@ const app = { runPreStartCountdown(seconds) { this.isPreStarting = true; - this.el.btnStart.disabled = true; - this.el.btnReset.disabled = true; + this.el.btnStart.disabled = true; this.el.btnReset.classList.remove("d-none"); + this.el.btnReset.disabled = false; this.el.sessionTitle.disabled = true; let remaining = seconds; @@ -448,10 +495,10 @@ const app = { this.isPaused = false; this.lastPlayedSecond = -1; - this.el.btnStart.disabled = true; + this.el.btnStart.disabled = true; this.el.btnReset.classList.remove("d-none"); this.el.btnPause.disabled = false; this.el.btnStop.disabled = false; - this.el.btnReset.disabled = true; + this.el.btnReset.disabled = false; this.el.sessionTitle.disabled = true; this.el.mainTimer.classList.remove('timer-finish'); this.el.mainTimer.classList.remove('timer-finish-theme'); @@ -465,6 +512,9 @@ const app = { document.querySelectorAll('.participant-name-input').forEach(i => i.disabled = true); document.querySelectorAll('.participant-color-input').forEach(i => i.disabled = true); + // Update UI + this.updateSaveButtonVisibility(); + this.tick(); }, @@ -482,6 +532,9 @@ const app = { if (this.currentMode === 'custom') { this.updateCurrentActivityStatus('paused'); } + + // Update UI + this.updateSaveButtonVisibility(); }, stopTimer() { @@ -517,6 +570,9 @@ const app = { } else if (this.currentMode === 'custom' && wasRunning) { this.updateCurrentActivityStatus('Canceled'); } + + // Update UI + this.updateSaveButtonVisibility(); }, resetTimer() { @@ -534,7 +590,7 @@ const app = { this.el.mainTimer.classList.remove('timer-finish-theme'); this.el.mainTimer.classList.remove('timer-alert'); - this.el.btnReset.disabled = true; + this.el.btnReset.disabled = false; this.el.btnStart.disabled = false; this.el.btnPause.disabled = true; this.el.btnStop.disabled = true; @@ -573,7 +629,9 @@ const app = { } // Move history back to side column default - this.el.timerSideColumn.querySelector('.card-precise').appendChild(this.el.historySection); + if (this.el.timerSideColumn.querySelector('.card-precise')) { + this.el.timerSideColumn.querySelector('.card-precise').appendChild(this.el.historySection); + } this.el.lapResultsContainer.classList.add('d-none'); this.el.lapSort.classList.add('d-none'); @@ -589,6 +647,8 @@ const app = { this.el.lapSort.classList.remove('d-none'); } + // Update UI + this.updateSaveButtonVisibility(); this.updateDisplay(); }, @@ -646,8 +706,8 @@ const app = { nextActivity() { const isCountdownMode = this.el.settingIsCountdown.checked; - // Mark current as OK - this.updateCurrentActivityStatus('OK'); + // Mark current as completed + this.updateCurrentActivityStatus('completed'); this.currentActivityIndex++; @@ -715,20 +775,27 @@ const app = { const ms = this.elapsedTime; let format = this.el.formatSelect.value; - if (this.currentMode === 'time-watch') { - if (format === 'hh:mm:ss.ms') format = 'hh:mm:ss'; - if (format === 'seconds.ms') format = 'seconds'; - } - - this.el.mainTimer.textContent = this.formatTime(ms, format); - - // Only show sub-timer if format is not milliseconds as requested - if (format !== 'hh:mm:ss.ms') { - this.el.subTimer.classList.add('d-none'); + if (this.currentMode === 'custom' && this.el.toggleDisplayMode.checked) { + const totalMs = this.getTotalProgress(); + this.el.mainTimer.textContent = this.formatTime(totalMs, format); } else { - // If main format IS milliseconds, we don't need a sub-timer showing the same thing - this.el.subTimer.classList.add('d-none'); + this.el.mainTimer.textContent = this.formatTime(ms, format); } + this.el.subTimer.classList.add('d-none'); + }, + + getTotalProgress() { + let completedDuration = 0; + for (let i = 0; i < this.currentActivityIndex; i++) { + completedDuration += this.customActivities[i].duration; + } + let currentElapsed = 0; + if (this.el.settingIsCountdown.checked) { + currentElapsed = Math.max(0, this.countdownStartValue - this.elapsedTime); + } else { + currentElapsed = this.elapsedTime; + } + return completedDuration + currentElapsed; }, formatTime(ms, format) { @@ -736,7 +803,6 @@ const app = { let seconds = Math.floor(absMs / 1000); let minutes = Math.floor(seconds / 60); let hours = Math.floor(minutes / 60); - let milliseconds = Math.floor(absMs % 1000); seconds %= 60; minutes %= 60; @@ -745,7 +811,7 @@ const app = { switch (format) { case 'hh:mm:ss.ms': - return `${pad(hours)}:${pad(minutes)}:${pad(seconds)}.${pad(milliseconds, 3)}`; + return `${pad(hours)}:${pad(minutes)}:${pad(seconds)}.${pad(Math.floor((absMs % 1000) / 10), 2)}`; case 'hh:mm:ss': return `${pad(hours)}:${pad(minutes)}:${pad(seconds)}`; case 'hours': @@ -755,12 +821,21 @@ const app = { case 'seconds': return (ms / 1000).toFixed(0) + ' s'; case 'seconds.ms': - return (ms / 1000).toFixed(3) + ' s'; + return (ms / 1000).toFixed(2) + ' s'; default: - return '00:00:00.000'; + return '00:00:00'; } }, + parseTime(str) { + const parts = str.split(':').reverse(); + let ms = 0; + if (parts[0]) ms += (parseFloat(parts[0]) || 0) * 1000; // seconds + if (parts[1]) ms += (parseInt(parts[1]) || 0) * 60000; // minutes + if (parts[2]) ms += (parseInt(parts[2]) || 0) * 3600000; // hours + return ms; + }, + recordLap() { const now = this.elapsedTime; const lastTime = this.history.length > 0 ? this.history[this.history.length - 1].time : 0; @@ -856,17 +931,17 @@ const app = { this.el.listBody.innerHTML = sortedHistory.map(h => ` ${h.id} - ${this.formatTime(h.startTime || 0, 'hh:mm:ss.ms')} - ${this.formatTime(h.delta, 'hh:mm:ss.ms')} - ${this.formatTime(h.time, 'hh:mm:ss.ms')} + ${this.formatTime(h.startTime || 0, 'hh:mm:ss')} + ${this.formatTime(h.delta, 'hh:mm:ss')} + ${this.formatTime(h.time, 'hh:mm:ss')} `).join(''); } else { this.el.listBody.innerHTML = sortedHistory.map(h => ` ${h.id} - ${this.formatTime(h.time, 'hh:mm:ss.ms')} - +${this.formatTime(h.delta, 'seconds.ms')} + ${this.formatTime(h.time, 'hh:mm:ss')} + +${this.formatTime(h.delta, 'seconds')} ${h.delta === minDelta && h.delta > 0 ? 'BEST' : ''} @@ -907,7 +982,7 @@ const app = {
Rest
- + @@ -927,7 +1002,7 @@ const app = {
- + @@ -985,15 +1060,37 @@ const app = { this.customActivities[index].name = val; }, + updateActDuration(index, val) { + if (val.includes(':')) { + this.customActivities[index].duration = this.parseTime(val); + } else { + this.customActivities[index].duration = (parseFloat(val) || 0) * 1000; + } + this.renderCustomBuilder(); + }, + applyUnit(index, unit) { const input = document.getElementById(`act-input-${index}`); - const val = parseFloat(input.value) || 0; + const val = input.value; let ms = 0; - switch(unit) { - case 'H': ms = val * 3600000; break; - case 'M': ms = val * 60000; break; - case 'S': ms = val * 1000; break; + if (val.includes(':')) { + ms = this.parseTime(val); + // If they clicked a unit button, maybe they want to multiply the currently parsed value? + // E.g. if it's "00:01:00" and they click "M", it becomes 60 minutes. + let seconds = ms / 1000; + switch(unit) { + case 'H': ms = seconds * 3600000; break; + case 'M': ms = seconds * 60000; break; + case 'S': ms = seconds * 1000; break; + } + } else { + let num = parseFloat(val) || 0; + switch(unit) { + case 'H': ms = num * 3600000; break; + case 'M': ms = num * 60000; break; + case 'S': ms = num * 1000; break; + } } this.customActivities[index].duration = ms; @@ -1109,7 +1206,7 @@ const app = { this.el.sessionTitle.value = timer.name; if (this.currentMode) this.sessionTitles[this.currentMode] = timer.name; this.el.settingIsCountdown.checked = !!config.isCountdown; - this.el.formatSelect.value = config.format || 'hh:mm:ss.ms'; + this.el.formatSelect.value = config.format || 'hh:mm:ss'; if (config.mode === 'custom' || (!config.mode && this.currentMode === 'custom')) { this.customActivities = config.activities || []; diff --git a/index.php b/index.php index 66d4d38..236aba3 100644 --- a/index.php +++ b/index.php @@ -292,18 +292,23 @@ $projectImage = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
-
00:00:00.000
-
00:00:00
+
00:00:00
+
00:00:00
+ +
+ + +
+ - - - + +