Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(timer): add timer-pause #9

Merged
merged 1 commit into from
Jan 23, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion src/components/elements/Timer.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,11 @@

if (timer.state === TimerState.running && interval === undefined)
interval = setInterval(updateTime, 100, undefined);
else if (timer.state === TimerState.stopped && interval >= 0) {
else if (
(timer.state === TimerState.reset ||
timer.state === TimerState.paused) &&
interval >= 0
) {
clearInterval(interval);
interval = undefined;
updateTime();
Expand Down
21 changes: 14 additions & 7 deletions src/components/layout/Navbar.svelte
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
<script lang="ts">
import { MenuIcon, XIcon, PlayIcon, PauseIcon } from 'svelte-feather-icons';
import Timer from '@components/elements/Timer.svelte';
import ThemePicker from '@components/elements/ThemePicker.svelte';
import Timer from '@components/elements/Timer.svelte';
import { TimerState } from '@lib/timer';
import activeSession from '@stores/activeSession';
import sidebarOpened from '@stores/sidebarOpened';
import {
MenuIcon,
PauseIcon,
PlayIcon,
RotateCcwIcon,
XIcon,
} from 'svelte-feather-icons';

function toggleTimer() {
activeSession.updateSelf(session => session.timer.toggle());
}

const pauseTimer = () => activeSession.updateSelf(x => x.timer.pause());
const resetTimer = () => activeSession.updateSelf(x => x.timer.reset());
function toggleSidebar() {
$sidebarOpened = !$sidebarOpened;
}
Expand All @@ -29,13 +33,16 @@
<div class="timer">
<ThemePicker />

<button on:click={toggleTimer}>
<button on:click={pauseTimer}>
{#if $activeSession.timer.state === TimerState.running}
<PauseIcon />
{:else}
<PlayIcon />
{/if}
</button>
<button on:click={resetTimer}>
<RotateCcwIcon />
</button>
<Timer />
</div>
</div>
Expand Down
61 changes: 41 additions & 20 deletions src/lib/timer.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
export enum TimerState {
stopped,
paused,
reset,
running,
}

export default class Timer {
private _startTimestamp: number;
private _stopTimestamp: number;
private _state = TimerState.stopped;
private _pauseStart: number;
private _pauseTimes: number[] = [];
private _state = TimerState.reset;

/** The timer`s current state */
get state() {
Expand All @@ -26,43 +29,61 @@ export default class Timer {
return this._startTimestamp;
}

/** Toggles pause state, starts the timer if reset */
pause() {
// start
if (this._state === TimerState.reset) return this.start();

// unpause
if (this._state === TimerState.paused) return this._unpause();

// pause
this._pause();
}

private _pause() {
this._state = TimerState.paused;
this._pauseStart = new Date().valueOf();
}
private _unpause() {
this._pauseTimes.push(new Date().valueOf() - this._pauseStart);
this._pauseStart = undefined;

this._state = TimerState.running;
}

/**
* Stops the timer.
* @returns the timer´s run duration equal to `getRunDuration()` or -1 if failed
*/
stop() {
if (this.state === TimerState.stopped) return -1;
if (this.state === TimerState.reset) return -1;

this._stopTimestamp = new Date().valueOf();
this._state = TimerState.stopped;
this._state = TimerState.reset;

return this.getRunDuration();
}

toggle() {
switch (this.state) {
case TimerState.running:
this.stop();
break;
case TimerState.stopped:
this.reset();
this.start();
break;
}
}

/** Resets the timer. */
reset() {
this._startTimestamp = undefined;
this._stopTimestamp = undefined;
this._state = TimerState.stopped;
this._pauseTimes = [];
this._pauseStart = undefined;
this._state = TimerState.reset;
}

/** @returns the total timer`s run duration */
getRunDuration(): number {
return (
(this._stopTimestamp ?? new Date().valueOf()) - this._startTimestamp
);
const now = new Date().valueOf();
// calculate pauses
let totalPause = this._pauseTimes.reduce((prev, x) => prev + x, 0);

if (this.state === TimerState.paused)
totalPause += now - this._pauseStart;

return (this._stopTimestamp ?? now) - this._startTimestamp - totalPause;
}

/** Creates a new timer from an json-like object */
Expand Down
55 changes: 41 additions & 14 deletions test/timer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ describe('Timer', () => {
});

it('starting', () => {
expect(timer.state).toBe(TimerState.stopped);
expect(timer.state).toBe(TimerState.reset);

const startTimestamp = timer.start();

Expand All @@ -23,7 +23,7 @@ describe('Timer', () => {

expect(timer['_stopTimestamp']).toBe(startTimestamp + runDuration);

expect(timer.state).toBe(TimerState.stopped);
expect(timer.state).toBe(TimerState.reset);
expect(runDuration).toBe(timer.getRunDuration());
});

Expand Down Expand Up @@ -52,7 +52,7 @@ describe('Timer', () => {
const runDuration = timer.stop();
const secondRunDuration = timer.stop();

expect(timer.state).toBe(TimerState.stopped);
expect(timer.state).toBe(TimerState.reset);

expect(startTimestamp).toBeTypeOf('number');
expect(startTimestamp).toBeTruthy();
Expand All @@ -68,26 +68,53 @@ describe('Timer', () => {
timer.start();
timer.reset();

expect(timer.state).toBe(TimerState.stopped);
expect(timer.state).toBe(TimerState.reset);
expect(timer['_startTimestamp']).toBeUndefined();
expect(timer['_stopTimestamp']).toBeUndefined();
});

it('toggle', () => {
timer.toggle();
it('pause (states)', () => {
timer.pause(); // start
expect(timer.state).toBe(TimerState.running);
timer.toggle();
expect(timer['_startTimestamp']).toBeTruthy();
expect(timer['_pauseStart']).toBeUndefined();
expect(timer['_stopTimestamp']).toBeUndefined();

expect(timer.state).toBe(TimerState.stopped);
timer.pause(); // pause
expect(timer.state).toBe(TimerState.paused);
expect(timer['_startTimestamp']).toBeTruthy();
let oldStartTimestamp = timer['_startTimestamp'];
expect(timer['_stopTimestamp']).toBeTruthy();
expect(timer['_pauseStart']).toBeTruthy();
expect(timer['_pauseTimes'].length).toBe(0);
expect(timer['_stopTimestamp']).toBeUndefined();

timer.toggle();
timer.pause(); // unpause
expect(timer.state).toBe(TimerState.running);
expect(timer['_startTimestamp']).toBeGreaterThanOrEqual(
oldStartTimestamp
);
expect(timer['_startTimestamp']).toBeTruthy();
expect(timer['_pauseStart']).toBeUndefined();
expect(timer['_pauseTimes'].length).toBe(1);
expect(timer['_stopTimestamp']).toBeUndefined();
});

it('pause (runtime calculation)', () => {
const timerAdvanceTimes = [1214, 7352, 4739];

vi.useFakeTimers({ now: 0, toFake: ['Date'] });
timer.pause(); // start

vi.advanceTimersByTime(timerAdvanceTimes[0]);
expect(timer.getRunDuration()).toBe(timerAdvanceTimes[0]);

timer.pause(); // pause

vi.advanceTimersByTime(timerAdvanceTimes[1]);
expect(timer.getRunDuration()).toBe(timerAdvanceTimes[0]);

timer.pause(); // unpause
vi.advanceTimersByTime(timerAdvanceTimes[2]);
expect(timer.getRunDuration()).toBe(
timerAdvanceTimes[0] + timerAdvanceTimes[2]
);

vi.useRealTimers();
});
});