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