Skip to content
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
5 changes: 5 additions & 0 deletions .changeset/neat-games-rescue.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'studytimer.io': minor
---

Add audio alerts for timer events and settings customization.
141 changes: 133 additions & 8 deletions src/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,30 @@ import { Router } from '@lit-labs/router';
import { LitElement, css, html, nothing } from 'lit';

import { notificationApiService } from './services/notification-api.service.js';
import { webAudioApiService } from './services/web-audio-api.service.js';
import { buttonStyles } from './shared/styles/buttonStyles.js';
import { captionTextStyles } from './shared/styles/captionTextStyles.js';
import { linkStyles } from './shared/styles/linkStyles.js';
import { modalStyles } from './shared/styles/modalStyles.js';
import { tooltipStyles } from './shared/styles/tooltipStyles.js';
import { appStore } from './stores/app.js';
import { DEFAULT_SETTINGS, settingsStore } from './stores/settings.js';
import {
AUDIO_SOUND,
AUDIO_VOLUME,
CLIENT_ERROR_MESSAGE,
NOTIFICATION_PERMISSION,
SETTINGS_EVENT,
STORAGE_KEY_NAMESPACE,
} from './utils/constants.js';
import { isBool, isNum } from './utils/helpers.js';
import { isBool, isNum, toSentenceCase } from './utils/helpers.js';

import './components/app-top-bar.js';
import './components/header.js';

const AUDIO_SOUNDS = Object.freeze(Object.values(AUDIO_SOUND));
const AUDIO_VOLUMES = Object.freeze(Object.values(AUDIO_VOLUME));

const POMODORO_RESOURCE_LINKS = Object.freeze({
WIKI: 'https://en.wikipedia.org/wiki/Pomodoro_Technique',
VIDEO: 'https://youtu.be/dC4ZYCiRF_w?si=ekRqmmWpnqrAZM-c&t=8',
Expand Down Expand Up @@ -85,6 +92,11 @@ export class App extends LitElement {

connectedCallback() {
super.connectedCallback();
window.addEventListener('click', this.#initAudio, { once: true });
window.addEventListener('pointerup', this.#initAudio, {
once: true,
});
window.addEventListener('keydown', this.#initAudio, { once: true });
this._settingsFormValues = { ...settingsStore.settings };
}

Expand Down Expand Up @@ -257,6 +269,8 @@ export class App extends LitElement {
pomodoroMinutes,
shortBreakMinutes,
longBreakMinutes,
audioSound,
audioVolume,
} = this._settingsFormValues;

return html`<div
Expand All @@ -268,7 +282,7 @@ export class App extends LitElement {
<h1>Settings</h1>
<div>
<form id="settingsForm" @submit=${this.#onSubmit}>
<h5>Preferences</h5>
<h2>Preferences</h2>

<div class="checkbox-group">
<label>
Expand Down Expand Up @@ -312,6 +326,7 @@ export class App extends LitElement {
</label>
</div>

<h3>Exercises</h3>
<div class="field-group">
<label>
Exercises Count:
Expand Down Expand Up @@ -347,7 +362,62 @@ export class App extends LitElement {
</label>
</div>

<h5>Set Times (In Minutes)</h5>
<div id="audioSection">
<h3>Audio</h3>
<div class="tooltip tooltip-right">
<button
?disabled=${audioVolume === AUDIO_VOLUME.MUTE}
@click=${this.#previewSound}
id="previewSound"
type="button"
>
</button>
<span class="tooltiptext">Preview sound</span>
</div>
</div>

<h4>Sound</h4>
<div class="field-group select-group">
<label>
<select
size=${AUDIO_SOUNDS.length}
name="audioSound"
@change=${this.#updateSettingsField}
.value=${audioSound}
>
${AUDIO_SOUNDS.map(
({ ID, NAME }) => html`
<option ?selected=${audioSound === ID} value=${ID}>
${toSentenceCase(NAME.replace(/-/g, ' '))}
</option>
`
)}
</select>
</label>
</div>

<h4>Volume</h4>
<div class="field-group select-group">
<label>
<select
size=${AUDIO_VOLUMES.length}
name="audioVolume"
@change=${this.#updateSettingsField}
.value=${audioVolume}
>
${AUDIO_VOLUMES.map(
(value) => html`
<option ?selected=${audioVolume === value} value=${value}>
${value === AUDIO_VOLUME.MUTE ? 'Mute' : `${value}%`}
</option>
`
)}
</select>
</label>
</div>

<h2>Set Times (In Minutes)</h2>
<div class="field-group">
<label>
Pomodoro:
Expand Down Expand Up @@ -396,16 +466,21 @@ export class App extends LitElement {
</div>`;
}

#previewSound() {
const { audioSound, audioVolume } = this._settingsFormValues;
webAudioApiService.playSound(audioSound, audioVolume);
}

/** @param {Event} event */
#onSubmit(event) {
event.preventDefault();

for (const value of Object.values(this._settingsFormValues)) {
for (const [key, value] of Object.entries(this._settingsFormValues)) {
if (!isNum(value) && !isBool(value)) {
alert(CLIENT_ERROR_MESSAGE.FORM.INVALID_INPUTS);
alert(CLIENT_ERROR_MESSAGE.INVALID_INPUTS);
return;
} else if (isNum(value) && Number(value) <= 0) {
alert(CLIENT_ERROR_MESSAGE.FORM.INVALID_POSITIVE_INTEGER);
} else if (key !== 'audioVolume' && isNum(value) && Number(value) <= 0) {
alert(CLIENT_ERROR_MESSAGE.INVALID_POSITIVE_INTEGER);
return;
}
}
Expand Down Expand Up @@ -443,9 +518,22 @@ export class App extends LitElement {
[name]: value,
};
}
} else if (target instanceof HTMLSelectElement) {
const { name, value } = target;

if (name === 'audioSound' || name === 'audioVolume') {
this._settingsFormValues = {
...this._settingsFormValues,
[name]: Number(value),
};
}
}
}

async #initAudio() {
await webAudioApiService.init();
}

#closeEnableNotificationsModal() {
this._enableNotificationsModalOpen = false;
}
Expand All @@ -467,6 +555,7 @@ export class App extends LitElement {
captionTextStyles,
modalStyles,
linkStyles,
tooltipStyles,
css`
:host {
display: flex;
Expand Down Expand Up @@ -526,7 +615,9 @@ export class App extends LitElement {
gap: 10px;
}

#settingsForm h5 {
#settingsForm h2,
#settingsForm h3,
#settingsForm h4 {
margin: 0;
padding: 0;
}
Expand All @@ -553,6 +644,40 @@ export class App extends LitElement {
#settingsForm .field-group input {
max-width: 200px;
}

#settingsForm .select-group label {
width: 100%;
}

#settingsForm .field-group select {
cursor: pointer;
width: 100%;
}

#audioSection {
display: flex;
gap: 0.5rem;
align-items: flex-end;
}

#previewSound {
border-radius: 50%;
border: 1px;
font-size: 0.7rem;
padding: 0.3rem 0.5rem;
}

#previewSound:hover {
opacity: 0.8;
}

#previewSound:disabled {
cursor: not-allowed !important;
}

.tooltip-right .tooltiptext {
transform: translateY(-30%);
}
`,
];
}
Expand Down
Binary file added src/assets/audio/alert-bells-echo.mp3
Binary file not shown.
Binary file added src/assets/audio/clear-announce-tones.mp3
Binary file not shown.
Binary file added src/assets/audio/home-standard-ding-dong.mp3
Binary file not shown.
Binary file added src/assets/audio/melodic-classic-door-bell.mp3
Binary file not shown.
Binary file added src/assets/audio/uplifting-flute-notification.mp3
Binary file not shown.
28 changes: 13 additions & 15 deletions src/index.d.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,20 @@

/** @typedef {"start" | "pause" | "reset"} PomodoroTimerAction */

/**
* @typedef {Record<PomodoroModeKind, number>} PomodoroMode
*/
/** @typedef {Record<PomodoroModeKind, number>} PomodoroMode */

/** @typedef {"chrome" | "chromium" | "edge" | "firefox" | "opera" | "safari" | "seamonkey" | "unknown"} UserAgent */

/** @typedef {"upperBody" | "lowerBody" | "core" | "cardio" | "mobility" | "balance" | "fullBody" | "staticStrength" | "yoga"} ExerciseCategory */

/** @typedef {[ExerciseCategory, readonly string[]]} ExerciseEntries */

/**
* @typedef {Record<string, string>} Exercise
* @property {string} category
* @property {string} name
*/

/**
* @typedef {object} Settings
* @property {boolean} showTimerInTitle
Expand All @@ -20,18 +28,8 @@
* @property {number} pomodoroMinutes
* @property {number} shortBreakMinutes
* @property {number} longBreakMinutes
*/

/** @typedef {"upperBody" | "lowerBody" | "core" | "cardio" | "mobility" | "balance" | "fullBody" | "staticStrength" | "yoga"} ExerciseCategory */

/**
* @typedef {[ExerciseCategory, readonly string[]]} ExerciseEntries
*/

/**
* @typedef {Record<string, string>} Exercise
* @property {string} category
* @property {string} name
* @property {number} audioSound
* @property {number} audioVolume
*/

export {};
22 changes: 17 additions & 5 deletions src/pages/home.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { LitElement, css, html, nothing } from 'lit';

import { notificationApiService } from '../services/notification-api.service.js';
import { webAudioApiService } from '../services/web-audio-api.service.js';
import { buttonStyles } from '../shared/styles/buttonStyles.js';
import { checkboxStyles } from '../shared/styles/checkboxStyles.js';
import ExercisesStore from '../stores/exercises.js';
import { DEFAULT_SETTINGS, settingsStore } from '../stores/settings.js';
import {
AUDIO_VOLUME,
CLIENT_ERROR_MESSAGE,
DEFAULT_POMODORO_TIMES,
POMODORO_MODE,
Expand Down Expand Up @@ -128,7 +130,7 @@ function getRandomMotivationalQuote() {
];
}

const POMODORO_MODES = Object.values(POMODORO_MODE);
const POMODORO_MODES = Object.freeze(Object.values(POMODORO_MODE));

export class HomePage extends LitElement {
static properties = {
Expand Down Expand Up @@ -316,7 +318,7 @@ export class HomePage extends LitElement {
this.#start();
this.#dismissExercises();
} else {
console.warn(CLIENT_ERROR_MESSAGE.UNKNOWN_POMODORO_MODE);
console.error(CLIENT_ERROR_MESSAGE.UNKNOWN_POMODORO_MODE);
}
}
}
Expand Down Expand Up @@ -347,7 +349,7 @@ export class HomePage extends LitElement {
this.#dismissExercises();
break;
default:
console.warn(CLIENT_ERROR_MESSAGE.UNKNOWN_TIMER_ACTION);
console.error(CLIENT_ERROR_MESSAGE.UNKNOWN_TIMER_ACTION);
}
}
}
Expand Down Expand Up @@ -443,10 +445,20 @@ export class HomePage extends LitElement {
};

#complete() {
const { enableNotifications, exercisesCount, showMotivationalQuote } =
this._settings;
const {
enableNotifications,
exercisesCount,
showMotivationalQuote,
audioSound,
audioVolume,
} = this._settings;
const isPomodoroModeSelected =
this._selectedPomodoroMode === POMODORO_MODE.POMODORO;
const isAudioVolumeMuteSelected = audioVolume === AUDIO_VOLUME.MUTE;

if (!isAudioVolumeMuteSelected) {
webAudioApiService.playSound(audioSound, audioVolume);
}

if (enableNotifications) {
notificationApiService.sendDesktopNotification(
Expand Down
Loading
Loading