Files
mindflux/src/main/resources/static/js/fluxtimer.js
T
AliceDarkest 02975ebb25
mindflux-ci / build (push) Successful in 21s
FluxTimer und Fundament für neue Spiele umsetzen
2026-07-12 22:03:44 +02:00

130 lines
12 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* FluxTimer – lokaler Pausen-Timer ohne Server- oder Drittdienst. */
(function () {
'use strict';
if (window.MindfluxTimer) return;
var KEY = 'fluxtimer';
var MIN_INTERVAL = 5;
var MAX_INTERVAL = 240;
var DEFAULTS = { enabled:false, intervalMinutes:45, reminders:['aufstehen','trinken','augenpause'], customText:'', sound:false, notify:false, nextFireAt:null, lastFiredAt:null, reminderIndex:0 };
var LABELS = {
aufstehen:'88 mph erreicht – kurz aufstehen, McFly!',
trinken:'Großer Scott! Zeit, etwas zu trinken.',
essen:'Dein Fluxkompensator braucht einen Snack.',
augenpause:'Blick in die Zukunft: Gönn deinen Augen eine Pause.'
};
var state = load();
var ringing = false;
var intervalId;
function number(value, fallback) { var n = Number(value); return Number.isFinite(n) ? n : fallback; }
function clamp(value, min, max) { return Math.min(max, Math.max(min, value)); }
function normalize(raw) {
raw = raw && typeof raw === 'object' ? raw : {};
var reminders = Array.isArray(raw.reminders) ? raw.reminders.filter(function (x) { return Object.prototype.hasOwnProperty.call(LABELS, x); }) : DEFAULTS.reminders.slice();
return {
enabled: raw.enabled === true,
intervalMinutes: clamp(Math.round(number(raw.intervalMinutes, 45)), MIN_INTERVAL, MAX_INTERVAL),
reminders: reminders,
customText: typeof raw.customText === 'string' ? raw.customText.trim().slice(0,120) : '',
sound: raw.sound === true,
notify: raw.notify === true,
nextFireAt: Number.isFinite(Number(raw.nextFireAt)) ? Number(raw.nextFireAt) : null,
lastFiredAt: Number.isFinite(Number(raw.lastFiredAt)) ? Number(raw.lastFiredAt) : null,
reminderIndex: Math.max(0, Math.round(number(raw.reminderIndex, 0)))
};
}
function load() { try { return normalize(JSON.parse(localStorage.getItem(KEY) || '{}')); } catch (e) { return normalize({}); } }
function save() { try { localStorage.setItem(KEY, JSON.stringify(state)); } catch (e) {} syncUi(); }
function schedule(from) { state.nextFireAt = (from || Date.now()) + state.intervalMinutes * 60000; ringing = false; save(); }
function setEnabled(enabled) { state.enabled = !!enabled; if (state.enabled) schedule(Date.now()); else { state.nextFireAt = null; ringing = false; closeOverlay(); save(); } }
function remainingMs(now) { return state.enabled && state.nextFireAt ? Math.max(0, state.nextFireAt - (now || Date.now())) : 0; }
function pad(n) { return String(n).padStart(2,'0'); }
function formatDuration(ms) { var seconds=Math.max(0,Math.ceil(ms/1000)); return pad(Math.floor(seconds/3600))+':'+pad(Math.floor((seconds%3600)/60))+':'+pad(seconds%60); }
function formatClock(ts) { if (!ts) return '--:--:--'; return new Date(ts).toLocaleTimeString('de-DE',{hour:'2-digit',minute:'2-digit',second:'2-digit'}); }
function reminderText() {
var pool = state.reminders.slice(); if (state.customText) pool.push('custom');
if (!pool.length) return 'Zeit für eine kurze Pause.';
var key = pool[state.reminderIndex % pool.length];
state.reminderIndex = (state.reminderIndex + 1) % pool.length;
return key === 'custom' ? state.customText : LABELS[key];
}
function ensureUi() {
if (!document.querySelector('.ft-popover')) {
var pop=document.createElement('section'); pop.className='ft-popover'; pop.setAttribute('aria-label','FluxTimer');
pop.innerHTML='<div class="ft-popover-head"><b>FLUXTIMER</b><button class="ft-close" type="button" data-ft-close aria-label="Schließen">×</button></div><div class="ft-circuit"><div class="ft-row primary"><span>NÄCHSTE PAUSE</span><strong data-ft-countdown>--:--:--</strong></div><div class="ft-row"><span>JETZT</span><strong data-ft-now>--:--:--</strong></div><div class="ft-row"><span>LETZTE PAUSE</span><strong data-ft-last>--:--:--</strong></div></div><p class="ft-popover-status" data-ft-status></p><div class="ft-popover-actions"><button class="ft-mini-toggle" type="button" data-ft-quick-toggle></button><a class="mf-btn outline" href="/profile/settings">Einstellungen</a></div>';
document.body.appendChild(pop);
}
if (!document.querySelector('.ft-overlay')) {
var overlay=document.createElement('div'); overlay.className='ft-overlay'; overlay.setAttribute('role','dialog'); overlay.setAttribute('aria-modal','true'); overlay.setAttribute('aria-labelledby','ft-title');
overlay.innerHTML='<div class="ft-modal"><span class="ft-kicker">ZEITSCHALTKREISE AKTIV</span><h2 id="ft-title">Zeit für eine Pause</h2><p data-ft-message></p><div class="ft-modal-actions"><button class="mf-btn outline" type="button" data-ft-snooze>+5 Minuten</button><button class="mf-btn" type="button" data-ft-done>Pause erledigt</button></div></div>';
document.body.appendChild(overlay);
}
}
function positionPopover() {
var trigger=document.querySelector('[data-fluxtimer-toggle]'), pop=document.querySelector('.ft-popover'); if (!trigger || !pop) return;
var r=trigger.getBoundingClientRect(); pop.style.top=(r.bottom+8)+'px'; pop.style.left=Math.max(12,Math.min(window.innerWidth-pop.offsetWidth-12,r.right-pop.offsetWidth))+'px';
}
function togglePopover() { var pop=document.querySelector('.ft-popover'); if (!pop) return; pop.classList.toggle('open'); var open=pop.classList.contains('open'); var trigger=document.querySelector('[data-fluxtimer-toggle]'); if(trigger) trigger.setAttribute('aria-expanded',String(open)); if(open){positionPopover();syncUi();} }
function closePopover() { var pop=document.querySelector('.ft-popover'); if(pop) pop.classList.remove('open'); var trigger=document.querySelector('[data-fluxtimer-toggle]'); if(trigger) trigger.setAttribute('aria-expanded','false'); }
function closeOverlay() { var overlay=document.querySelector('.ft-overlay'); if(overlay) overlay.classList.remove('open'); }
function playSound() { if (!state.sound) return; try { var C=window.AudioContext||window.webkitAudioContext; if(!C)return; var ctx=new C(),o=ctx.createOscillator(),g=ctx.createGain(); o.frequency.value=523.25; g.gain.setValueAtTime(.05,ctx.currentTime); g.gain.exponentialRampToValueAtTime(.001,ctx.currentTime+.35); o.connect(g);g.connect(ctx.destination);o.start();o.stop(ctx.currentTime+.35); } catch(e){} }
function notify(text) { if (!state.notify || !('Notification' in window) || Notification.permission!=='granted') return; try { new Notification('Mindflux FluxTimer',{body:text,tag:'mindflux-fluxtimer'}); } catch(e){} }
function fire(testOnly) { ensureUi(); if(ringing&&!testOnly)return; ringing=true; var text=reminderText(); state.lastFiredAt=Date.now(); save(); var msg=document.querySelector('[data-ft-message]'); if(msg)msg.textContent=text; document.querySelector('.ft-overlay').classList.add('open'); playSound(); notify(text); }
function done() { closeOverlay(); schedule(Date.now()); }
function snooze() { closeOverlay(); state.nextFireAt=Date.now()+5*60000; ringing=false; save(); }
function tick() { var now=Date.now(); if(state.enabled&&state.nextFireAt&&now>=state.nextFireAt)fire(false); syncUi(now); }
function syncUi(now) {
now=now||Date.now(); var countdown=state.enabled?formatDuration(remainingMs(now)):'PAUSIERT';
document.querySelectorAll('[data-ft-countdown]').forEach(function(el){el.textContent=countdown;});
document.querySelectorAll('[data-ft-now]').forEach(function(el){el.textContent=formatClock(now);});
document.querySelectorAll('[data-ft-last]').forEach(function(el){el.textContent=formatClock(state.lastFiredAt);});
document.querySelectorAll('[data-ft-status]').forEach(function(el){el.textContent=state.enabled?'Aktiv · '+state.intervalMinutes+' Minuten':'Der Timer ist deaktiviert.';});
document.querySelectorAll('[data-ft-quick-toggle]').forEach(function(el){el.textContent=state.enabled?'Timer pausieren':'Timer aktivieren';});
document.querySelectorAll('[data-fluxtimer-toggle]').forEach(function(el){el.classList.toggle('ft-active',state.enabled);});
var fields=document.querySelector('[data-fluxtimer-settings]'); if(fields){
var enabled=fields.querySelector('[data-ft-field="enabled"]'); if(enabled&&document.activeElement!==enabled)enabled.checked=state.enabled;
var interval=fields.querySelector('[data-ft-field="intervalMinutes"]'); if(interval&&document.activeElement!==interval)interval.value=state.intervalMinutes;
var custom=fields.querySelector('[data-ft-field="customText"]'); if(custom&&document.activeElement!==custom)custom.value=state.customText;
['sound','notify'].forEach(function(k){var el=fields.querySelector('[data-ft-field="'+k+'"]');if(el&&document.activeElement!==el)el.checked=state[k];});
fields.querySelectorAll('[data-ft-reminder]').forEach(function(el){if(document.activeElement!==el)el.checked=state.reminders.indexOf(el.value)>=0;});
}
}
function readSettings(container) {
var interval=container.querySelector('[data-ft-field="intervalMinutes"]'), previousInterval=state.intervalMinutes; state.intervalMinutes=clamp(Math.round(number(interval.value,45)),MIN_INTERVAL,MAX_INTERVAL); interval.value=state.intervalMinutes;
state.customText=container.querySelector('[data-ft-field="customText"]').value.trim().slice(0,120);
state.sound=container.querySelector('[data-ft-field="sound"]').checked;
state.notify=container.querySelector('[data-ft-field="notify"]').checked;
state.reminders=Array.prototype.map.call(container.querySelectorAll('[data-ft-reminder]:checked'),function(el){return el.value;});
if(state.enabled&&previousInterval!==state.intervalMinutes)schedule(Date.now());else save();
var status=container.querySelector('[data-ft-save-status]');if(status){status.textContent='Auf diesem Gerät gespeichert.';setTimeout(function(){status.textContent='Wird nur auf diesem Gerät gespeichert.';},1800);}
}
function init() {
if(location.pathname==='/'||location.pathname==='/login'||location.pathname==='/register')return;
ensureUi();
if(state.enabled&&!state.nextFireAt)schedule(Date.now());
document.addEventListener('click',function(e){
if(e.target.closest('[data-fluxtimer-toggle]')){togglePopover();return;}
if(e.target.closest('[data-ft-close]')){closePopover();return;}
if(e.target.closest('[data-ft-quick-toggle]')){setEnabled(!state.enabled);return;}
if(e.target.closest('[data-ft-done]')){done();return;}
if(e.target.closest('[data-ft-snooze]')){snooze();return;}
if(e.target.closest('[data-ft-test]')){fire(true);return;}
var pop=e.target.closest('.ft-popover'); if(!pop&&!e.target.closest('[data-fluxtimer-toggle]'))closePopover();
});
var settings=document.querySelector('[data-fluxtimer-settings]'); if(settings){
settings.addEventListener('change',function(e){
if(e.target.matches('[data-ft-field="enabled"]')){setEnabled(e.target.checked);return;}
if(e.target.matches('[data-ft-field="notify"]')&&e.target.checked&&'Notification' in window&&Notification.permission==='default') Notification.requestPermission().then(function(p){if(p!=='granted'){e.target.checked=false;state.notify=false;save();}});
readSettings(settings);
});
settings.addEventListener('input',function(e){if(e.target.matches('[data-ft-field="customText"],[data-ft-field="intervalMinutes"]'))readSettings(settings);});
}
window.addEventListener('storage',function(e){if(e.key===KEY){state=load();syncUi();}});
window.addEventListener('resize',positionPopover);
intervalId=window.setInterval(tick,1000); tick();
}
window.MindfluxTimer={getState:function(){return JSON.parse(JSON.stringify(state));},setEnabled:setEnabled,remainingMs:remainingMs,formatDuration:formatDuration,test:function(){fire(true);},reset:function(){state=normalize({});save();}};
if(document.readyState==='loading')document.addEventListener('DOMContentLoaded',init);else init();
})();