Initial commit: DashCaddy v1.0
Full codebase including API server (32 modules + routes), dashboard frontend, DashCA certificate distribution, installer script, and deployment skills.
This commit is contained in:
330
status/js/clock.js
Normal file
330
status/js/clock.js
Normal file
@@ -0,0 +1,330 @@
|
||||
// ========== 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();
|
||||
});
|
||||
});
|
||||
})();
|
||||
Reference in New Issue
Block a user