diff --git a/src/resources/beeping/beep.ogg b/src/resources/beeping/beep.ogg
new file mode 100644
index 0000000..7ca36de
Binary files /dev/null and b/src/resources/beeping/beep.ogg differ
diff --git a/src/resources/beeping/index.html b/src/resources/beeping/index.html
new file mode 100644
index 0000000..c5bc016
--- /dev/null
+++ b/src/resources/beeping/index.html
@@ -0,0 +1,47 @@
+
+
+
+
+
+ Beeper!
+
+
+
+
+
+
+
+
+
+
0.00
+
+
Do a beep at
+
+
second intervals
+
+
+
+
+
+
+
+
Custom audio file:
+
+
default audio
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/resources/beeping/main.js b/src/resources/beeping/main.js
new file mode 100644
index 0000000..65dc275
--- /dev/null
+++ b/src/resources/beeping/main.js
@@ -0,0 +1,160 @@
+var beeperBegun = false;
+var beepInterval = 0;
+var beepTimeCounter = 0;
+var intervalId = 0;
+var timeChangedWhileBegun = false;
+var lastSec = -1;
+
+var showBeep = 0;
+
+var audioCtx = new (window.AudioContext || window.webkitAudioContext)();
+
+function main() {
+ timeChanged();
+ updateInvalidness();
+
+ document.getElementById("plus").onclick = () => changeSeconds(1);
+ document.getElementById("minus").onclick = () => changeSeconds(-1);
+ document.getElementById("time-input").onchange = () => {
+ timeChanged();
+ updateInvalidness();
+ };
+ document.getElementById("begin").onclick = toggle;
+ var audio = document.getElementById("audio");
+ let slider = document.getElementById("volume-slider");
+ document.getElementById("volume-slider").onchange = () => {
+ audio.volume = (slider.value / 100);
+ document.getElementById("volume").innerText = slider.value;
+ };
+
+ document.getElementById("file").addEventListener('change', () => {
+ var file = document.getElementById("file").files[0];
+ document.getElementById("audiosource").setAttribute("src", URL.createObjectURL(file));
+ document.getElementById("audio").load();
+ });
+ document.getElementById("audio").addEventListener("loadeddata", (event) => {
+ document.getElementById("audiotext").innerText = "Loaded audio";
+ });
+ document.getElementById("audio").addEventListener("loadstart", (event) => {
+ document.getElementById("audiotext").innerText = "Loading/Error";
+ });
+
+ console.log("Beeper initialized!");
+}
+
+function isTimeValid() {
+ value = document.getElementById("time-input").value;
+ if (value != "" && (isNaN(value) || value < 1)) {
+ return false;
+ }
+ return true;
+}
+
+function getTime() {
+ if (!isTimeValid()) {
+ return 1;
+ }
+ value = document.getElementById("time-input").value;
+ if (value == "") {
+ return 5;
+ }
+ return parseFloat(value);
+}
+
+
+function updateInvalidness() {
+ input = document.getElementById("time-input");
+ if (!isTimeValid()) {
+ input.classList = "invalid";
+ } else {
+ input.classList = "";
+ }
+}
+
+function changeSeconds(diff) {
+ value = document.getElementById("time-input").value;
+ if (!isNaN(value)) {
+ if (value == "") {
+ value = 5;
+ }
+ value = parseFloat(value);
+ if ((value + diff) >= 1 || diff > 0) {
+ input.value = value + diff;
+ updateInvalidness();
+ timeChanged();
+ }
+ }
+}
+
+function timeChanged() {
+ if (isTimeValid()) {
+ if (beeperBegun) {
+ timeChangedWhileBegun = true;
+ return;
+ }
+ time = getTime();
+ sec = Math.floor(time);
+ millis = Math.floor((time - sec) * 100) / 100;
+ millis = Intl.NumberFormat("en-US", { minimumFractionDigits: 2 }).format(millis);
+ millis = ("" + millis).slice(2);
+
+ seconds = document.getElementById("seconds").innerText = sec;
+ milliseconds = document.getElementById("milliseconds").innerText = millis;
+
+ beepInterval = getTime();
+ beepTimeCounter = beepInterval;
+ }
+}
+
+function toggle() {
+ var button = document.getElementById("begin");
+ if (!beeperBegun && !isTimeValid()) {
+ return;
+ }
+ beeperBegun = !beeperBegun;
+ if (beeperBegun) {
+ button.innerText = "Stop!";
+ intervalId = setInterval(update, 10);
+ } else {
+ button.innerText = "Begin!";
+ clearInterval(intervalId);
+ if (timeChangedWhileBegun) {
+ timeChangedWhileBegun = false;
+ timeChanged();
+ }
+ lastSec = -1;
+ showBeep = 0;
+ document.title = "Beeper!";
+
+ document.getElementById("audio").pause()
+ }
+}
+
+function update() {
+ var seconds = document.getElementById("seconds");
+ var milliseconds = document.getElementById("milliseconds");
+
+ beepTimeCounter -= 0.01;
+ showBeep -= 0.1;
+ if (beepTimeCounter <= 0) {
+ beepTimeCounter = beepInterval;
+ showBeep = 0.5;
+ document.getElementById("audio").fastSeek(0)
+ document.getElementById("audio").play()
+ }
+ var sec = Math.floor(beepTimeCounter);
+ seconds.innerText = sec;
+ var millis = Math.floor((beepTimeCounter - sec) * 100) / 100;
+ millis = Intl.NumberFormat("en-US", { minimumFractionDigits: 2 }).format(millis);
+ millis = ("" + millis).slice(2);
+ milliseconds.innerText = millis;
+
+ if (lastSec != sec) {
+ lastSec = sec;
+ if (showBeep > 0) {
+ document.title = "Beeeep!";
+ } else {
+ document.title = "Beeper - " + sec;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/resources/beeping/style.css b/src/resources/beeping/style.css
new file mode 100644
index 0000000..891646c
--- /dev/null
+++ b/src/resources/beeping/style.css
@@ -0,0 +1,154 @@
+:root {
+ font-family: Helvetica, sans-serif;
+}
+
+* {
+ margin: 0;
+ padding: 0;
+}
+
+body {
+ width: 100%;
+ height: 100%;
+ background-color: #44444a;
+ font-size: 1.1em;
+}
+
+.padding {
+ margin-bottom: 2em;
+}
+
+.content {
+ padding-top: 10vh;
+ text-align: center;
+ color: #eee;
+}
+
+.timer {
+ font-size: 15vw;
+ margin: 0;
+}
+
+.timer-milliseconds {
+ font-size: 25%;
+ font-family: Iosevka, monospace;
+}
+
+.inline {
+ display: inline-block;
+}
+
+.input-container {
+ display: inline-block;
+ height: 1.9em;
+}
+
+button {
+ display: inline-block;
+ border: none;
+ margin: 0;
+ padding: 0.3em;
+ padding-left: 0.6em;
+ padding-right: 0.6em;
+ font-size: 1em;
+ transition: 0.2s;
+ font-family: helvetica, sans-serif;
+ background-color: #ddd;
+ font-size: 1em;
+}
+
+audio {
+ display: none;
+}
+
+.scroller {
+ height: 100%;
+}
+
+.scroller:first-child {
+ border-top-left-radius: 0.3em;
+ border-bottom-left-radius: 0.3em;
+ background-color: #eb6868;
+}
+
+.scroller:last-child {
+ border-top-right-radius: 0.3em;
+ border-bottom-right-radius: 0.3em;
+ background-color: #77e777;
+}
+
+.scroller:first-child:hover {
+ background-color: #dd5151;
+}
+
+.scroller:last-child:hover {
+ background-color: #53db53;
+}
+
+button:hover {
+ background-color: #c0c0c0;
+}
+
+#time-input {
+ border: none;
+ background: none;
+ position: relative;
+ background-color: #eee;
+ max-width: 5ch;
+ height: 100%;
+ text-align: center;
+ transition: 0.2s;
+ font-size: 1em;
+ font-family: helvetica, sans-serif;
+}
+
+#volume-slider {
+ display: inline-block;
+}
+
+#time-input:hover {
+ background-color: #cbcbcb;
+}
+
+#audiocontrols {
+ display: inline-flex;
+ width: 50vw;
+}
+
+#audiocontrols > * {
+ width: 50%;
+}
+
+#file {
+ width: 10em;
+}
+
+.invalid {
+ background-color: #dd7979
+}
+
+.invalid:hover {
+ background-color: #c56161
+}
+
+.footnote {
+ position: absolute;
+ width: 100%;
+ text-align: center;
+ bottom: 0;
+ padding-bottom: 2em;
+}
+
+.footnote a {
+ color: #c7c5ee;
+}
+
+@media (max-width: 900px) {
+ .content {
+ padding: 0;
+ }
+
+ .timer {
+ font-size: 40vw;
+ }
+}
\ No newline at end of file