Autosave: 20260306-184312

This commit is contained in:
Flatlogic Bot 2026-03-06 18:43:12 +00:00
parent 81a9931032
commit c7011b706f
2 changed files with 145 additions and 43 deletions

View File

@ -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 => `
<tr>
<td class="font-tabular fw-bold" style="border-left: 4px solid ${h.color}; padding-left: 10px;">${h.id}</td>
<td class="font-tabular">${this.formatTime(h.startTime || 0, 'hh:mm:ss.ms')}</td>
<td class="font-tabular text-primary">${this.formatTime(h.delta, 'hh:mm:ss.ms')}</td>
<td class="text-end font-tabular fw-bold">${this.formatTime(h.time, 'hh:mm:ss.ms')}</td>
<td class="font-tabular">${this.formatTime(h.startTime || 0, 'hh:mm:ss')}</td>
<td class="font-tabular text-primary">${this.formatTime(h.delta, 'hh:mm:ss')}</td>
<td class="text-end font-tabular fw-bold">${this.formatTime(h.time, 'hh:mm:ss')}</td>
</tr>
`).join('');
} else {
this.el.listBody.innerHTML = sortedHistory.map(h => `
<tr class="${h.delta === minDelta && h.delta > 0 ? 'table-success' : ''}">
<td class="font-tabular fw-bold">${h.id}</td>
<td class="font-tabular">${this.formatTime(h.time, 'hh:mm:ss.ms')}</td>
<td class="font-tabular text-muted">+${this.formatTime(h.delta, 'seconds.ms')}</td>
<td class="font-tabular">${this.formatTime(h.time, 'hh:mm:ss')}</td>
<td class="font-tabular text-muted">+${this.formatTime(h.delta, 'seconds')}</td>
<td class="text-end">
${h.delta === minDelta && h.delta > 0 ? '<span class="badge-best">BEST</span>' : ''}
</td>
@ -907,7 +982,7 @@ const app = {
<div class="d-flex align-items-center gap-2 flex-grow-1">
<span class="fw-bold text-uppercase fs-tiny tracking-wider" style="min-width: 40px;">Rest</span>
<div class="input-group input-group-sm flex-grow-1">
<input type="text" id="act-input-${i}" class="form-control form-control-precise form-control-compact" placeholder="Time..." value="${this.formatTime(act.duration, 'hh:mm:ss')}">
<input type="text" id="act-input-${i}" class="form-control form-control-precise form-control-compact" placeholder="Time..." value="${this.formatTime(act.duration, 'hh:mm:ss')}" onchange="app.updateActDuration(${i}, this.value)">
<button class="btn btn-outline-secondary btn-compact" onclick="app.applyUnit(${i}, 'H')">H</button>
<button class="btn btn-outline-secondary btn-compact" onclick="app.applyUnit(${i}, 'M')">M</button>
<button class="btn btn-outline-secondary btn-compact" onclick="app.applyUnit(${i}, 'S')">S</button>
@ -927,7 +1002,7 @@ const app = {
<div class="col-7">
<div class="d-flex align-items-center gap-1">
<div class="input-group input-group-sm">
<input type="text" id="act-input-${i}" class="form-control form-control-precise form-control-compact" placeholder="Time..." value="${this.formatTime(act.duration, 'hh:mm:ss')}">
<input type="text" id="act-input-${i}" class="form-control form-control-precise form-control-compact" placeholder="Time..." value="${this.formatTime(act.duration, 'hh:mm:ss')}" onchange="app.updateActDuration(${i}, this.value)">
<button class="btn btn-outline-secondary btn-compact" onclick="app.applyUnit(${i}, 'H')">H</button>
<button class="btn btn-outline-secondary btn-compact" onclick="app.applyUnit(${i}, 'M')">M</button>
<button class="btn btn-outline-secondary btn-compact" onclick="app.applyUnit(${i}, 'S')">S</button>
@ -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 || [];

View File

@ -292,18 +292,23 @@ $projectImage = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
</div>
</div>
<div class="timer-display font-tabular mb-2" id="main-timer">00:00:00.000</div>
<div class="timer-sub-display font-tabular mb-4" id="sub-timer">00:00:00</div>
<div class="timer-display font-tabular mb-2" id="main-timer">00:00:00</div>
<div class="timer-sub-display font-tabular mb-4 d-none" id="sub-timer">00:00:00</div>
<!-- Controls -->
<!-- Toggle Display Mode -->
<div id="display-mode-container" class="form-check form-switch d-flex justify-content-center mb-3 d-none">
<input class="form-check-input me-2" type="checkbox" id="toggle-display-mode">
<label class="form-check-label small text-muted text-uppercase tracking-wider" for="toggle-display-mode">Show Total Progress</label>
</div>
<div class="d-flex justify-content-center flex-wrap gap-2 mb-5">
<button id="btn-start" class="btn btn-primary btn-precise px-4">Start</button>
<button id="btn-reset" class="btn btn-outline-dark btn-precise px-4">Reset</button>
<button id="btn-pause" class="btn btn-outline-secondary btn-precise px-4">Pause</button>
<button id="btn-lap" class="btn btn-outline-primary btn-precise px-4 d-none">Lap</button>
<button id="btn-next" class="btn btn-outline-primary btn-precise px-4 d-none">Next</button>
<button id="btn-stop" class="btn btn-danger btn-precise px-4">Stop</button>
<button id="btn-save-main" class="btn btn-outline-success btn-precise px-4 d-none">Save</button>
<button id="btn-reset" class="btn btn-outline-dark btn-precise px-4">Reset</button>
<button id="btn-lap" class="btn btn-outline-primary btn-precise px-4 d-none">Lap</button>
<button id="btn-next" class="btn btn-outline-primary btn-precise px-4 d-none">Next</button>
</div>
<!-- Lap Results Container (placed under timer when stopped) -->