Files
dashcaddy/status/js/clock.js
Sami f61e85d9a7 Initial commit: DashCaddy v1.0
Full codebase including API server (32 modules + routes), dashboard frontend,
DashCA certificate distribution, installer script, and deployment skills.
2026-03-05 02:26:12 -08:00

331 lines
13 KiB
JavaScript

// ========== DIGITAL CLOCK WIDGET ==========
(function() {
const widget = document.getElementById('clock-widget');
const render = document.getElementById('clock-render');
if (!widget || !render) return;
const DAYS = ['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday'];
const MONTHS = ['January','February','March','April','May','June','July','August','September','October','November','December'];
const ROMAN = ['XII','I','II','III','IV','V','VI','VII','VIII','IX','X','XI'];
let currentStyle = safeGet('clock-style') || 'default';
let lastChimeHour = -1;
let chimePlaying = false;
let prevFlipDigits = '';
// ===== CHIMES =====
function playChimes(count) {
if (chimePlaying) return;
if (safeGet('clock-chimes') !== 'true') return;
chimePlaying = true;
const vol = parseInt(safeGet('clock-chime-volume') || '50', 10) / 100;
let i = 0;
function strike() {
if (i >= count) { chimePlaying = false; return; }
const bell = new Audio('/assets/sounds/church-bell.mp3');
bell.volume = vol;
bell.play().catch(() => {});
i++;
if (i < count) setTimeout(strike, 2500);
else setTimeout(() => { chimePlaying = false; }, 2500);
}
strike();
}
// ===== DATE STRING =====
function dateStr(now) {
return DAYS[now.getDay()] + ', ' + MONTHS[now.getMonth()] + ' ' + now.getDate() + ', ' + now.getFullYear();
}
// ===== STYLE RENDERERS =====
function renderDefault(now) {
const h24 = now.getHours(), m = now.getMinutes(), s = now.getSeconds();
const ampm = h24 >= 12 ? 'PM' : 'AM';
const h = h24 % 12 || 12;
render.innerHTML =
`<div class="clock-time">${h}:${String(m).padStart(2,'0')}<span class="clock-seconds">:${String(s).padStart(2,'0')}</span><span class="clock-ampm">${ampm}</span></div>` +
`<div class="clock-date">${dateStr(now)}</div>`;
}
function renderLcd(now, cls) {
const h24 = now.getHours(), m = now.getMinutes(), s = now.getSeconds();
const ampm = h24 >= 12 ? 'PM' : 'AM';
const h = h24 % 12 || 12;
render.innerHTML =
`<div class="clock-time">${String(h).padStart(2,'0')}:${String(m).padStart(2,'0')}<span class="clock-seconds">:${String(s).padStart(2,'0')}</span><span class="clock-ampm">${ampm}</span></div>` +
`<div class="clock-date">${dateStr(now)}</div>`;
}
function renderFlip(now) {
const h24 = now.getHours(), m = now.getMinutes(), s = now.getSeconds();
const ampm = h24 >= 12 ? 'PM' : 'AM';
const h = h24 % 12 || 12;
const digits = String(h).padStart(2,' ') + String(m).padStart(2,'0') + String(s).padStart(2,'0');
let html = '<div class="flip-clock-row">';
// Hours
html += flipCard(digits[0], 0);
html += flipCard(digits[1], 1);
html += '<span class="flip-colon">:</span>';
// Minutes
html += flipCard(digits[2], 2);
html += flipCard(digits[3], 3);
html += '<span class="flip-colon">:</span>';
// Seconds
html += flipCard(digits[4], 4);
html += flipCard(digits[5], 5);
html += `<span class="flip-ampm">${ampm}</span>`;
html += '</div>';
html += `<div class="clock-date">${dateStr(now)}</div>`;
render.innerHTML = html;
// Trigger flip animation on changed digits
if (prevFlipDigits) {
for (let i = 0; i < 6; i++) {
if (digits[i] !== prevFlipDigits[i]) {
const card = render.querySelector(`.flip-card[data-idx="${i}"]`);
if (card) card.classList.add('flipping');
}
}
}
prevFlipDigits = digits;
}
function flipCard(digit, idx) {
const d = digit === ' ' ? '' : digit;
return `<div class="flip-card" data-idx="${idx}"><div class="flip-top">${d}</div><div class="flip-bottom">${d}</div></div>`;
}
function renderBinary(now) {
const h24 = now.getHours(), m = now.getMinutes(), s = now.getSeconds();
const h = h24 % 12 || 12;
const ampm = h24 >= 12 ? 'PM' : 'AM';
// 6 columns: H tens, H ones, M tens, M ones, S tens, S ones
const cols = [
Math.floor(h / 10), h % 10,
Math.floor(m / 10), m % 10,
Math.floor(s / 10), s % 10
];
let html = '<div class="binary-clock">';
// Labels
html += '<div class="binary-labels"><span>H</span><span>H</span><span>M</span><span>M</span><span>S</span><span>S</span></div>';
// 4 rows for bits 8,4,2,1
for (let bit = 3; bit >= 0; bit--) {
html += '<div class="binary-row">';
for (let col = 0; col < 6; col++) {
const on = (cols[col] >> bit) & 1;
html += `<div class="binary-dot ${on ? 'on' : ''}"></div>`;
}
html += '</div>';
}
// Value row
html += '<div class="binary-values">';
for (let col = 0; col < 6; col++) {
html += `<span>${cols[col]}</span>`;
}
html += '</div>';
html += `<div class="binary-ampm">${ampm}</div>`;
html += '</div>';
html += `<div class="clock-date">${dateStr(now)}</div>`;
render.innerHTML = html;
}
function renderAnalog(now, useRoman) {
const h = now.getHours(), m = now.getMinutes(), s = now.getSeconds();
const size = 120;
const cx = size / 2, cy = size / 2;
// Angles
const sAngle = (s / 60) * 360 - 90;
const mAngle = ((m + s / 60) / 60) * 360 - 90;
const hAngle = (((h % 12) + m / 60) / 12) * 360 - 90;
// Number labels
let labels = '';
for (let i = 1; i <= 12; i++) {
const angle = (i / 12) * 2 * Math.PI - Math.PI / 2;
const r = 47;
const x = cx + r * Math.cos(angle);
const y = cy + r * Math.sin(angle);
const label = useRoman ? ROMAN[i % 12] : i;
const fs = useRoman ? '7' : '9';
labels += `<text x="${x}" y="${y}" text-anchor="middle" dominant-baseline="central" fill="var(--fg)" font-size="${fs}" font-weight="600" font-family="'Sami Grotesk',sans-serif">${label}</text>`;
}
// Tick marks
let ticks = '';
for (let i = 0; i < 60; i++) {
const angle = (i / 60) * 2 * Math.PI - Math.PI / 2;
const outer = 56;
const inner = i % 5 === 0 ? 52 : 54;
const x1 = cx + inner * Math.cos(angle);
const y1 = cy + inner * Math.sin(angle);
const x2 = cx + outer * Math.cos(angle);
const y2 = cy + outer * Math.sin(angle);
const w = i % 5 === 0 ? 1.5 : 0.5;
ticks += `<line x1="${x1}" y1="${y1}" x2="${x2}" y2="${y2}" stroke="var(--muted)" stroke-width="${w}" stroke-linecap="round"/>`;
}
const svg = `<svg class="analog-clock-svg" viewBox="0 0 ${size} ${size}" width="${size}" height="${size}">
<circle cx="${cx}" cy="${cy}" r="58" fill="none" stroke="var(--border)" stroke-width="2"/>
${ticks}
${labels}
<line x1="${cx}" y1="${cy}" x2="${cx + 28 * Math.cos(hAngle * Math.PI / 180)}" y2="${cy + 28 * Math.sin(hAngle * Math.PI / 180)}" stroke="var(--fg)" stroke-width="3" stroke-linecap="round"/>
<line x1="${cx}" y1="${cy}" x2="${cx + 38 * Math.cos(mAngle * Math.PI / 180)}" y2="${cy + 38 * Math.sin(mAngle * Math.PI / 180)}" stroke="var(--fg)" stroke-width="2" stroke-linecap="round"/>
<line x1="${cx}" y1="${cy}" x2="${cx + 42 * Math.cos(sAngle * Math.PI / 180)}" y2="${cy + 42 * Math.sin(sAngle * Math.PI / 180)}" stroke="#e74c3c" stroke-width="1" stroke-linecap="round"/>
<circle cx="${cx}" cy="${cy}" r="3" fill="var(--fg)"/>
</svg>`;
const ampm = now.getHours() >= 12 ? 'PM' : 'AM';
render.innerHTML = `<div class="analog-clock-wrap">${svg}<div class="analog-info"><span class="analog-digital">${(now.getHours() % 12 || 12)}:${String(m).padStart(2,'0')} ${ampm}</span><span class="analog-date-sm">${dateStr(now)}</span></div></div>`;
}
// ===== MAIN TICK =====
function tick() {
const now = new Date();
const h = now.getHours() % 12 || 12;
const m = now.getMinutes();
const s = now.getSeconds();
// Set style class on widget
widget.className = 'clock-widget' + (currentStyle !== 'default' ? ' ' + currentStyle : '');
switch (currentStyle) {
case 'lcd': renderLcd(now); break;
case 'lcd-blue': renderLcd(now); break;
case 'lcd-amber': renderLcd(now); break;
case 'lcd-retro': renderLcd(now); break;
case 'lcd-taxi': renderLcd(now); break;
case 'flip': renderFlip(now); break;
case 'binary': renderBinary(now); break;
case 'analog': renderAnalog(now, false); break;
case 'roman': renderAnalog(now, true); break;
default: renderDefault(now);
}
// Hourly chimes
if (m === 0 && s === 0 && h !== lastChimeHour) {
lastChimeHour = h;
playChimes(h);
}
if (m !== 0) lastChimeHour = -1;
}
tick();
setInterval(tick, 1000);
// ===== SETTINGS MODAL =====
const STYLES = [
{ id: 'default', label: 'Default', icon: '🕐' },
{ id: 'lcd', label: 'LCD Green', icon: '💚' },
{ id: 'lcd-blue', label: 'LCD Blue', icon: '💙' },
{ id: 'lcd-amber', label: 'LCD Amber', icon: '🟠' },
{ id: 'lcd-retro', label: 'LCD Retro', icon: '🟩' },
{ id: 'lcd-taxi', label: 'LCD Taxi', icon: '🟡' },
{ id: 'flip', label: 'Flip Clock', icon: '📟' },
{ id: 'binary', label: 'Binary', icon: '💻' },
{ id: 'analog', label: 'Analog', icon: '⏰' },
{ id: 'roman', label: 'Roman', icon: '🏛️' },
];
let styleOptionsHtml = '<div class="clock-style-grid">';
STYLES.forEach(s => {
styleOptionsHtml += `<label class="clock-style-option">
<input type="radio" name="clock-style-radio" value="${s.id}">
<span class="clock-style-card"><span class="clock-style-icon">${s.icon}</span><span class="clock-style-label">${s.label}</span></span>
</label>`;
});
styleOptionsHtml += '</div>';
injectModal('clock-settings-modal', `<div id="clock-settings-modal" class="weather-modal">
<div class="weather-modal-content" style="max-width: 420px;">
<h3>Clock Settings</h3>
<div style="margin-bottom: 16px;">
<label style="display: block; margin-bottom: 8px; font-weight: 600; color: var(--fg);">Style</label>
${styleOptionsHtml}
</div>
<div style="margin-bottom: 16px; padding-top: 12px; border-top: 1px solid var(--border);">
<label style="display: flex; align-items: center; gap: 8px; cursor: pointer; font-weight: 600; color: var(--fg);">
<input type="checkbox" id="clock-chimes-toggle"> Hourly church bell chimes
</label>
<div style="font-size: 0.78rem; color: var(--muted); margin-top: 4px; margin-left: 24px;">
Strikes the number of the hour (e.g., 3 bells at 3:00)
</div>
</div>
<div style="margin-bottom: 16px;" id="clock-volume-section">
<label style="display: block; margin-bottom: 6px; font-weight: 600; color: var(--fg);">Volume</label>
<div style="display: flex; align-items: center; gap: 10px;">
<span style="font-size: 0.85rem;">🔈</span>
<input type="range" id="clock-chime-volume" min="0" max="100" value="50" style="flex: 1; accent-color: var(--ok-fg);">
<span style="font-size: 0.85rem;">🔊</span>
<button id="clock-chime-test" style="padding: 4px 10px; font-size: 0.78rem; border-radius: 4px; cursor: pointer;">Test</button>
</div>
</div>
<div class="weather-modal-buttons">
<button id="clock-settings-cancel">Cancel</button>
<button id="clock-settings-save">Save</button>
</div>
</div>
</div>`);
const modal = document.getElementById('clock-settings-modal');
const chimesToggle = document.getElementById('clock-chimes-toggle');
const volumeSlider = document.getElementById('clock-chime-volume');
const volumeSection = document.getElementById('clock-volume-section');
function loadSettings() {
const style = safeGet('clock-style') || 'default';
const radio = modal.querySelector(`input[value="${style}"]`);
if (radio) radio.checked = true;
chimesToggle.checked = safeGet('clock-chimes') === 'true';
volumeSlider.value = safeGet('clock-chime-volume') || '50';
volumeSection.style.opacity = chimesToggle.checked ? '1' : '0.4';
}
chimesToggle?.addEventListener('change', () => {
volumeSection.style.opacity = chimesToggle.checked ? '1' : '0.4';
});
document.getElementById('clock-settings')?.addEventListener('click', () => {
loadSettings();
modal.classList.add('show');
});
document.getElementById('clock-chime-test')?.addEventListener('click', () => {
const vol = parseInt(volumeSlider.value, 10) / 100;
const bell = new Audio('/assets/sounds/church-bell.mp3');
bell.volume = vol;
bell.play().catch(() => {});
});
document.getElementById('clock-settings-save')?.addEventListener('click', () => {
const radio = modal.querySelector('input[name="clock-style-radio"]:checked');
const style = radio ? radio.value : 'default';
safeSet('clock-style', style);
safeSet('clock-chimes', String(chimesToggle.checked));
safeSet('clock-chime-volume', volumeSlider.value);
currentStyle = style;
prevFlipDigits = '';
tick();
modal.classList.remove('show');
showNotification('Clock settings saved', 'success', 2000);
});
document.getElementById('clock-settings-cancel')?.addEventListener('click', () => {
modal.classList.remove('show');
});
wireModal(modal);
// Live preview when clicking style options
modal?.querySelectorAll('input[name="clock-style-radio"]').forEach(radio => {
radio.addEventListener('change', () => {
currentStyle = radio.value;
prevFlipDigits = '';
tick();
});
});
})();