Last active
February 2, 2021 17:39
-
-
Save warseph/69a3332d9268b9e98ad3bda5d8e72ed5 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // ==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