/* 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='
FLUXTIMER
NÄCHSTE PAUSE--:--:--
JETZT--:--:--
LETZTE PAUSE--:--:--

Einstellungen
'; 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='
ZEITSCHALTKREISE AKTIV

Zeit für eine Pause

'; 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(); })();