Autosave: 20260306-184312
This commit is contained in:
parent
81a9931032
commit
c7011b706f
@ -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 || [];
|
||||
|
||||
15
index.php
15
index.php
@ -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) -->
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user