Files
hyprcrd/public/plugins/watch/watch.js
T
thandClaude Opus 4.6 b9345a410f watch plugin: analog clock overlay with SVG watchface
Simple watchface with second-hand wobble animation, 5-min idle
auto-show, W key toggle, light/dark theme colors, backdrop blur
from theme settings. Grace period prevents instant dismiss.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 21:16:25 +02:00

170 lines
4.6 KiB
JavaScript

// ── Watch plugin — analog clock overlay ──
const $ = id => document.getElementById(id);
const LAYER_MAP = {
digits: 'watch-layer-digits',
hour: 'watch-layer-hour',
minute: 'watch-layer-minute',
second: 'watch-second-wobble',
};
let visible = false;
let tickTimer = null;
let idleTimer = null;
let lastActivity = Date.now();
let showedAt = 0;
let wobbleKick = null, wobbleRebound = null;
let svgCache = {};
let loaded = false;
const IDLE_MINUTES = 5;
// ── SVG loading ──
async function loadSvg(layer) {
if (svgCache[layer]) return svgCache[layer];
try {
const res = await fetch(`plugins/watch/faces/simple/${layer}.svg`);
if (!res.ok) return '';
let svg = await res.text();
svg = svg.replace(/<\?xml[\s\S]*?\?>/gi, '').replace(/<!DOCTYPE[\s\S]*?>/gi, '').trim();
svgCache[layer] = svg;
return svg;
} catch { return ''; }
}
async function loadFace() {
if (loaded) return;
for (const [layer, elId] of Object.entries(LAYER_MAP)) {
const el = $(elId);
if (el) el.innerHTML = await loadSvg(layer);
}
loaded = true;
}
// ── Clock logic ──
function tick() {
const now = new Date();
const h12 = now.getHours() % 12;
const m = now.getMinutes();
const s = now.getSeconds();
const hourEl = $('watch-layer-hour');
const minEl = $('watch-layer-minute');
const secEl = $('watch-layer-second');
if (hourEl) hourEl.style.transform = `rotate(${(((h12 + m / 60) / 12) * 360).toFixed(3)}deg)`;
if (minEl) minEl.style.transform = `rotate(${(((m + s / 60) / 60) * 360).toFixed(3)}deg)`;
if (secEl) secEl.style.transform = `rotate(${((s / 60) * 360).toFixed(3)}deg)`;
triggerWobble();
}
function triggerWobble() {
const wobEl = $('watch-second-wobble');
if (!wobEl) return;
clearTimeout(wobbleKick);
clearTimeout(wobbleRebound);
wobEl.style.transform = 'rotate(0deg)';
requestAnimationFrame(() => {
wobEl.style.transform = 'rotate(0.78deg)';
wobbleKick = setTimeout(() => { wobEl.style.transform = 'rotate(-0.25deg)'; }, 95);
wobbleRebound = setTimeout(() => { wobEl.style.transform = 'rotate(0deg)'; }, 205);
});
}
function startTick() {
if (tickTimer) return;
tick();
tickTimer = setInterval(tick, 1000);
}
function stopTick() {
if (tickTimer) { clearInterval(tickTimer); tickTimer = null; }
clearTimeout(wobbleKick);
clearTimeout(wobbleRebound);
}
// ── Show / hide ──
async function show() {
if (visible) return;
visible = true;
const overlay = $('watch-overlay');
if (!overlay) return;
showedAt = Date.now();
await loadFace();
overlay.classList.remove('hidden');
startTick();
requestAnimationFrame(() => overlay.classList.add('visible'));
}
function hide() {
if (!visible) return;
visible = false;
const overlay = $('watch-overlay');
if (!overlay) return;
overlay.classList.remove('visible');
stopTick();
setTimeout(() => { if (!visible) overlay.classList.add('hidden'); }, 250);
}
function toggle() { visible ? hide() : show(); }
// ── Idle auto-show ──
function resetIdleTimer() {
clearTimeout(idleTimer);
if (!IDLE_MINUTES) return;
idleTimer = setTimeout(() => {
if (visible) return;
if (document.visibilityState !== 'visible') return;
if (document.querySelector('.overlay.open')) return;
show();
}, IDLE_MINUTES * 60000);
}
function onActivity() {
const now = Date.now();
if (now - lastActivity < 300) return;
lastActivity = now;
if (visible && now - showedAt > 500) hide();
resetIdleTimer();
}
// ── Init ──
function init() {
const overlay = $('watch-overlay');
if (!overlay) return;
overlay.addEventListener('click', () => { hide(); onActivity(); });
window.addEventListener('keydown', e => {
if (e.key === 'Escape' && visible) {
e.preventDefault(); e.stopPropagation(); e.stopImmediatePropagation();
hide(); onActivity();
}
}, { capture: true });
for (const ev of ['pointermove', 'pointerdown', 'keydown', 'wheel', 'touchstart']) {
window.addEventListener(ev, onActivity, { passive: true });
}
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'visible') onActivity();
});
resetIdleTimer();
window.hyprcrd?.registerPlugin({ name: 'watch', version: '1.0.0' });
}
window._watchToggle = toggle;
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}