FluxTimer und Fundament für neue Spiele umsetzen
mindflux-ci / build (push) Successful in 21s

This commit is contained in:
AliceDarkest
2026-07-12 22:03:44 +02:00
parent bcd216fe60
commit 02975ebb25
9 changed files with 308 additions and 25 deletions
@@ -0,0 +1,47 @@
/* FluxTimer – Zeitschaltkreis-Optik, rein clientseitig. */
.ft-bell { position:relative; }
.ft-bell-dot { position:absolute; right:5px; top:5px; width:7px; height:7px; border-radius:50%; background:var(--muted); border:1px solid var(--surface); }
.ft-bell.ft-active .ft-bell-dot { background:var(--fluxtimer-primary); box-shadow:0 0 8px var(--fluxtimer-primary); }
.ft-popover { position:fixed; z-index:1600; width:min(340px,calc(100vw - 24px)); padding:1rem; border:1px solid color-mix(in srgb,var(--fluxtimer-secondary) 55%,var(--border)); border-radius:16px; background:var(--panel); box-shadow:0 18px 55px rgba(0,0,0,.38),0 0 24px color-mix(in srgb,var(--fluxtimer-secondary) 22%,transparent); display:none; }
.ft-popover.open { display:block; animation:ft-in .18s ease-out; }
.ft-popover-head,.ft-popover-actions,.ft-settings-head,.ft-settings-actions,.ft-options { display:flex; align-items:center; gap:.7rem; }
.ft-popover-head { justify-content:space-between; margin-bottom:.75rem; }
.ft-popover-head b { color:var(--heading); letter-spacing:.08em; }
.ft-close { border:0; background:none; color:var(--muted); cursor:pointer; font-size:1.25rem; }
.ft-circuit { display:grid; gap:.55rem; padding:.8rem; border-radius:12px; background:var(--fluxtimer-display); border:1px solid color-mix(in srgb,var(--fluxtimer-primary) 45%,var(--border)); box-shadow:inset 0 0 22px rgba(0,0,0,.28); }
.ft-row { display:grid; grid-template-columns:1fr auto; align-items:center; gap:.7rem; }
.ft-row span { font-size:.65rem; letter-spacing:.12em; color:var(--muted); }
.ft-row strong,.ft-preview strong { color:var(--fluxtimer-segment); font-family:"Courier New",monospace; font-variant-numeric:tabular-nums; letter-spacing:.08em; text-shadow:0 0 9px color-mix(in srgb,var(--fluxtimer-segment) 58%,transparent); }
.ft-row.primary strong { font-size:1.3rem; }
.ft-popover-status { color:var(--muted); font-size:.8rem; margin:.7rem 0; }
.ft-popover-actions { justify-content:space-between; flex-wrap:wrap; }
.ft-mini-toggle { border:1px solid var(--border-accent); background:var(--surface-2); color:var(--text); border-radius:9px; padding:.45rem .7rem; cursor:pointer; font-weight:700; }
.ft-settings-head { justify-content:space-between; align-items:flex-start; margin-bottom:1rem; }
.ft-settings-head p { color:var(--muted); font-size:.88rem; margin-top:.25rem; }
.ft-settings-grid { display:grid; grid-template-columns:1fr 220px; gap:1rem; align-items:stretch; }
.ft-preview { display:flex; flex-direction:column; justify-content:center; align-items:center; padding:.8rem; border-radius:12px; background:var(--fluxtimer-display); border:1px solid color-mix(in srgb,var(--fluxtimer-primary) 42%,var(--border)); }
.ft-preview span { color:var(--muted); font-size:.65rem; letter-spacing:.13em; }
.ft-preview strong { font-size:1.55rem; margin-top:.25rem; }
.ft-switch { display:inline-flex; align-items:center; gap:.5rem; cursor:pointer; color:var(--text); }
.ft-switch input { position:absolute; opacity:0; }
.ft-switch span { width:44px; height:24px; border-radius:99px; background:var(--surface-2); border:1px solid var(--border); position:relative; transition:.2s; }
.ft-switch span::after { content:""; position:absolute; width:18px; height:18px; left:2px; top:2px; border-radius:50%; background:var(--muted); transition:.2s; }
.ft-switch input:checked + span { background:color-mix(in srgb,var(--fluxtimer-secondary) 34%,var(--surface)); border-color:var(--fluxtimer-secondary); }
.ft-switch input:checked + span::after { transform:translateX(20px); background:var(--fluxtimer-secondary); box-shadow:0 0 8px var(--fluxtimer-secondary); }
.ft-reminders { display:flex; flex-wrap:wrap; gap:.2rem 1rem; border:0; margin:.3rem 0 .8rem; }
.ft-reminders legend { width:100%; margin-bottom:.25rem; }
.ft-options { flex-wrap:wrap; margin:.25rem 0 .8rem; }
.ft-settings-actions { justify-content:space-between; flex-wrap:wrap; }
.ft-overlay { position:fixed; inset:0; z-index:1900; display:none; place-items:center; padding:1rem; background:rgba(5,3,2,.76); backdrop-filter:blur(7px); }
.ft-overlay.open { display:grid; }
.ft-overlay::before { content:""; position:absolute; inset:0; pointer-events:none; background:linear-gradient(115deg,transparent 35%,rgba(255,255,255,.7) 49%,transparent 55%); transform:translateX(-120%); }
.ft-overlay.open::before { animation:ft-flash .7s ease-out; }
.ft-modal { position:relative; width:min(560px,100%); text-align:center; padding:1.5rem; border-radius:20px; background:var(--panel); border:1px solid var(--fluxtimer-secondary); box-shadow:0 0 50px color-mix(in srgb,var(--fluxtimer-secondary) 35%,transparent); }
.ft-modal .ft-kicker { color:var(--fluxtimer-secondary); text-transform:uppercase; letter-spacing:.18em; font-size:.72rem; font-weight:800; }
.ft-modal h2 { color:var(--heading); font-size:1.7rem; margin:.7rem 0 .4rem; }
.ft-modal p { color:var(--muted); line-height:1.55; }
.ft-modal-actions { display:flex; justify-content:center; flex-wrap:wrap; gap:.7rem; margin-top:1.2rem; }
@keyframes ft-in { from{opacity:0;transform:translateY(-6px)} to{opacity:1;transform:none} }
@keyframes ft-flash { to{transform:translateX(120%)} }
@media (max-width:620px){ .ft-settings-grid{grid-template-columns:1fr}.ft-settings-head{align-items:stretch;flex-direction:column}.ft-popover{left:12px!important;right:12px!important;top:64px!important;width:auto} }
@media (prefers-reduced-motion:reduce){ .ft-popover.open,.ft-overlay.open::before{animation:none} }
+12
View File
@@ -14,6 +14,12 @@
--secondary:#2d9cce; --secondary-2:#5cc4e8;
--text:#ece3d2; --muted:#8c8170; --heading:#ffffff;
--success:#2d9cce; --danger:#ff4d6d;
--esc-accent:#f0a81a; --esc-glow:rgba(240,168,26,.48);
--bh-accent:#d94b64; --bh-glow:rgba(217,75,100,.42);
--sc-accent:#38b9d6; --sc-glow:rgba(56,185,214,.40);
--nw-accent:#4f8ff7; --nw-glow:rgba(79,143,247,.42);
--fluxtimer-primary:#ff5a5f; --fluxtimer-secondary:#f0a81a;
--fluxtimer-display:#170704; --fluxtimer-segment:#ff765f;
--glow-s:rgba(240,168,26,.55); --glow-w:rgba(240,168,26,.16);
--g-soft:0 0 18px var(--glow-w);
}
@@ -25,6 +31,12 @@
--secondary:#1f86b8; --secondary-2:#2d9cce;
--text:#33271c; --muted:#8a7d6c; --heading:#241a12;
--success:#1f86b8; --danger:#be123c;
--esc-accent:#c96a12; --esc-glow:rgba(201,106,18,.30);
--bh-accent:#b42342; --bh-glow:rgba(180,35,66,.26);
--sc-accent:#147f9e; --sc-glow:rgba(20,127,158,.26);
--nw-accent:#2868c7; --nw-glow:rgba(40,104,199,.26);
--fluxtimer-primary:#b42342; --fluxtimer-secondary:#c96a12;
--fluxtimer-display:#fff5ec; --fluxtimer-segment:#a93131;
--glow-s:rgba(214,81,22,.40); --glow-w:rgba(214,81,22,.12);
--g-soft:0 6px 18px var(--glow-w);
}
+129
View File
@@ -0,0 +1,129 @@
/* 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();
})();
+17
View File
@@ -8,6 +8,23 @@
apply(read()); // früh -> kein Aufblitzen
// Globale, rein clientseitige Komfortfunktionen. FluxTimer bleibt auf öffentlichen
// Seiten inaktiv, läuft bei aktivierter Einstellung aber über Seitennavigationen weiter.
if (!document.querySelector('link[data-fluxtimer]')) {
var timerCss = document.createElement('link');
timerCss.rel = 'stylesheet';
timerCss.href = '/css/fluxtimer.css?v=1';
timerCss.setAttribute('data-fluxtimer', '');
document.head.appendChild(timerCss);
}
if (!document.querySelector('script[data-fluxtimer]')) {
var timerScript = document.createElement('script');
timerScript.src = '/js/fluxtimer.js?v=1';
timerScript.defer = true;
timerScript.setAttribute('data-fluxtimer', '');
document.head.appendChild(timerScript);
}
var SUN = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4"/></svg>';
var MOON = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8z"/></svg>';