174 lines
5.6 KiB
JavaScript
174 lines
5.6 KiB
JavaScript
|
|
'use strict';
|
|||
|
|
|
|||
|
|
// ---------------------------------------------------------------------------
|
|||
|
|
// Focus Timer
|
|||
|
|
// ---------------------------------------------------------------------------
|
|||
|
|
|
|||
|
|
const TIMER_WORK = 25 * 60;
|
|||
|
|
const TIMER_BREAK = 5 * 60;
|
|||
|
|
let timerSeconds = TIMER_WORK;
|
|||
|
|
let timerRunning = false;
|
|||
|
|
let timerIsBreak = false;
|
|||
|
|
let timerInterval = null;
|
|||
|
|
|
|||
|
|
function timerTick() {
|
|||
|
|
timerSeconds--;
|
|||
|
|
renderTimer();
|
|||
|
|
if (timerSeconds <= 0) {
|
|||
|
|
clearInterval(timerInterval);
|
|||
|
|
timerInterval = null;
|
|||
|
|
timerRunning = false;
|
|||
|
|
if (!timerIsBreak) {
|
|||
|
|
// work session ended → start break
|
|||
|
|
timerIsBreak = true;
|
|||
|
|
recordFocusSession();
|
|||
|
|
timerSeconds = TIMER_BREAK;
|
|||
|
|
showTimerNotification('Pause! 5 Minuten.');
|
|||
|
|
// auto-pause playback during break
|
|||
|
|
if (audio.src && !audio.paused) audio.pause();
|
|||
|
|
} else {
|
|||
|
|
// break ended → reset to work
|
|||
|
|
timerIsBreak = false;
|
|||
|
|
timerSeconds = TIMER_WORK;
|
|||
|
|
showTimerNotification('Pause vorbei. Weiter geht’s.');
|
|||
|
|
}
|
|||
|
|
renderTimer();
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function toggleTimer() {
|
|||
|
|
if (timerRunning) {
|
|||
|
|
clearInterval(timerInterval);
|
|||
|
|
timerInterval = null;
|
|||
|
|
timerRunning = false;
|
|||
|
|
} else {
|
|||
|
|
if ('Notification' in window && Notification.permission === 'default') {
|
|||
|
|
Notification.requestPermission();
|
|||
|
|
}
|
|||
|
|
timerRunning = true;
|
|||
|
|
timerInterval = setInterval(timerTick, 1000);
|
|||
|
|
}
|
|||
|
|
renderTimer();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function resetTimer() {
|
|||
|
|
clearInterval(timerInterval);
|
|||
|
|
timerInterval = null;
|
|||
|
|
timerRunning = false;
|
|||
|
|
timerIsBreak = false;
|
|||
|
|
timerSeconds = TIMER_WORK;
|
|||
|
|
renderTimer();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function renderTimer() {
|
|||
|
|
const m = String(Math.floor(timerSeconds / 60)).padStart(2, '0');
|
|||
|
|
const s = String(timerSeconds % 60).padStart(2, '0');
|
|||
|
|
const display = $('timer-display');
|
|||
|
|
const btn = $('timer-toggle-btn');
|
|||
|
|
const label = $('timer-phase-label');
|
|||
|
|
if (display) display.textContent = `${m}:${s}`;
|
|||
|
|
if (btn) btn.textContent = timerRunning ? '⏸' : '▶';
|
|||
|
|
if (label) label.textContent = timerIsBreak ? 'Pause' : 'Fokus';
|
|||
|
|
// colour the display red when break
|
|||
|
|
if (display) display.style.color = timerIsBreak ? '#e63946' : '';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function showTimerNotification(msg) {
|
|||
|
|
if ('Notification' in window && Notification.permission === 'granted') {
|
|||
|
|
new Notification('diora', { body: msg });
|
|||
|
|
}
|
|||
|
|
// also flash in the timer label
|
|||
|
|
const label = $('timer-phase-label');
|
|||
|
|
if (label) { label.textContent = msg; setTimeout(() => renderTimer(), 3000); }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ---------------------------------------------------------------------------
|
|||
|
|
// Focus session recording
|
|||
|
|
// ---------------------------------------------------------------------------
|
|||
|
|
|
|||
|
|
async function recordFocusSession() {
|
|||
|
|
try {
|
|||
|
|
await fetch('/radio/focus/record/', {
|
|||
|
|
method: 'POST',
|
|||
|
|
headers: { 'Content-Type': 'application/json', 'X-CSRFToken': getCsrfToken() },
|
|||
|
|
body: JSON.stringify({
|
|||
|
|
station_name: currentStation ? currentStation.name : '',
|
|||
|
|
duration_minutes: 25,
|
|||
|
|
}),
|
|||
|
|
});
|
|||
|
|
loadFocusStats();
|
|||
|
|
} catch (e) {}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async function loadFocusStats() {
|
|||
|
|
try {
|
|||
|
|
const res = await fetch('/radio/focus/stats/');
|
|||
|
|
const data = await res.json();
|
|||
|
|
const widget = document.getElementById('focus-today-widget');
|
|||
|
|
if (widget) {
|
|||
|
|
if (data.today_sessions > 0) {
|
|||
|
|
widget.textContent = `Heute: ${data.today_sessions} ${data.today_sessions !== 1 ? 'Sitzungen' : 'Sitzung'} · ${data.today_minutes} min`;
|
|||
|
|
widget.style.display = '';
|
|||
|
|
} else {
|
|||
|
|
widget.style.display = 'none';
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
// populate focus tab
|
|||
|
|
const tbody = document.getElementById('focus-tbody');
|
|||
|
|
if (!tbody) return;
|
|||
|
|
tbody.innerHTML = '';
|
|||
|
|
if (!data.sessions.length) {
|
|||
|
|
tbody.innerHTML = '<tr><td colspan="3" class="empty-msg">Noch keine Fokus-Sitzungen. Starte den Timer!</td></tr>';
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
data.sessions.forEach(s => {
|
|||
|
|
const tr = document.createElement('tr');
|
|||
|
|
const dt = new Date(s.completed_at).toLocaleString([], {dateStyle: 'short', timeStyle: 'short'});
|
|||
|
|
tr.innerHTML = `<td>${dt}</td><td>${escapeHtml(s.station_name || '—')}</td><td>${s.duration_minutes} min</td>`;
|
|||
|
|
tbody.appendChild(tr);
|
|||
|
|
});
|
|||
|
|
} catch (e) {}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ---------------------------------------------------------------------------
|
|||
|
|
// Do Not Disturb / focus mode
|
|||
|
|
// ---------------------------------------------------------------------------
|
|||
|
|
|
|||
|
|
let dndActive = false;
|
|||
|
|
|
|||
|
|
function toggleDNDLight() {
|
|||
|
|
document.body.classList.toggle('dnd-dark');
|
|||
|
|
const btn = $('dnd-light-btn');
|
|||
|
|
if (btn) btn.style.opacity = document.body.classList.contains('dnd-dark') ? '0.4' : '1';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function toggleDND() {
|
|||
|
|
dndActive = !dndActive;
|
|||
|
|
if (!dndActive) document.body.classList.remove('dnd-dark');
|
|||
|
|
document.body.classList.toggle('dnd-mode', dndActive);
|
|||
|
|
const btn = $('dnd-btn');
|
|||
|
|
if (btn) btn.classList.toggle('active', dndActive);
|
|||
|
|
if (dndActive) {
|
|||
|
|
const el = document.documentElement;
|
|||
|
|
if (el.requestFullscreen) el.requestFullscreen();
|
|||
|
|
else if (el.webkitRequestFullscreen) el.webkitRequestFullscreen();
|
|||
|
|
} else {
|
|||
|
|
if (document.fullscreenElement && document.exitFullscreen) document.exitFullscreen();
|
|||
|
|
else if (document.webkitFullscreenElement && document.webkitExitFullscreen) document.webkitExitFullscreen();
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Exit DND on Escape (browser also exits fullscreen on Escape, so sync state)
|
|||
|
|
document.addEventListener('fullscreenchange', () => {
|
|||
|
|
if (!document.fullscreenElement && dndActive) {
|
|||
|
|
dndActive = false;
|
|||
|
|
document.body.classList.remove('dnd-mode');
|
|||
|
|
const btn = $('dnd-btn');
|
|||
|
|
if (btn) btn.classList.remove('active');
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
document.addEventListener('keydown', e => {
|
|||
|
|
if (e.key === 'Escape' && dndActive) toggleDND();
|
|||
|
|
});
|