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>
173 lines
5.6 KiB
JavaScript
173 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();
|
||
});
|