diora-web/static/js/app/14-focus.js
mrwnslz 656520fe8e
Some checks failed
Build and push Docker image / build (push) Successful in 14s
Test / test (push) Successful in 8m23s
Test / e2e (push) Failing after 1m54s
Refactoring Schritt 2: app.js nach Bereichen in static/js/app/ aufgeteilt; sw v106
32 klassische Skripte in der alten Reihenfolge (01-core … 99-init), ohne
Änderung am Code darin: geschnitten nur zwischen Anweisungen, je Datei
'use strict' wie vorher für die ganze Datei. Geprüft (acorn): kein Code, der
beim Laden läuft, braucht einen Namen aus einer späteren Datei. Globale
Funktionen und let/const bleiben global, die App-Schichten (native.js,
native-eink.js, …) umhüllen sie wie bisher. Template und sw.js-SHELL listen
die Dateien; diora-android build_web nimmt static/js rekursiv.

Getestet: 14 Browser-Tests (e2e), Desktop-App (Buch, Lesestand, Leiste im
Reader, Hell/Dunkel).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 18:30:43 +02:00

173 lines
5.6 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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