86 lines
18 KiB
JavaScript
86 lines
18 KiB
JavaScript
class CountdownTimer{constructor(options={}){this.config={name:options.name||'default_timer',duration:options.duration||1800,autoStart:options.autoStart||false,container:options.container||'countdown-timer',size:options.size||'large',theme:options.theme||'primary',showProgress:options.showProgress!==false,showControls:options.showControls!==false,warningThreshold:options.warningThreshold||30,showWarning:options.showWarning!==false,warningMessage:options.warningMessage||'Timer läuft ab!',forceQuitEnabled:options.forceQuitEnabled!==false,forceQuitAction:options.forceQuitAction||'logout',customEndpoint:options.customEndpoint||null,onTick:options.onTick||null,onWarning:options.onWarning||null,onExpired:options.onExpired||null,onForceQuit:options.onForceQuit||null,apiBase:options.apiBase||'/api/timers',updateInterval:options.updateInterval||1000,syncWithServer:options.syncWithServer!==false};this.state={remaining:this.config.duration,total:this.config.duration,status:'stopped',warningShown:false,lastServerSync:null};this.elements={};this.intervals={countdown:null,serverSync:null};this.listeners=new Map();this.init();}
|
|
init(){this.createUI();this.attachEventListeners();if(this.config.syncWithServer){this.syncWithServer();this.startServerSync();}
|
|
if(this.config.autoStart){this.start();}
|
|
console.log(`Timer'${this.config.name}'initialisiert`);}
|
|
createUI(){const container=document.getElementById(this.config.container);if(!container){console.error(`Container'${this.config.container}'nicht gefunden`);return;}
|
|
const timerWrapper=document.createElement('div');timerWrapper.className=`countdown-timer-wrapper size-${this.config.size}theme-${this.config.theme}`;timerWrapper.innerHTML=this.getTimerHTML();container.appendChild(timerWrapper);this.elements={wrapper:timerWrapper,display:timerWrapper.querySelector('.timer-display'),timeText:timerWrapper.querySelector('.time-text'),progressBar:timerWrapper.querySelector('.progress-fill'),progressText:timerWrapper.querySelector('.progress-text'),statusIndicator:timerWrapper.querySelector('.status-indicator'),warningBox:timerWrapper.querySelector('.warning-box'),warningText:timerWrapper.querySelector('.warning-text'),controls:timerWrapper.querySelector('.timer-controls'),startBtn:timerWrapper.querySelector('.btn-start'),pauseBtn:timerWrapper.querySelector('.btn-pause'),stopBtn:timerWrapper.querySelector('.btn-stop'),resetBtn:timerWrapper.querySelector('.btn-reset'),extendBtn:timerWrapper.querySelector('.btn-extend')};this.updateDisplay();}
|
|
getTimerHTML(){return`<div class="countdown-timer-container"><!--Timer-Display--><div class="timer-display"><div class="time-display"><span class="time-text">${this.formatTime(this.state.remaining)}</span><span class="time-label">verbleibend</span></div><div class="status-indicator"><i class="fas fa-circle"></i><span class="status-text">Gestoppt</span></div></div><!--Fortschrittsbalken-->${this.config.showProgress?`<div class="progress-container"><div class="progress-bar"><div class="progress-fill"style="width: 0%"></div></div><div class="progress-text">0%abgelaufen</div></div>`:''}<!--Warnungsbereich--><div class="warning-box"style="display: none;"><div class="warning-content"><i class="fas fa-exclamation-triangle"></i><span class="warning-text">${this.config.warningMessage}</span></div></div><!--Steuerungsbuttons-->${this.config.showControls?`<div class="timer-controls"><button class="btn btn-success btn-start"title="Timer starten"><i class="fas fa-play"></i><span>Start</span></button><button class="btn btn-warning btn-pause"title="Timer pausieren"style="display: none;"><i class="fas fa-pause"></i><span>Pause</span></button><button class="btn btn-danger btn-stop"title="Timer stoppen"><i class="fas fa-stop"></i><span>Stop</span></button><button class="btn btn-secondary btn-reset"title="Timer zurücksetzen"><i class="fas fa-redo"></i><span>Reset</span></button><button class="btn btn-info btn-extend"title="Timer verlängern"><i class="fas fa-plus"></i><span>+5min</span></button></div>`:''}</div>`;}
|
|
attachEventListeners(){if(this.elements.startBtn){this.elements.startBtn.addEventListener('click',()=>this.start());}
|
|
if(this.elements.pauseBtn){this.elements.pauseBtn.addEventListener('click',()=>this.pause());}
|
|
if(this.elements.stopBtn){this.elements.stopBtn.addEventListener('click',()=>this.stop());}
|
|
if(this.elements.resetBtn){this.elements.resetBtn.addEventListener('click',()=>this.reset());}
|
|
if(this.elements.extendBtn){this.elements.extendBtn.addEventListener('click',()=>this.extend(300));}
|
|
document.addEventListener('keydown',(e)=>this.handleKeyboardShortcuts(e));document.addEventListener('visibilitychange',()=>this.handleVisibilityChange());window.addEventListener('beforeunload',(e)=>this.handleBeforeUnload(e));}
|
|
async start(){try{if(this.state.status==='running'){return true;}
|
|
if(this.config.syncWithServer){const response=await this.apiCall('start','POST');if(!response.success){this.showError('Fehler beim Starten des Timers');return false;}}
|
|
this.state.status='running';this.startCountdown();this.updateControls();this.updateStatusIndicator();console.log(`Timer'${this.config.name}'gestartet`);return true;}catch(error){console.error('Fehler beim Starten des Timers:',error);this.showError('Timer konnte nicht gestartet werden');return false;}}
|
|
async pause(){try{if(this.state.status!=='running'){return true;}
|
|
if(this.config.syncWithServer){const response=await this.apiCall('pause','POST');if(!response.success){this.showError('Fehler beim Pausieren des Timers');return false;}}
|
|
this.state.status='paused';this.stopCountdown();this.updateControls();this.updateStatusIndicator();console.log(`Timer'${this.config.name}'pausiert`);return true;}catch(error){console.error('Fehler beim Pausieren des Timers:',error);this.showError('Timer konnte nicht pausiert werden');return false;}}
|
|
async stop(){try{if(this.config.syncWithServer){const response=await this.apiCall('stop','POST');if(!response.success){this.showError('Fehler beim Stoppen des Timers');return false;}}
|
|
this.state.status='stopped';this.state.remaining=this.state.total;this.state.warningShown=false;this.stopCountdown();this.hideWarning();this.updateDisplay();this.updateControls();this.updateStatusIndicator();console.log(`Timer'${this.config.name}'gestoppt`);return true;}catch(error){console.error('Fehler beim Stoppen des Timers:',error);this.showError('Timer konnte nicht gestoppt werden');return false;}}
|
|
async reset(){try{if(this.config.syncWithServer){const response=await this.apiCall('reset','POST');if(!response.success){this.showError('Fehler beim Zurücksetzen des Timers');return false;}}
|
|
this.stop();this.state.remaining=this.state.total;this.updateDisplay();console.log(`Timer'${this.config.name}'zurückgesetzt`);return true;}catch(error){console.error('Fehler beim Zurücksetzen des Timers:',error);this.showError('Timer konnte nicht zurückgesetzt werden');return false;}}
|
|
async extend(seconds){try{if(this.config.syncWithServer){const response=await this.apiCall('extend','POST',{seconds});if(!response.success){this.showError('Fehler beim Verlängern des Timers');return false;}}
|
|
this.state.remaining+=seconds;this.state.total+=seconds;this.state.warningShown=false;this.hideWarning();this.updateDisplay();this.showToast(`Timer um ${Math.floor(seconds/60)}Minuten verlängert`,'success');console.log(`Timer'${this.config.name}'um ${seconds}Sekunden verlängert`);return true;}catch(error){console.error('Fehler beim Verlängern des Timers:',error);this.showError('Timer konnte nicht verlängert werden');return false;}}
|
|
startCountdown(){this.stopCountdown();this.intervals.countdown=setInterval(()=>{this.tick();},this.config.updateInterval);}
|
|
stopCountdown(){if(this.intervals.countdown){clearInterval(this.intervals.countdown);this.intervals.countdown=null;}}
|
|
tick(){if(this.state.status!=='running'){return;}
|
|
this.state.remaining=Math.max(0,this.state.remaining-1);this.updateDisplay();if(this.config.onTick){this.config.onTick(this.state.remaining,this.state.total);}
|
|
if(!this.state.warningShown&&this.state.remaining<=this.config.warningThreshold&&this.state.remaining>0){this.showWarning();}
|
|
if(this.state.remaining<=0){this.handleExpired();}}
|
|
async handleExpired(){console.warn(`Timer'${this.config.name}'ist abgelaufen`);this.state.status='expired';this.stopCountdown();this.updateDisplay();this.updateStatusIndicator();if(this.config.onExpired){this.config.onExpired();}
|
|
if(this.config.forceQuitEnabled){await this.executeForceQuit();}}
|
|
async executeForceQuit(){try{console.warn(`Force-Quit für Timer'${this.config.name}'wird ausgeführt...`);if(this.config.onForceQuit){const shouldContinue=this.config.onForceQuit(this.config.forceQuitAction);if(!shouldContinue){return;}}
|
|
if(this.config.syncWithServer){const response=await this.apiCall('force-quit','POST');if(!response.success){console.error('Force-Quit-API-Aufruf fehlgeschlagen');}}
|
|
switch(this.config.forceQuitAction){case'logout':this.performLogout();break;case'redirect':this.performRedirect();break;case'refresh':this.performRefresh();break;case'custom':this.performCustomAction();break;default:console.warn(`Unbekannte Force-Quit-Aktion:${this.config.forceQuitAction}`);}}catch(error){console.error('Fehler bei Force-Quit-Ausführung:',error);}}
|
|
performLogout(){this.showModal('Session abgelaufen','Sie werden automatisch abgemeldet...','warning');setTimeout(()=>{window.location.href='/auth/logout';},2000);}
|
|
performRedirect(){const redirectUrl=this.config.redirectUrl||'/';this.showModal('Umleitung','Sie werden weitergeleitet...','info');setTimeout(()=>{window.location.href=redirectUrl;},2000);}
|
|
performRefresh(){this.showModal('Seite wird aktualisiert','Die Seite wird automatisch neu geladen...','info');setTimeout(()=>{window.location.reload();},2000);}
|
|
performCustomAction(){if(this.config.customEndpoint){fetch(this.config.customEndpoint,{method:'POST',headers:{'Content-Type':'application/json','X-CSRFToken':this.getCSRFToken()},body:JSON.stringify({timer_name:this.config.name,action:'force_quit'})}).catch(error=>{console.error('Custom-Action-Request fehlgeschlagen:',error);});}}
|
|
showWarning(){if(!this.config.showWarning||this.state.warningShown){return;}
|
|
this.state.warningShown=true;if(this.elements.warningBox){this.elements.warningBox.style.display='block';this.elements.warningBox.classList.add('pulse');}
|
|
if(this.config.onWarning){this.config.onWarning(this.state.remaining);}
|
|
this.showNotification('Timer-Warnung',this.config.warningMessage);console.warn(`Timer-Warnung für'${this.config.name}':${this.state.remaining}Sekunden verbleiben`);}
|
|
hideWarning(){this.state.warningShown=false;if(this.elements.warningBox){this.elements.warningBox.style.display='none';this.elements.warningBox.classList.remove('pulse');}}
|
|
updateDisplay(){if(this.elements.timeText){this.elements.timeText.textContent=this.formatTime(this.state.remaining);}
|
|
if(this.config.showProgress&&this.elements.progressBar){const progress=((this.state.total-this.state.remaining)/this.state.total)*100;this.elements.progressBar.style.width=`${progress}%`;if(this.elements.progressText){this.elements.progressText.textContent=`${Math.round(progress)}%abgelaufen`;}}
|
|
this.updateTheme();}
|
|
updateTheme(){if(!this.elements.wrapper)return;const progress=(this.state.total-this.state.remaining)/this.state.total;this.elements.wrapper.classList.remove('theme-primary','theme-warning','theme-danger');if(progress<0.7){this.elements.wrapper.classList.add('theme-primary');}else if(progress<0.9){this.elements.wrapper.classList.add('theme-warning');}else{this.elements.wrapper.classList.add('theme-danger');}}
|
|
updateControls(){if(!this.config.showControls)return;const isRunning=this.state.status==='running';const isPaused=this.state.status==='paused';const isStopped=this.state.status==='stopped';if(this.elements.startBtn){this.elements.startBtn.style.display=(isStopped||isPaused)?'inline-flex':'none';}
|
|
if(this.elements.pauseBtn){this.elements.pauseBtn.style.display=isRunning?'inline-flex':'none';}
|
|
if(this.elements.stopBtn){this.elements.stopBtn.disabled=isStopped;}
|
|
if(this.elements.resetBtn){this.elements.resetBtn.disabled=isRunning;}
|
|
if(this.elements.extendBtn){this.elements.extendBtn.disabled=this.state.status==='expired';}}
|
|
updateStatusIndicator(){if(!this.elements.statusIndicator)return;const statusText=this.elements.statusIndicator.querySelector('.status-text');const statusIcon=this.elements.statusIndicator.querySelector('i');if(statusText&&statusIcon){switch(this.state.status){case'running':statusText.textContent='Läuft';statusIcon.className='fas fa-circle text-success';break;case'paused':statusText.textContent='Pausiert';statusIcon.className='fas fa-circle text-warning';break;case'expired':statusText.textContent='Abgelaufen';statusIcon.className='fas fa-circle text-danger';break;default:statusText.textContent='Gestoppt';statusIcon.className='fas fa-circle text-secondary';}}}
|
|
formatTime(seconds){const minutes=Math.floor(seconds/60);const secs=seconds%60;return`${minutes.toString().padStart(2,'0')}:${secs.toString().padStart(2,'0')}`;}
|
|
async syncWithServer(){try{const response=await this.apiCall('status','GET');if(response.success&&response.data){const serverState=response.data;this.state.remaining=serverState.remaining_seconds||this.state.remaining;this.state.total=serverState.duration_seconds||this.state.total;this.state.status=serverState.status||this.state.status;this.updateDisplay();this.updateControls();this.updateStatusIndicator();this.state.lastServerSync=new Date();}}catch(error){console.error('Server-Synchronisation fehlgeschlagen:',error);}}
|
|
startServerSync(){if(this.intervals.serverSync){clearInterval(this.intervals.serverSync);}
|
|
this.intervals.serverSync=setInterval(()=>{this.syncWithServer();},30000);}
|
|
async apiCall(action,method='GET',data=null){const url=`${this.config.apiBase}/${this.config.name}/${action}`;const options={method,headers:{'Content-Type':'application/json','X-CSRFToken':this.getCSRFToken()}};if(data&&(method==='POST'||method==='PUT')){options.body=JSON.stringify(data);}
|
|
const response=await fetch(url,options);return await response.json();}
|
|
getCSRFToken(){const token=document.querySelector('meta[name="csrf-token"]');return token?token.getAttribute('content'):'';}
|
|
showNotification(title,message){if('Notification'in window&&Notification.permission==='granted'){new Notification(title,{body:message,icon:'/static/icons/timer-icon.png'});}}
|
|
showToast(message,type='info'){console.log(`Toast[${type}]:${message}`);}
|
|
showError(message){this.showModal('Fehler',message,'danger');}
|
|
showModal(title,message,type='info'){console.log(`Modal[${type}]${title}:${message}`);}
|
|
handleKeyboardShortcuts(e){if(e.ctrlKey||e.metaKey){switch(e.key){case' ':e.preventDefault();if(this.state.status==='running'){this.pause();}else{this.start();}
|
|
break;case'r':e.preventDefault();this.reset();break;case's':e.preventDefault();this.stop();break;}}}
|
|
handleVisibilityChange(){if(document.hidden){this.config._wasRunning=this.state.status==='running';}else{if(this.config.syncWithServer){this.syncWithServer();}}}
|
|
handleBeforeUnload(e){if(this.state.status==='running'&&this.state.remaining>0){e.preventDefault();e.returnValue='Timer läuft noch. Möchten Sie die Seite wirklich verlassen?';return e.returnValue;}}
|
|
destroy(){this.stopCountdown();if(this.intervals.serverSync){clearInterval(this.intervals.serverSync);}
|
|
if(this.elements.wrapper){this.elements.wrapper.remove();}
|
|
this.listeners.forEach((listener,element)=>{element.removeEventListener(listener.event,listener.handler);});console.log(`Timer'${this.config.name}'zerstört`);}}
|
|
const timerStyles=`<style id="countdown-timer-styles">.countdown-timer-wrapper{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;max-width:400px;margin:0 auto;text-align:center;border-radius:12px;box-shadow:0 4px 12px rgba(0,0,0,0.15);background:white;overflow:hidden;}.countdown-timer-container{padding:24px;}
|
|
.size-small{max-width:250px;font-size:0.9em;}.size-medium{max-width:350px;}.size-large{max-width:450px;font-size:1.1em;}
|
|
.theme-primary{border-left:4px solid#007bff;}.theme-warning{border-left:4px solid#ffc107;}.theme-danger{border-left:4px solid#dc3545;}.theme-success{border-left:4px solid#28a745;}
|
|
.timer-display{margin-bottom:20px;}.time-display{margin-bottom:8px;}.time-text{font-size:3em;font-weight:bold;color:#2c3e50;font-family:'Courier New',monospace;}.time-label{display:block;font-size:0.9em;color:#6c757d;margin-top:4px;}.status-indicator{display:flex;align-items:center;justify-content:center;gap:8px;font-size:0.9em;color:#6c757d;}
|
|
.progress-container{margin-bottom:20px;}.progress-bar{height:8px;background-color:#e9ecef;border-radius:4px;overflow:hidden;margin-bottom:8px;}.progress-fill{height:100%;background:linear-gradient(90deg,#007bff,#0056b3);transition:width 0.5s ease-in-out;border-radius:4px;}.progress-text{font-size:0.85em;color:#6c757d;}
|
|
.warning-box{background:linear-gradient(135deg,#fff3cd,#ffeaa7);border:1px solid#ffc107;border-radius:8px;padding:12px;margin-bottom:20px;animation:pulse 2s infinite;}.warning-content{display:flex;align-items:center;justify-content:center;gap:8px;color:#856404;font-weight:500;}.warning-content i{color:#ffc107;}
|
|
.timer-controls{display:flex;gap:8px;justify-content:center;flex-wrap:wrap;}.timer-controls.btn{display:inline-flex;align-items:center;gap:6px;padding:8px 16px;border:none;border-radius:6px;font-size:0.9em;font-weight:500;cursor:pointer;transition:all 0.2s ease;text-decoration:none;}.timer-controls.btn:hover{transform:translateY(-1px);box-shadow:0 2px 8px rgba(0,0,0,0.2);}.timer-controls.btn:disabled{opacity:0.6;cursor:not-allowed;transform:none;}.btn-success{background:#28a745;color:white;}.btn-warning{background:#ffc107;color:#212529;}.btn-danger{background:#dc3545;color:white;}.btn-secondary{background:#6c757d;color:white;}.btn-info{background:#17a2b8;color:white;}
|
|
@keyframes pulse{0%,100%{opacity:1;}
|
|
50%{opacity:0.7;}}
|
|
@media(max-width:480px){.countdown-timer-wrapper{margin:0 16px;}.time-text{font-size:2.5em;}.timer-controls{flex-direction:column;}.timer-controls.btn{width:100%;justify-content:center;}}
|
|
@media(prefers-color-scheme:dark){.countdown-timer-wrapper{background:#2c3e50;color:white;}.time-text{color:#ecf0f1;}.progress-bar{background-color:#34495e;}}</style>`;if(!document.getElementById('countdown-timer-styles')){document.head.insertAdjacentHTML('beforeend',timerStyles);}
|
|
window.CountdownTimer=CountdownTimer;window.TimerManager={timers:new Map(),create(name,options){if(this.timers.has(name)){console.warn(`Timer'${name}'existiert bereits`);return this.timers.get(name);}
|
|
const timer=new CountdownTimer({...options,name:name});this.timers.set(name,timer);return timer;},get(name){return this.timers.get(name);},destroy(name){const timer=this.timers.get(name);if(timer){timer.destroy();this.timers.delete(name);}},destroyAll(){this.timers.forEach(timer=>timer.destroy());this.timers.clear();}}; |