Skip to content

Instantly share code, notes, and snippets.

@warseph
Last active February 2, 2021 17:39
Show Gist options
  • Select an option

  • Save warseph/69a3332d9268b9e98ad3bda5d8e72ed5 to your computer and use it in GitHub Desktop.

Select an option

Save warseph/69a3332d9268b9e98ad3bda5d8e72ed5 to your computer and use it in GitHub Desktop.
// ==UserScript==
// @name Countdown Timer
// @namespace http://warseph.com/
// @version 0.11
// @description Add a countdown timer to any page
// @author Ezequiel Rabinovich
// @match http*://*/*
// @grant GM_setValue
// @grant GM_getValue
// @updateURL https://gist.githubusercontent.com/warseph/69a3332d9268b9e98ad3bda5d8e72ed5/raw/countdown-timer.user.js
// @run-at context-menu
// ==/UserScript==
(function() {
'use strict';
class Emitter {
constructor() {
this.handlers = {};
}
on(event, handler) {
this.handlers[event] = (this.handlers[event]||[]).concat(handler);
return this;
}
emit(event,...data) {
(this.handlers[event] || []).forEach(handler => handler(...data));
}
}
class CountdownTimer extends Emitter {
constructor(alarm, handlers = () => {}) {
super();
this.time = undefined;
this.current = undefined;
this.interval = undefined;
this.alarm = new Audio(alarm);
this.alarm.loop = true;
this.state = new StateMachine('stopped', ['running'])
.add('running', ['paused', 'done', 'stopped'])
.add('paused', ['running', 'stopped'])
.add('done', ['stopped'])
;
this.startHandlers();
handlers(this.state);
}
startHandlers() {
this.state
.on('stopped', () => {
this.clearInterval();
this.alarm.pause();
this.resetTime();
})
.on('running', () => {
this.resume();
this.tick();
})
.on('paused', () => {
this.clearInterval();
})
.on('done', () => {
this.clearInterval();
this.alarm.play()
.catch(error => console.error(error));
})
;
}
setTime(time) {
this.state.transition('stopped');
this.current = this.time = time;
this.emit('new-time', time);
return this;
}
resetTime() {
this.current = this.time;
this.emit('tick', this.current);
}
tick() {
this.current = Math.max(0, this.current - 1000);
this.emit('tick', this.current);
if (this.current === 0) this.state.transition('done');
}
clearInterval() {
clearInterval(this.interval);
this.interval = undefined;
}
resume() {
this.interval = setInterval(() => this.tick(), 1000);
}
reset() {
this.state.transition('stopped');
}
start() {
this.state.transition('running');
}
pause() {
this.state.transition('paused');
}
mute() {
this.alarm.pause();
}
}
class StateMachine extends Emitter {
constructor(initial, transitions) {
super();
this.states = {
[initial]: transitions
};
this.current = initial;
this.handlers = {};
}
add(name, transitions) {
this.states[name] = transitions;
return this;
}
transition(name) {
if (!this.states[this.current].includes(name)) return false;
this.emit(name, this.current);
this.current = name;
return true;
}
start() {
this.emit(this.current, null);
}
}
const id = name => `tamper-monkey-userscript-timer-${name}`;
const elem = name => document.getElementById(id(name));
const defaultTime = 300000;
const sound = 'https://freesound.org/data/previews/233/233645_1752933-lq.mp3';
// Alarm Clock Digital by zanox licensed under CC BY 3.0
// (https://freesound.org/people/zanox/sounds/233645/)
const timer = new CountdownTimer(sound, state => state
.on('running', () => showControl('pause'))
.on('stopped', () => showControl('start'))
.on('stopped', () => stopShaking())
.on('paused', () => stopShaking())
.on('paused', () => showControl('start'))
.on('done', () => shake())
.on('done', () => showControl('mute'))
)
.on('tick', (timeValue) => { time(timeValue); })
.on('new-time', (time) => GM_setValue('time', time))
.setTime(GM_getValue('time', defaultTime))
;
const html = `
<style>
@keyframes tamper-monkey-userscript-timer-shake {
0% { -webkit-transform: translate(calc(-50% + 2px), 1px) rotate(0deg); }
10% { -webkit-transform: translate(calc(-50% + -1px), -2px) rotate(-1deg); }
20% { -webkit-transform: translate(calc(-50% + -3px), 0px) rotate(1deg); }
30% { -webkit-transform: translate(calc(-50% + 0px), 2px) rotate(0deg); }
40% { -webkit-transform: translate(calc(-50% + 1px), -1px) rotate(1deg); }
50% { -webkit-transform: translate(calc(-50% + -1px), 2px) rotate(-1deg); }
60% { -webkit-transform: translate(calc(-50% + -3px), 1px) rotate(0deg); }
70% { -webkit-transform: translate(calc(-50% + 2px), 1px) rotate(-1deg); }
80% { -webkit-transform: translate(calc(-50% + -1px), -1px) rotate(1deg); }
90% { -webkit-transform: translate(calc(-50% + 2px), 2px) rotate(0deg); }
100% { -webkit-transform: translate(calc(-50% + 1px), -2px) rotate(-1deg); }
}
.tamper-monkey-userscript-timer-shake {
animation-name: tamper-monkey-userscript-timer-shake;
animation-duration: 0.5s;
animation-iteration-count: infinite;
}
#tamper-monkey-userscript-timer-base {
border: 1px solid #999;
width: 245px;
height: 50px;
border-radius: 20px;
box-shadow: 0 0 15px 0 rgba(0,0,0,0.75);
background-color: rgba(255,255,255,0.8);
position: fixed;
bottom: 5px;
left: 50%;
transform: translate(-50%,0);
z-index: 9999;
font: 400 18px Arial;
}
#tamper-monkey-userscript-timer-time {
background: none;
margin: 15px 10px;
padding: initial;
width: 115px;
border: none;
text-align: left;
font: 400 18px Arial;
color: #333;
display: inline-block;
transform: initial;
align-items: initial;
box-sizing: initial;
word-spacing: initial;
text-indent: initial;
text-transform: initial;
letter-spacing: initial;
text-rendering: initial;
text-shadow: initial;
appearance: initial;
writing-mode: initial;
transition: initial;
outline: initial;
border-radius: initial;
box-shadow: initial;
}
#tamper-monkey-userscript-timer-base button {
border: none;
background: none;
font: 400 18px Arial;
margin: 0 4px;
padding: 0;
transform: initial;
align-items: initial;
box-sizing: initial;
word-spacing: initial;
text-indent: initial;
text-transform: initial;
display: initial;
text-align: initial;
color: initial;
letter-spacing: initial;
text-rendering: initial;
text-shadow: initial;
appearance: initial;
writing-mode: initial;
transition: initial;
outline: initial;
border-radius: initial;
box-shadow: initial;
}
#tamper-monkey-userscript-timer-base #tamper-monkey-userscript-timer-pause {
display: none;
}
#tamper-monkey-userscript-timer-base #tamper-monkey-userscript-timer-mute {
display: none;
}
</style>
<div id="tamper-monkey-userscript-timer-base">
<input id="tamper-monkey-userscript-timer-time" type="time" step="1" value="00:02:00">
<button id="tamper-monkey-userscript-timer-start">▶️</button>
<button id="tamper-monkey-userscript-timer-pause">⏸</button>
<button id="tamper-monkey-userscript-timer-mute">🔕</button>
<button id="tamper-monkey-userscript-timer-reset">⏮</button>
<button id="tamper-monkey-userscript-timer-close">❎</button>
</div>`;
const controls = ['start', 'pause', 'mute'];
function instrument() {
if (elem('base')) return;
document.body.insertAdjacentHTML('beforeend', html);
time(timer.time);
elem('close').addEventListener('click', () => timer.reset());
elem('reset').addEventListener('click', () => timer.reset());
elem('start').addEventListener('click', () => timer.start());
elem('pause').addEventListener('click', () => timer.pause());
elem('mute').addEventListener('click', () => timer.mute());
elem('time').addEventListener('change', () => timer.setTime(time()));
elem('close').addEventListener('click', () => elem('base').remove());
}
function showControl(name) {
controls.forEach(control =>
elem(control).style.display = control === name ? 'initial' : 'none'
);
}
function shake() {
elem('base').className = 'tamper-monkey-userscript-timer-shake';
}
function stopShaking() {
elem('base').className = '';
}
function time(newValue = undefined) {
if (newValue !== undefined) {
elem('time').valueAsNumber = newValue;
} else {
return elem('time').valueAsNumber;
}
}
instrument();
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment