Elysia-Suite's picture
Upload 7 files
e1301d4 verified
raw
history blame
38.5 kB
/**
* Lo-fi Focus Timer — Main Script
* ═══════════════════════════════════════════════════════════════════════════
* A minimal, elegant pomodoro timer with lo-fi vibes
* Made with 💙 by Kai
* ═══════════════════════════════════════════════════════════════════════════
*/
(function () {
"use strict";
// ═══════════════════════════════════════════════════════════════════════
// CONSTANTS
// ═══════════════════════════════════════════════════════════════════════
// Mutable for custom durations
let MODES = {
focus: { time: 25, label: "Focus Time", color: "focus" },
short: { time: 5, label: "Short Break", color: "short" },
long: { time: 15, label: "Long Break", color: "long" }
};
// Ambient sounds 🌙
const AMBIENT_SOUNDS = {
rain: { name: "Rain", file: "sounds/rain.mp3" },
fire: { name: "Fire", file: "sounds/fire.mp3" },
cafe: { name: "Café", file: "sounds/cafe.mp3" },
forest: { name: "Forest", file: "sounds/forest.mp3" },
waves: { name: "Waves", file: "sounds/waves.mp3" },
thunder: { name: "Thunder", file: "sounds/thunder.mp3" }
};
const RING_CIRCUMFERENCE = 565.48; // 2 * π * 90
const SESSIONS_BEFORE_LONG_BREAK = 4;
// Radio stations 🎵
const RADIO_STATIONS = {
// === Lo-Fi & Chill ===
"lofi-girl": {
name: "☕ Lofi Girl",
url: "https://play.streamafrica.net/lofiradio"
},
chillhop: {
name: "🎧 Chillhop",
url: "https://streams.fluxfm.de/Chillhop/mp3-320"
},
"jazz-lofi": {
name: "🎷 Jazz Lo-Fi",
url: "https://streams.fluxfm.de/jazzradio/mp3-320"
},
// === FIP (Radio France) ===
"fip-groove": {
name: "🎸 FIP Groove",
url: "https://icecast.radiofrance.fr/fipgroove-midfi.mp3"
},
"fip-jazz": {
name: "🎺 FIP Jazz",
url: "https://icecast.radiofrance.fr/fipjazz-midfi.mp3"
},
"fip-electro": {
name: "🎹 FIP Electro",
url: "https://icecast.radiofrance.fr/fipelectro-midfi.mp3"
},
"fip-world": {
name: "🌍 FIP World",
url: "https://icecast.radiofrance.fr/fipworld-midfi.mp3"
},
"fip-pop": {
name: "🎤 FIP Pop",
url: "https://icecast.radiofrance.fr/fippop-midfi.mp3"
},
// === Ambient & Focus ===
"soma-drone": {
name: "🌌 SomaFM Drone",
url: "https://ice1.somafm.com/dronezone-128-mp3"
},
"soma-space": {
name: "🚀 SomaFM Space",
url: "https://ice1.somafm.com/spacestation-128-mp3"
},
"soma-groove": {
name: "🎶 SomaFM Groove",
url: "https://ice1.somafm.com/groovesalad-128-mp3"
}
};
// ═══════════════════════════════════════════════════════════════════════
// STATE
// ═══════════════════════════════════════════════════════════════════════
let state = {
mode: "focus",
timeRemaining: MODES.focus.time * 60, // in seconds
totalTime: MODES.focus.time * 60,
isRunning: false,
sessionCount: 1,
intervalId: null,
settings: {
soundEnabled: true,
autoStartBreaks: false
},
radio: {
isPlaying: false,
currentStation: "fip-groove",
volume: 0.5
},
ambient: {
active: [], // Multiple sounds can play
volume: 0.3
},
customDurations: {
focus: 25,
short: 5,
long: 15
},
notificationsEnabled: false
};
// Audio element for radio
let radioAudio = null;
// Ambient audio elements (one per sound type)
let ambientAudios = {};
// Single AudioContext for notifications (avoid memory leaks)
let audioContext = null;
// ═══════════════════════════════════════════════════════════════════════
// BROWSER NOTIFICATIONS 🔔
// ═══════════════════════════════════════════════════════════════════════
function requestNotificationPermission() {
if (!("Notification" in window)) {
console.log("🔔 Notifications not supported");
return;
}
if (Notification.permission === "granted") {
state.notificationsEnabled = true;
} else if (Notification.permission !== "denied") {
Notification.requestPermission().then(permission => {
state.notificationsEnabled = permission === "granted";
if (elements.notifToggle) {
elements.notifToggle.checked = state.notificationsEnabled;
}
saveSettings();
});
}
}
function sendNotification(title, body) {
if (!state.notificationsEnabled || Notification.permission !== "granted") return;
try {
const notif = new Notification(title, {
body: body,
icon: "data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>⚡</text></svg>",
badge: "⚡",
tag: "lofi-focus-timer",
silent: true // We have our own sound
});
// Auto-close after 5 seconds
setTimeout(() => notif.close(), 5000);
// Click to focus the window
notif.onclick = () => {
window.focus();
notif.close();
};
} catch (e) {
console.log("🔔 Notification failed:", e);
}
}
// ═══════════════════════════════════════════════════════════════════════
// AMBIENT SOUNDS 🌙
// ═══════════════════════════════════════════════════════════════════════
function initAmbient() {
// Pre-create audio elements for each sound
Object.keys(AMBIENT_SOUNDS).forEach(key => {
const audio = new Audio();
audio.loop = true;
audio.volume = state.ambient.volume;
audio.preload = "none"; // Only load when needed
ambientAudios[key] = audio;
});
loadAmbientSettings();
}
function toggleAmbientSound(soundKey) {
const audio = ambientAudios[soundKey];
const btn = document.querySelector(`.ambient-btn[data-sound="${soundKey}"]`);
if (!audio || !btn) return;
if (state.ambient.active.includes(soundKey)) {
// Stop this sound
audio.pause();
audio.currentTime = 0;
state.ambient.active = state.ambient.active.filter(s => s !== soundKey);
btn.classList.remove("active");
console.log(`🌙 Stopped: ${AMBIENT_SOUNDS[soundKey].name}`);
// Update visualizer connection
updateVisualizerConnection();
} else {
// Play this sound
audio.src = AMBIENT_SOUNDS[soundKey].file;
audio.volume = state.ambient.volume;
audio
.play()
.then(() => {
// Connect to visualizer if this is the first/only active sound
updateVisualizerConnection();
})
.catch(e => {
console.log(`🌙 Could not play ${soundKey}:`, e.message);
btn.classList.add("error");
setTimeout(() => btn.classList.remove("error"), 2000);
});
state.ambient.active.push(soundKey);
btn.classList.add("active");
console.log(`🌙 Playing: ${AMBIENT_SOUNDS[soundKey].name}`);
}
saveAmbientSettings();
}
// Connect the best audio source to visualizer 🎵⚡
function updateVisualizerConnection() {
if (typeof window.connectAudioVisualizer !== "function") return;
// Priority: Radio > First active ambient sound
if (state.radio.isPlaying && radioAudio) {
window.connectAudioVisualizer(radioAudio);
return;
}
// Try first active ambient sound
if (state.ambient.active.length > 0) {
const firstActiveKey = state.ambient.active[0];
const audio = ambientAudios[firstActiveKey];
if (audio && !audio.paused) {
window.connectAudioVisualizer(audio);
console.log(`🎵 Visualizer connected to: ${AMBIENT_SOUNDS[firstActiveKey].name}`);
return;
}
}
// Nothing playing, disconnect
if (typeof window.disconnectAudioVisualizer === "function") {
window.disconnectAudioVisualizer();
}
}
function setAmbientVolume(value) {
state.ambient.volume = value / 100;
Object.values(ambientAudios).forEach(audio => {
audio.volume = state.ambient.volume;
});
saveAmbientSettings();
}
function loadAmbientSettings() {
try {
const saved = localStorage.getItem("lofi-focus-ambient");
if (saved) {
const settings = JSON.parse(saved);
state.ambient.volume = settings.volume ?? 0.3;
if (elements.ambientVolume) {
elements.ambientVolume.value = state.ambient.volume * 100;
}
}
} catch (e) {
console.log("🌙 Could not load ambient settings");
}
}
function saveAmbientSettings() {
try {
localStorage.setItem(
"lofi-focus-ambient",
JSON.stringify({
volume: state.ambient.volume
})
);
} catch (e) {
console.log("🌙 Could not save ambient settings");
}
}
// ═══════════════════════════════════════════════════════════════════════
// CUSTOM TIMER DURATIONS 🎨
// ═══════════════════════════════════════════════════════════════════════
function updateCustomDuration(mode, value) {
const duration = parseInt(value, 10);
if (isNaN(duration) || duration < 1) return;
state.customDurations[mode] = duration;
MODES[mode].time = duration;
// Update button text
const btn = document.querySelector(`.mode-btn[data-mode="${mode}"]`);
if (btn) {
const timeSpan = btn.querySelector(".mode-time");
if (timeSpan) timeSpan.textContent = `${duration}m`;
}
// If current mode, update timer
if (state.mode === mode && !state.isRunning) {
state.totalTime = duration * 60;
state.timeRemaining = state.totalTime;
updateDisplay();
}
saveSettings();
console.log(`⏱️ ${mode} duration set to ${duration} min`);
}
// ═══════════════════════════════════════════════════════════════════════
// DOM ELEMENTS
// ═══════════════════════════════════════════════════════════════════════
const elements = {
minutes: document.getElementById("minutes"),
seconds: document.getElementById("seconds"),
sessionType: document.getElementById("session-type"),
sessionNumber: document.getElementById("session-number"),
btnStart: document.getElementById("btn-start"),
btnPause: document.getElementById("btn-pause"),
btnReset: document.getElementById("btn-reset"),
btnSettings: document.getElementById("btn-settings"),
settingsPanel: document.getElementById("settings-panel"),
soundToggle: document.getElementById("sound-toggle"),
autoStartToggle: document.getElementById("auto-start"),
progressRing: document.querySelector(".progress-ring__progress"),
timerSection: document.querySelector(".timer-section"),
modeButtons: document.querySelectorAll(".mode-btn"),
// Radio elements
btnRadio: document.getElementById("btn-radio"),
radioIcon: document.getElementById("radio-icon"),
radioStatus: document.getElementById("radio-status"),
radioSelect: document.getElementById("radio-select"),
radioVolume: document.getElementById("radio-volume"),
radioPlayer: document.querySelector(".radio-player"),
// Ambient elements
ambientButtons: document.querySelectorAll(".ambient-btn"),
ambientVolume: document.getElementById("ambient-volume"),
// Notifications & custom durations
notifToggle: document.getElementById("notif-toggle"),
focusDuration: document.getElementById("focus-duration"),
shortDuration: document.getElementById("short-duration"),
longDuration: document.getElementById("long-duration")
};
// ═══════════════════════════════════════════════════════════════════════
// RADIO PLAYER 🎵
// ═══════════════════════════════════════════════════════════════════════
function initRadio() {
radioAudio = new Audio();
radioAudio.volume = state.radio.volume;
radioAudio.crossOrigin = "anonymous";
radioAudio.addEventListener("playing", () => {
state.radio.isPlaying = true;
updateRadioUI();
console.log("🎵 Radio playing:", RADIO_STATIONS[state.radio.currentStation].name);
});
radioAudio.addEventListener("pause", () => {
state.radio.isPlaying = false;
updateRadioUI();
});
radioAudio.addEventListener("error", e => {
console.log("🎵 Radio error, trying to reconnect...", e);
state.radio.isPlaying = false;
updateRadioUI();
elements.radioStatus.textContent = "Connection error";
});
radioAudio.addEventListener("waiting", () => {
elements.radioStatus.textContent = "Buffering...";
});
radioAudio.addEventListener("canplay", () => {
if (state.radio.isPlaying) {
elements.radioStatus.textContent = "Now Playing";
}
});
// Load saved radio settings
loadRadioSettings();
}
function toggleRadio() {
if (state.radio.isPlaying) {
stopRadio();
} else {
playRadio();
}
}
function playRadio() {
const station = RADIO_STATIONS[state.radio.currentStation];
if (!station) return;
elements.radioStatus.textContent = "Connecting...";
radioAudio.src = station.url;
radioAudio
.play()
.then(() => {
// Update visualizer connection (radio has priority)
updateVisualizerConnection();
})
.catch(e => {
console.log("🎵 Autoplay blocked, user interaction needed");
elements.radioStatus.textContent = "Click to play";
});
}
function stopRadio() {
radioAudio.pause();
radioAudio.src = "";
state.radio.isPlaying = false;
updateRadioUI();
// Update visualizer (might switch to ambient if playing)
updateVisualizerConnection();
console.log("🎵 Radio stopped");
}
function changeStation(stationId) {
state.radio.currentStation = stationId;
saveRadioSettings();
if (state.radio.isPlaying) {
playRadio();
}
}
function setVolume(value) {
state.radio.volume = value / 100;
if (radioAudio) {
radioAudio.volume = state.radio.volume;
}
saveRadioSettings();
}
function updateRadioUI() {
if (state.radio.isPlaying) {
elements.btnRadio.classList.add("playing");
elements.radioPlayer.classList.add("playing");
elements.radioStatus.classList.add("playing");
elements.radioIcon.textContent = "🔊";
elements.radioStatus.textContent = "Now Playing";
} else {
elements.btnRadio.classList.remove("playing");
elements.radioPlayer.classList.remove("playing");
elements.radioStatus.classList.remove("playing");
elements.radioIcon.textContent = "🎵";
elements.radioStatus.textContent = "Radio Off";
}
}
function loadRadioSettings() {
try {
const saved = localStorage.getItem("lofi-focus-radio");
if (saved) {
const settings = JSON.parse(saved);
state.radio = { ...state.radio, ...settings };
elements.radioSelect.value = state.radio.currentStation;
elements.radioVolume.value = state.radio.volume * 100;
if (radioAudio) {
radioAudio.volume = state.radio.volume;
}
}
} catch (e) {
console.log("🎵 Could not load radio settings");
}
}
function saveRadioSettings() {
try {
localStorage.setItem(
"lofi-focus-radio",
JSON.stringify({
currentStation: state.radio.currentStation,
volume: state.radio.volume
})
);
} catch (e) {
console.log("🎵 Could not save radio settings");
}
}
// ═══════════════════════════════════════════════════════════════════════
// AUDIO (simple beep using Web Audio API)
// ═══════════════════════════════════════════════════════════════════════
function getAudioContext() {
if (!audioContext || audioContext.state === "closed") {
audioContext = new (window.AudioContext || window.webkitAudioContext)();
}
// Resume if suspended (browser autoplay policy)
if (audioContext.state === "suspended") {
audioContext.resume();
}
return audioContext;
}
function playBeep(frequency = 800, duration = 0.3, delay = 0) {
try {
const ctx = getAudioContext();
const startTime = ctx.currentTime + delay;
const oscillator = ctx.createOscillator();
const gainNode = ctx.createGain();
oscillator.connect(gainNode);
gainNode.connect(ctx.destination);
oscillator.frequency.value = frequency;
oscillator.type = "sine";
gainNode.gain.setValueAtTime(0.3, startTime);
gainNode.gain.exponentialRampToValueAtTime(0.01, startTime + duration);
oscillator.start(startTime);
oscillator.stop(startTime + duration);
} catch (e) {
console.log("⚡ Audio not supported");
}
}
function playNotificationSound() {
if (!state.settings.soundEnabled) return;
// Play 3 beeps with slight delays
playBeep(800, 0.3, 0);
playBeep(800, 0.3, 0.2);
playBeep(1000, 0.4, 0.4); // Last beep slightly higher
}
// ═══════════════════════════════════════════════════════════════════════
// TIMER LOGIC
// ═══════════════════════════════════════════════════════════════════════
function startTimer() {
if (state.isRunning) return;
state.isRunning = true;
elements.timerSection.classList.remove("timer-paused");
updateControlButtons();
state.intervalId = setInterval(() => {
state.timeRemaining--;
if (state.timeRemaining <= 0) {
timerComplete();
} else {
updateDisplay();
}
}, 1000);
console.log("⚡ Timer started!");
}
function pauseTimer() {
if (!state.isRunning) return;
state.isRunning = false;
elements.timerSection.classList.add("timer-paused");
clearInterval(state.intervalId);
updateControlButtons();
console.log("⏸ Timer paused");
}
function resetTimer() {
pauseTimer();
state.timeRemaining = state.totalTime;
updateDisplay();
console.log("↺ Timer reset");
}
function timerComplete() {
pauseTimer();
playNotificationSound();
// Send browser notification 🔔
const modeLabel = MODES[state.mode].label;
if (state.mode === "focus") {
sendNotification("⚡ Focus Complete!", "Time for a break. You earned it!");
} else {
sendNotification("☕ Break Over!", "Ready to focus again?");
}
console.log("🎉 Timer complete!");
// Determine next mode
if (state.mode === "focus") {
// Long break every 4 completed focus sessions
if (state.sessionCount % SESSIONS_BEFORE_LONG_BREAK === 0) {
setMode("long");
} else {
setMode("short");
}
// Increment session count AFTER deciding break type
state.sessionCount++;
} else {
// After any break, go back to focus
setMode("focus");
}
// Auto-start if enabled
if (state.settings.autoStartBreaks) {
setTimeout(startTimer, 1000);
}
}
// ═══════════════════════════════════════════════════════════════════════
// MODE MANAGEMENT
// ═══════════════════════════════════════════════════════════════════════
function setMode(mode) {
if (!MODES[mode]) return;
// If timer is running, pause first
if (state.isRunning) {
pauseTimer();
}
state.mode = mode;
state.totalTime = MODES[mode].time * 60;
state.timeRemaining = state.totalTime;
// Update body attribute for CSS theming
document.body.setAttribute("data-mode", mode);
// Update session type label
elements.sessionType.textContent = MODES[mode].label;
// Update mode buttons
elements.modeButtons.forEach(btn => {
btn.classList.toggle("active", btn.dataset.mode === mode);
});
// Update visualizer colors! 🎨
if (typeof window.setVisualizerMode === "function") {
window.setVisualizerMode(mode);
}
updateDisplay();
console.log(`⚡ Mode changed to: ${mode}`);
}
// ═══════════════════════════════════════════════════════════════════════
// DISPLAY UPDATES
// ═══════════════════════════════════════════════════════════════════════
function updateDisplay() {
const minutes = Math.floor(state.timeRemaining / 60);
const seconds = state.timeRemaining % 60;
elements.minutes.textContent = String(minutes).padStart(2, "0");
elements.seconds.textContent = String(seconds).padStart(2, "0");
elements.sessionNumber.textContent = state.sessionCount;
updateProgressRing();
updateDocumentTitle(minutes, seconds);
}
function updateProgressRing() {
const progress = state.timeRemaining / state.totalTime;
const offset = RING_CIRCUMFERENCE * (1 - progress);
elements.progressRing.style.strokeDashoffset = offset;
}
function updateDocumentTitle(minutes, seconds) {
const timeStr = `${String(minutes).padStart(2, "0")}:${String(seconds).padStart(2, "0")}`;
const modeEmoji = state.mode === "focus" ? "⚡" : "☕";
document.title = `${timeStr} ${modeEmoji} Lo-fi Focus`;
}
function updateControlButtons() {
if (state.isRunning) {
elements.btnStart.classList.add("hidden");
elements.btnPause.classList.remove("hidden");
} else {
elements.btnStart.classList.remove("hidden");
elements.btnPause.classList.add("hidden");
}
}
// ═══════════════════════════════════════════════════════════════════════
// SETTINGS
// ═══════════════════════════════════════════════════════════════════════
function toggleSettings() {
elements.settingsPanel.classList.toggle("hidden");
}
function loadSettings() {
try {
const saved = localStorage.getItem("lofi-focus-settings");
if (saved) {
const parsed = JSON.parse(saved);
state.settings = { ...state.settings, ...parsed };
elements.soundToggle.checked = state.settings.soundEnabled;
elements.autoStartToggle.checked = state.settings.autoStartBreaks;
}
// Load custom durations
const durations = localStorage.getItem("lofi-focus-durations");
if (durations) {
state.customDurations = { ...state.customDurations, ...JSON.parse(durations) };
// Apply to MODES
MODES.focus.time = state.customDurations.focus;
MODES.short.time = state.customDurations.short;
MODES.long.time = state.customDurations.long;
// Update inputs
if (elements.focusDuration) elements.focusDuration.value = state.customDurations.focus;
if (elements.shortDuration) elements.shortDuration.value = state.customDurations.short;
if (elements.longDuration) elements.longDuration.value = state.customDurations.long;
// Update button labels
document.querySelectorAll(".mode-btn").forEach(btn => {
const mode = btn.dataset.mode;
const timeSpan = btn.querySelector(".mode-time");
if (timeSpan && state.customDurations[mode]) {
timeSpan.textContent = `${state.customDurations[mode]}m`;
}
});
}
// Load notification preference
const notifPref = localStorage.getItem("lofi-focus-notif");
if (notifPref === "true" && Notification.permission === "granted") {
state.notificationsEnabled = true;
if (elements.notifToggle) elements.notifToggle.checked = true;
}
} catch (e) {
console.log("⚡ Could not load settings, using defaults");
}
}
function saveSettings() {
try {
localStorage.setItem("lofi-focus-settings", JSON.stringify(state.settings));
localStorage.setItem("lofi-focus-durations", JSON.stringify(state.customDurations));
localStorage.setItem("lofi-focus-notif", state.notificationsEnabled.toString());
} catch (e) {
console.log("⚡ Could not save settings");
}
}
// ═══════════════════════════════════════════════════════════════════════
// EVENT LISTENERS
// ═══════════════════════════════════════════════════════════════════════
function setupEventListeners() {
// Timer controls
elements.btnStart.addEventListener("click", startTimer);
elements.btnPause.addEventListener("click", pauseTimer);
elements.btnReset.addEventListener("click", resetTimer);
// Mode buttons
elements.modeButtons.forEach(btn => {
btn.addEventListener("click", () => {
setMode(btn.dataset.mode);
});
});
// Settings
elements.btnSettings.addEventListener("click", toggleSettings);
elements.soundToggle.addEventListener("change", e => {
state.settings.soundEnabled = e.target.checked;
saveSettings();
});
elements.autoStartToggle.addEventListener("change", e => {
state.settings.autoStartBreaks = e.target.checked;
saveSettings();
});
// Radio controls 🎵
elements.btnRadio.addEventListener("click", toggleRadio);
elements.radioSelect.addEventListener("change", e => {
changeStation(e.target.value);
});
elements.radioVolume.addEventListener("input", e => {
setVolume(e.target.value);
});
// Ambient sounds 🌙
elements.ambientButtons.forEach(btn => {
btn.addEventListener("click", () => {
toggleAmbientSound(btn.dataset.sound);
});
});
if (elements.ambientVolume) {
elements.ambientVolume.addEventListener("input", e => {
setAmbientVolume(e.target.value);
});
}
// Browser notifications 🔔
if (elements.notifToggle) {
elements.notifToggle.addEventListener("change", e => {
if (e.target.checked) {
requestNotificationPermission();
} else {
state.notificationsEnabled = false;
saveSettings();
}
});
}
// Custom durations 🎨
if (elements.focusDuration) {
elements.focusDuration.addEventListener("change", e => {
updateCustomDuration("focus", e.target.value);
});
}
if (elements.shortDuration) {
elements.shortDuration.addEventListener("change", e => {
updateCustomDuration("short", e.target.value);
});
}
if (elements.longDuration) {
elements.longDuration.addEventListener("change", e => {
updateCustomDuration("long", e.target.value);
});
}
// Keyboard shortcuts
document.addEventListener("keydown", e => {
// Ignore if typing in an input
if (e.target.tagName === "INPUT" || e.target.tagName === "SELECT") return;
// Space to start/pause
if (e.code === "Space") {
e.preventDefault();
state.isRunning ? pauseTimer() : startTimer();
}
// R to reset
if (e.code === "KeyR") {
e.preventDefault();
resetTimer();
}
// 1, 2, 3 for modes
if (e.code === "Digit1") setMode("focus");
if (e.code === "Digit2") setMode("short");
if (e.code === "Digit3") setMode("long");
// M to toggle music/radio
if (e.code === "KeyM") {
e.preventDefault();
toggleRadio();
}
});
}
// ═══════════════════════════════════════════════════════════════════════
// INITIALIZATION
// ═══════════════════════════════════════════════════════════════════════
function init() {
loadSettings();
initRadio();
initAmbient();
setupEventListeners();
setMode("focus");
updateDisplay();
setupAboutModal();
console.log("⚡ Lo-fi Focus Timer initialized!");
console.log("💙 Made with love by Kai");
console.log("─────────────────────────────────");
console.log("Keyboard shortcuts:");
console.log(" [Space] Start/Pause timer");
console.log(" [R] Reset timer");
console.log(" [1] Focus mode");
console.log(" [2] Short break");
console.log(" [3] Long break");
console.log(" [M] Toggle radio 🎵");
console.log("─────────────────────────────────");
console.log("🌙 Ambient sounds ready");
console.log("🔔 Browser notifications: " + (Notification.permission === "granted" ? "enabled" : "click to enable"));
}
// ═══════════════════════════════════════════════════════════════════════
// ABOUT MODAL ⚡
// ═══════════════════════════════════════════════════════════════════════
function setupAboutModal() {
const modal = document.getElementById("about-modal");
const btnAbout = document.getElementById("btn-about");
const btnAbout2 = document.getElementById("btn-about-2");
const btnClose = document.getElementById("modal-close");
const overlay = modal?.querySelector(".modal-overlay");
if (!modal) return;
function openModal(e) {
e.preventDefault();
modal.classList.remove("hidden");
document.body.style.overflow = "hidden";
}
function closeModal() {
modal.classList.add("hidden");
document.body.style.overflow = "";
}
btnAbout?.addEventListener("click", openModal);
btnAbout2?.addEventListener("click", openModal);
btnClose?.addEventListener("click", closeModal);
overlay?.addEventListener("click", closeModal);
// Close on Escape key
document.addEventListener("keydown", e => {
if (e.key === "Escape" && !modal.classList.contains("hidden")) {
closeModal();
}
});
}
// Start the app
init();
})();