feat: barebones pomodoro timer

This commit is contained in:
2026-04-03 14:12:27 +02:00
parent db8e39f672
commit a4592a1869
4 changed files with 156 additions and 0 deletions

View File

@@ -6,6 +6,7 @@
import Window from '../ui/window/window.svelte';
import TodoList from './todo-list.svelte';
import Twentytwentytwenty from './twentytwentytwenty.svelte';
import Pomodoro from './pomodoro.svelte';
// svelte-ignore non_reactive_update
let audioElement: HTMLAudioElement;
@@ -260,4 +261,16 @@
show={appState.show202020}
>
<Twentytwentytwenty></Twentytwentytwenty>
</Window>
<Window
title="Pomodoro Timer"
showTitleBar={true}
showCloseButton={true}
width={320}
height={250}
onClose={() => appState.showPomodoro = false}
show={appState.showPomodoro}
>
<Pomodoro></Pomodoro>
</Window>

View File

@@ -3,6 +3,7 @@
import Check from '@lucide/svelte/icons/check';
import Button from '../ui/button/button.svelte';
import Binoculars from '@lucide/svelte/icons/binoculars';
import Clock from '@lucide/svelte/icons/clock';
</script>
<div
@@ -17,4 +18,10 @@
>
<Binoculars class="size-4" />
</Button>
<Button
size="icon"
onclick={() => (appState.showPomodoro = !appState.showPomodoro)}
>
<Clock class="size-4" />
</Button>
</div>

View File

@@ -0,0 +1,130 @@
<script lang="ts">
import Button from '@/components/ui/button/button.svelte';
import { state as appState } from '@/state.svelte';
import { onMount } from 'svelte';
let timeLeft = $state(0);
let intervalHandle: ReturnType<typeof setInterval> | null = null;
let startSoundElement: HTMLAudioElement;
let endSoundElement: HTMLAudioElement;
let minutes = $derived(Math.floor(timeLeft / 60));
let seconds = $derived(timeLeft % 60);
onMount(() => {
const defaultFirst = 20 * 60;
const defaultSecond = 5 * 60;
if (!appState.pomodoroTimer) appState.pomodoroTimer = defaultFirst;
if (!appState.pomodoroBreakTimer) appState.pomodoroBreakTimer = defaultSecond;
timeLeft = appState.pomodoroTimer;
});
function playSound(element: HTMLAudioElement) {
if (!element) return;
element.currentTime = 0;
const playPromise = element.play();
if (playPromise) {
playPromise.catch((error) => {
console.error('Audio play error:', error);
if (error.name === 'NotAllowedError') {
console.warn('Audio playback blocked by browser. User interaction required.');
}
});
}
}
function startCountdown() {
if (intervalHandle) {
clearInterval(intervalHandle);
intervalHandle = null;
}
intervalHandle = setInterval(() => {
if (timeLeft > 0) {
timeLeft--;
} else {
if (appState.pomodoroWorkPhase) {
appState.pomodoroWorkPhase = false;
timeLeft = appState.pomodoroBreakTimer;
playSound(startSoundElement);
} else {
appState.pomodoroWorkPhase = true;
timeLeft = appState.pomodoroTimer;
playSound(endSoundElement);
appState.isPomodoroActive = false;
}
}
}, 1000);
}
$effect(() => {
if (appState.isPomodoroActive) {
startCountdown();
} else if (intervalHandle) {
clearInterval(intervalHandle);
intervalHandle = null;
}
return () => {
if (intervalHandle) {
clearInterval(intervalHandle);
intervalHandle = null;
appState.isPomodoroActive = false;
}
};
});
function startTimer() {
reset();
timeLeft = appState.pomodoroTimer;
appState.pomodoroWorkPhase = true;
appState.isPomodoroActive = true;
}
function stopTimer() {
reset();
appState.isPomodoroActive = false;
}
function reset() {
appState.pomodoroTimer = 20 * 60;
appState.pomodoroBreakTimer = 5 * 60;
}
</script>
<audio
bind:this={startSoundElement}
src="https://lofi-cdn.srizan.dev/assets/202020/start.mp3"
preload="auto"
></audio>
<audio
bind:this={endSoundElement}
src="https://lofi-cdn.srizan.dev/assets/202020/done.mp3"
preload="auto"
></audio>
<div class="flex flex-col p-4">
<div class="mb-6">
<div class="text-2xl font-bold">
{appState.pomodoroWorkPhase ? 'Work Time' : 'Break Time'}
</div>
<div class="text-3xl font-bold mt-2">
{minutes.toString().padStart(2, '0')}:{seconds.toString().padStart(2, '0')}
</div>
</div>
<div class="flex gap-2">
{#if appState.isPomodoroActive}
<Button variant="destructive" onclick={stopTimer}>Stop</Button>
{:else}
<Button onclick={startTimer}>Start</Button>
{/if}
</div>
</div>

View File

@@ -31,6 +31,12 @@ export const state = $state({
workRuleTimer: 20 * 60,
restRuleTimer: 20,
showPomodoro: false,
pomodoroTimer: 25 * 60,
pomodoroBreakTimer: 5 * 60,
isPomodoroActive: false,
pomodoroWorkPhase: true,
// in daemon.svelte
togglePlay: (() => {}) as () => void,
});