Skip to content

Commit

Permalink
Merge pull request #14 from asherhe/dev
Browse files Browse the repository at this point in the history
add timer settings
  • Loading branch information
asherhe authored Apr 10, 2024
2 parents 25d5a87 + 7dfddcc commit 0bc3b6d
Show file tree
Hide file tree
Showing 9 changed files with 441 additions and 26 deletions.
73 changes: 59 additions & 14 deletions src/components/app.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import React, { useState, useEffect, useCallback, useMemo } from "react";
import TimerDisplay from "./timer-display";
import TimerControls from "./timer-controls";
import TypeControls from "./type-controls";
import { TimerConfig, defaultConfig } from "./timer-config";

import TimerWorker from "../timer-worker";
import buildWorker from "../worker-builder";
Expand All @@ -17,12 +18,19 @@ class TimerType {
this.breakCount = 0;
}

duration() {
/**
*
* @param {import("./timer-config").config} config
* @returns {number} the duration of the timer's current state
*/
duration(config) {
switch (this.state) {
case "work":
return 1500;
return config.work;
case "break":
return this.breakCount % 4 === 3 ? 900 : 300;
return (this.breakCount + 1) % config.longfreq === 0
? config.longbreak
: config.break;
default:
return -1;
}
Expand All @@ -49,28 +57,42 @@ class TimerType {
* @returns {React.ReactNode}
*/
function App(props) {
/** @type {[DOMHighResTimeStamp?, React.SetStateAction<DOMHighResTimeStamp?>]} */
/**
* app config
* @type {[import("./timer-config").config, React.SetStateAction<import("./timer-config").config>]}
*/
const [config, setConfig] = useState(defaultConfig);

/**
* timestamp of when the timer was started
* @type {[DOMHighResTimeStamp?, React.SetStateAction<DOMHighResTimeStamp?>]}
*/
const [timerStart, setTimerStart] = useState(undefined);
const [elapsed, setElapsed] = useState(0);
const [timerType, setTimerType] = useState(new TimerType("work"));
const [duration, setDuration] = useState(timerType.duration());
const [duration, setDuration] = useState(timerType.duration(config));

// timer worker (runs in background)
const worker = useMemo(() => {
let worker = buildWorker(TimerWorker);
worker.postMessage([performance.timeOrigin]); // post time origin
return worker;
}, []);

// time update from worker
worker.onmessage = (e) => {
let elapsedTime = e.data;
// console.log(elapsedTime);
if (elapsedTime !== elapsed) {
if (elapsedTime >= duration) timerFinish();
else setElapsed(elapsedTime);
}
};

const bell = useMemo(() => new Audio(`${process.env.PUBLIC_URL}/bell.mp3`), []);
// load the bell sound
const bell = useMemo(
() => new Audio(`${process.env.PUBLIC_URL}/bell.mp3`),
[]
);

const isPlaying = useCallback(() => {
return timerStart !== undefined;
Expand All @@ -87,22 +109,28 @@ function App(props) {
};

const restart = () => {
setDuration(timerType.duration());
setDuration(timerType.duration(config));
if (isPlaying()) setTimerStart(performance.now());
};

const timerFinish = useCallback(() => {
// make a new notification
const notifyFinish = () => {
new Notification("Timer done", {
body: timerType.state === "work" ? "It's time to get back to work!" : "It's time to take a break!",
body:
timerType.state === "work"
? "It's time to get back to work!"
: "It's time to take a break!",
});
};

// reset timer
setTimerStart(undefined);
setElapsed(0);
setTimerType(timerType.next());
setDuration(timerType.duration());
setDuration(timerType.duration(config));

// send notification
if (!("Notification" in window)) {
alert("Timer done!");
} else if (Notification.permission === "granted") {
Expand All @@ -115,30 +143,47 @@ function App(props) {
});
}
bell.play();
}, [timerType, bell]);

if (config.autoStart) {
play();
}
}, [timerType, bell, config]);

// update time for woker
useEffect(() => {
worker.postMessage(timerStart);
}, [timerStart, worker]);

// update timer duration if config is updated and timer is not running
useEffect(() => {
if (!isPlaying()) setDuration(timerType.duration(config));
}, [config, isPlaying, timerType]);

let playing = isPlaying();
return (
<div className={"timer-app" + (timerType.state === "break" ? " break" : "")}>
<div
className={"timer-app" + (timerType.state === "break" ? " break" : "")}
>
<div className="timer-content">
<TimerDisplay time={duration - elapsed} />
<TimerControls mode={playing ? "pause" : "play"} onplay={playing ? pause : play} onrestart={restart} />
<TimerControls
mode={playing ? "pause" : "play"}
onplay={playing ? pause : play}
onrestart={restart}
/>
<TypeControls
type={timerType.state}
setType={(type) => {
let newType = new TimerType(type);
setTimerStart(undefined);
setElapsed(0);
setTimerType(newType);
setDuration(newType.duration());
setDuration(newType.duration(config));
}}
active={!playing}
/>
</div>
<TimerConfig config={config} setConfig={setConfig} />
</div>
);
}
Expand Down
20 changes: 20 additions & 0 deletions src/components/checkbox.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { useCallback } from "react";

/**
*
* @param {{val: boolean, setVal: (val: boolean) => void}} props
*/
function CheckBox({ val, setVal }) {
const onClick = useCallback(() => setVal(!val), [val, setVal]);

return (
<input
className="input-toggle"
type="checkbox"
defaultChecked={val}
onClick={onClick}
/>
);
}

export default CheckBox;
35 changes: 35 additions & 0 deletions src/components/number-input.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { useCallback } from "react";

/**
*
* @param {{val: number, setVal: (val: number) => void, min: number, max: number, step: number}} props
*/
function NumberInput({
val,
setVal,
min = undefined,
max = undefined,
step = 1,
}) {
const onChange = useCallback(
(e) => {
let v = parseFloat(e.target.value);
setVal(v);
},
[setVal]
);

return (
<input
className="input-number"
type="number"
value={val}
min={min}
max={max}
step={step}
onChange={onChange}
/>
);
}

export default NumberInput;
108 changes: 108 additions & 0 deletions src/components/timer-config.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import { useCallback, useState } from "react";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faGear } from "@fortawesome/free-solid-svg-icons";
import NumberInput from "./number-input";
import CheckBox from "./checkbox";

/**
* @typedef {{work: number, break: number, longbreak: number, longfreq: number, autoStart: boolean }} config
* @type {config}
*/
export const defaultConfig = {
work: 1500,
break: 300,
longbreak: 900,
longfreq: 4,
autoStart: false,
};

/**
* @param {{config: config, setConfig: (v: config) => void}} props
*/
export function TimerConfig({ config, setConfig }) {
const [show, setShow] = useState(false);

/**
* sets some config property `prop` to `val`
* @param {string} prop
* @param {*} val
*/
const setProp = useCallback(
(prop, val) => {
let c = structuredClone(config);
c[prop] = val;
setConfig(c);
},
[config, setConfig]
);

return (
<div className={"timer-config" + (show ? " show" : "")}>
<div className="timer-config-button" onClick={() => setShow(!show)}>
<FontAwesomeIcon icon={faGear} />
</div>
<div className="timer-config-menu">
<div className="timer-config-title">Settings</div>
<div className="timer-config-list">
<div>
<span className="timer-config-list-title">Work duration:</span>
<span>
<NumberInput
val={config.work / 60}
setVal={(v) => setProp("work", v * 60)}
min="1"
/>
&nbsp;minutes
</span>
</div>
<div>
<span className="timer-config-list-title">Break duration:</span>
<span>
<NumberInput
val={config.break / 60}
setVal={(v) => setProp("break", v * 60)}
min="1"
/>
&nbsp;minutes
</span>
</div>
<div>
<span className="timer-config-list-title">
Long break duration:
</span>
<span>
<NumberInput
val={config.longbreak / 60}
setVal={(v) => setProp("longbreak", v * 60)}
min="1"
/>
&nbsp;minutes
</span>
</div>
<div>
<span className="timer-config-list-title">
Long break frequency:
</span>
<span>
<NumberInput
val={config.longfreq}
setVal={(v) => setProp("longfreq", v)}
min="0"
/>
&nbsp;breaks
</span>
</div>
<div>
<span className="timer-config-list-title">
Auto start next timer?
</span>
<CheckBox
val={config.autoStart}
setVal={(v) => setProp("autoStart", v)}
/>
</div>
</div>
</div>
</div>
);
}
7 changes: 5 additions & 2 deletions src/components/timer-controls.jsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faPlay, faPause, faArrowRotateLeft } from "@fortawesome/free-solid-svg-icons";
import {
faPlay,
faPause,
faArrowRotateLeft,
} from "@fortawesome/free-solid-svg-icons";

/**
* @param {{mode: "play" | "pause", onplay: () => void, onrestart: () => void}} props
* @returns
*/
function TimerControls({ mode, onplay, onrestart }) {
return (
Expand Down
13 changes: 7 additions & 6 deletions src/components/timer-display.jsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import React from "react";

/**
* @param {{time: number}} props
* @returns {React.ReactNode}
*/
function TimerDisplay({ time }) {
let timeStr = `${Math.floor(time / 60)
.toString()
.padStart(2, "0")}:${(time % 60).toString().padStart(2, "0")}`;
let secs = time % 60,
mins = Math.floor(time / 60) % 60,
hrs = Math.floor(time / 3600);
secs = secs.toString().padStart(2, "0");
mins = mins.toString().padStart(2, "0");
hrs = hrs ? hrs.toString().padStart(2, "0") + ":" : "";
let timeStr = `${hrs}${mins}:${secs}`;
document.title = `${timeStr} - timeato`;
return <span className="timer-display">{timeStr}</span>;
}
Expand Down
2 changes: 0 additions & 2 deletions src/components/type-controls.jsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
/**
*
* @param {{type: "work" | "break", setType: (value: "work" | "break") => void, active: boolean}} props
* @returns
*/
function TypeControls({ type, setType, active }) {
return (
Expand Down
Loading

0 comments on commit 0bc3b6d

Please sign in to comment.