Adds a draggable timer/stopwatch widget to every website that saves minimized state, position, mode, and time.
// ==UserScript==
// @name Universal Floating Timer
// @namespace Violentmonkey Scripts
// @version 1.2.0
// @description Adds a draggable timer/stopwatch widget to every website that saves minimized state, position, mode, and time.
// @author Mateo Benavides-Kastner
// @license MIT
// @match *://*/*
// @grant none
// ==/UserScript==
(function () {
"use strict";
if (window.top !== window.self) return;
if (document.getElementById("mateo-universal-timer")) return;
const STORAGE_KEY = "mateo-universal-floating-timer-state-v2";
function loadState() {
try {
return JSON.parse(localStorage.getItem(STORAGE_KEY)) || {};
} catch {
return {};
}
}
const saved = loadState();
const isMinimized = saved.minimized !== undefined ? saved.minimized : true;
const timer = document.createElement("div");
timer.id = "mateo-universal-timer";
if (isMinimized) {
timer.classList.add("mut-minimized");
}
timer.innerHTML = `
<div id="mut-header">
<span>⏱ Timer</span>
<button id="mut-toggle" type="button">${isMinimized ? "+" : "−"}</button>
</div>
<div id="mut-body">
<div id="mut-mode-row">
<span>Stopwatch</span>
<label id="mut-switch">
<input id="mut-mode-switch" type="checkbox">
<span id="mut-slider"></span>
</label>
<span>Timer</span>
</div>
<div id="mut-display">00:00:00</div>
<div id="mut-timer-inputs">
<input id="mut-hours" type="number" min="0" max="99" value="0" title="Hours">
<span>:</span>
<input id="mut-minutes" type="number" min="0" max="59" value="5" title="Minutes">
<span>:</span>
<input id="mut-seconds" type="number" min="0" max="59" value="0" title="Seconds">
</div>
<div id="mut-buttons">
<button id="mut-start" type="button">Start</button>
<button id="mut-stop" type="button">Stop</button>
<button id="mut-reset" type="button">Reset</button>
</div>
</div>
`;
document.body.appendChild(timer);
const style = document.createElement("style");
style.textContent = `
#mateo-universal-timer {
position: fixed;
top: ${typeof saved.top === "number" ? saved.top + "px" : "20px"};
left: ${typeof saved.left === "number" ? saved.left + "px" : "50%"};
transform: ${typeof saved.left === "number" ? "none" : "translateX(-50%)"};
width: 280px;
background: #1f1f1f;
color: white;
border-radius: 14px;
box-shadow: 0 8px 30px rgba(0,0,0,0.4);
z-index: 2147483647;
font-family: Arial, sans-serif;
overflow: hidden;
user-select: none;
}
#mut-header {
background: #111;
padding: 10px 12px;
cursor: move;
display: flex;
justify-content: space-between;
align-items: center;
font-weight: bold;
font-size: 14px;
}
#mut-toggle {
background: #333;
color: white;
border: none;
border-radius: 6px;
width: 28px;
height: 24px;
cursor: pointer;
font-size: 18px;
line-height: 18px;
}
#mut-body {
padding: 12px;
}
#mut-mode-row {
display: flex;
justify-content: center;
align-items: center;
gap: 8px;
font-size: 12px;
margin-bottom: 10px;
color: #ccc;
}
#mut-switch {
position: relative;
display: inline-block;
width: 46px;
height: 24px;
}
#mut-switch input {
opacity: 0;
width: 0;
height: 0;
}
#mut-slider {
position: absolute;
cursor: pointer;
inset: 0;
background: #444;
border-radius: 999px;
transition: 0.2s;
}
#mut-slider::before {
content: "";
position: absolute;
height: 18px;
width: 18px;
left: 3px;
bottom: 3px;
background: white;
border-radius: 50%;
transition: 0.2s;
}
#mut-mode-switch:checked + #mut-slider {
background: #0078ff;
}
#mut-mode-switch:checked + #mut-slider::before {
transform: translateX(22px);
}
#mut-display {
background: #000;
color: #00ff88;
border-radius: 8px;
padding: 12px;
text-align: center;
font-size: 30px;
font-weight: bold;
letter-spacing: 1px;
margin-bottom: 10px;
}
#mut-timer-inputs {
display: none;
justify-content: center;
align-items: center;
gap: 5px;
margin-bottom: 10px;
}
#mut-timer-inputs input {
width: 52px;
height: 32px;
border: none;
border-radius: 7px;
background: #000;
color: #00ff88;
text-align: center;
font-size: 15px;
outline: none;
}
#mut-buttons {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 7px;
}
#mut-buttons button {
height: 34px;
border: none;
border-radius: 8px;
background: #333;
color: white;
cursor: pointer;
font-size: 14px;
}
#mut-buttons button:hover,
#mut-toggle:hover {
background: #555;
}
#mut-start {
background: #137333 !important;
}
#mut-stop {
background: #9a3412 !important;
}
#mut-reset {
background: #c0392b !important;
}
.mut-timer-mode #mut-timer-inputs {
display: flex;
}
.mut-minimized {
width: 130px !important;
}
.mut-minimized #mut-body {
display: none;
}
`;
document.head.appendChild(style);
const header = document.getElementById("mut-header");
const toggle = document.getElementById("mut-toggle");
const display = document.getElementById("mut-display");
const modeSwitch = document.getElementById("mut-mode-switch");
const hoursInput = document.getElementById("mut-hours");
const minutesInput = document.getElementById("mut-minutes");
const secondsInput = document.getElementById("mut-seconds");
const startBtn = document.getElementById("mut-start");
const stopBtn = document.getElementById("mut-stop");
const resetBtn = document.getElementById("mut-reset");
let mode = saved.mode === "timer" ? "timer" : "stopwatch";
let running = saved.running === true;
let intervalId = null;
let stopwatchSeconds = Number(saved.stopwatchSeconds) || 0;
let timerSeconds = Number(saved.timerSeconds) || 5 * 60;
let timerOriginalSeconds = Number(saved.timerOriginalSeconds) || 5 * 60;
hoursInput.value = Number(saved.hours ?? 0);
minutesInput.value = Number(saved.minutes ?? 5);
secondsInput.value = Number(saved.seconds ?? 0);
if (mode === "timer") {
timer.classList.add("mut-timer-mode");
modeSwitch.checked = true;
}
function pad(number) {
return String(number).padStart(2, "0");
}
function formatTime(totalSeconds) {
totalSeconds = Math.max(0, Math.floor(totalSeconds));
const hours = Math.floor(totalSeconds / 3600);
const minutes = Math.floor((totalSeconds % 3600) / 60);
const seconds = totalSeconds % 60;
return `${pad(hours)}:${pad(minutes)}:${pad(seconds)}`;
}
function getInputSeconds() {
const hours = Math.max(0, Number(hoursInput.value) || 0);
const minutes = Math.min(59, Math.max(0, Number(minutesInput.value) || 0));
const seconds = Math.min(59, Math.max(0, Number(secondsInput.value) || 0));
hoursInput.value = hours;
minutesInput.value = minutes;
secondsInput.value = seconds;
return hours * 3600 + minutes * 60 + seconds;
}
function saveState() {
const rect = timer.getBoundingClientRect();
localStorage.setItem(STORAGE_KEY, JSON.stringify({
minimized: timer.classList.contains("mut-minimized"),
mode,
running,
stopwatchSeconds,
timerSeconds,
timerOriginalSeconds,
hours: Number(hoursInput.value) || 0,
minutes: Number(minutesInput.value) || 0,
seconds: Number(secondsInput.value) || 0,
left: rect.left,
top: rect.top
}));
}
function updateDisplay() {
display.textContent = mode === "stopwatch"
? formatTime(stopwatchSeconds)
: formatTime(timerSeconds);
saveState();
}
function stop() {
running = false;
if (intervalId) {
clearInterval(intervalId);
intervalId = null;
}
saveState();
}
function start() {
if (running) return;
if (mode === "timer" && timerSeconds <= 0) {
timerSeconds = getInputSeconds();
timerOriginalSeconds = timerSeconds;
}
if (mode === "timer" && timerSeconds <= 0) return;
running = true;
saveState();
intervalId = setInterval(() => {
if (mode === "stopwatch") {
stopwatchSeconds++;
} else {
timerSeconds--;
if (timerSeconds <= 0) {
timerSeconds = 0;
updateDisplay();
stop();
alert("Timer done!");
return;
}
}
updateDisplay();
}, 1000);
}
function reset() {
stop();
if (mode === "stopwatch") {
stopwatchSeconds = 0;
} else {
timerSeconds = getInputSeconds();
timerOriginalSeconds = timerSeconds;
}
updateDisplay();
}
function setMode(newMode) {
stop();
mode = newMode;
if (mode === "timer") {
timer.classList.add("mut-timer-mode");
timerSeconds = getInputSeconds();
timerOriginalSeconds = timerSeconds;
} else {
timer.classList.remove("mut-timer-mode");
}
updateDisplay();
}
function resumeIfNeeded() {
if (!running) return;
running = false;
start();
}
toggle.addEventListener("click", () => {
timer.classList.toggle("mut-minimized");
toggle.textContent = timer.classList.contains("mut-minimized") ? "+" : "−";
saveState();
});
modeSwitch.addEventListener("change", () => {
setMode(modeSwitch.checked ? "timer" : "stopwatch");
});
startBtn.addEventListener("click", start);
stopBtn.addEventListener("click", stop);
resetBtn.addEventListener("click", reset);
[hoursInput, minutesInput, secondsInput].forEach((input) => {
input.addEventListener("input", () => {
if (!running && mode === "timer") {
timerSeconds = getInputSeconds();
timerOriginalSeconds = timerSeconds;
updateDisplay();
}
});
});
let dragging = false;
let offsetX = 0;
let offsetY = 0;
header.addEventListener("mousedown", (event) => {
if (event.target === toggle) return;
dragging = true;
const rect = timer.getBoundingClientRect();
timer.style.left = `${rect.left}px`;
timer.style.top = `${rect.top}px`;
timer.style.right = "auto";
timer.style.transform = "none";
offsetX = event.clientX - rect.left;
offsetY = event.clientY - rect.top;
});
document.addEventListener("mousemove", (event) => {
if (!dragging) return;
timer.style.left = `${event.clientX - offsetX}px`;
timer.style.top = `${event.clientY - offsetY}px`;
});
document.addEventListener("mouseup", () => {
if (dragging) {
dragging = false;
saveState();
}
});
window.addEventListener("beforeunload", saveState);
updateDisplay();
resumeIfNeeded();
})();