Adds floating draggable website tools: notes, calculator, and timer/stopwatch. Saves notes, minimized states, positions, timer data, and calculator display.
// ==UserScript==
// @name Floating Website Tools
// @namespace Violentmonkey Scripts
// @version 1.0.2
// @description Adds floating draggable website tools: notes, calculator, and timer/stopwatch. Saves notes, minimized states, positions, timer data, and calculator display.
// @author Mateo Benavides-Kastner
// @license MIT
// @match *://*/*
// @grant none
// ==/UserScript==
(function () {
"use strict";
if (window.top !== window.self) return;
if (document.getElementById("mateo-floating-tools-style")) return;
const TOOL_Z = 2147483647;
function loadJSON(key) {
try {
return JSON.parse(localStorage.getItem(key)) || {};
} catch {
return {};
}
}
function saveJSON(key, value) {
localStorage.setItem(key, JSON.stringify(value));
}
const style = document.createElement("style");
style.id = "mateo-floating-tools-style";
style.textContent = `
.mft-widget {
position: fixed;
background: #1f1f1f;
color: white;
border-radius: 14px;
box-shadow: 0 8px 30px rgba(0,0,0,0.4);
z-index: ${TOOL_Z};
font-family: Arial, sans-serif;
overflow: hidden;
user-select: none;
}
.mft-header {
background: #111;
padding: 10px 12px;
cursor: move;
display: flex;
justify-content: space-between;
align-items: center;
font-weight: bold;
font-size: 14px;
}
.mft-header button,
.mft-body button {
background: #333;
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
}
.mft-header button:hover,
.mft-body button:hover {
background: #555;
}
.mft-toggle {
width: 28px;
height: 24px;
font-size: 18px;
line-height: 18px;
}
.mft-body {
padding: 12px;
}
#mateo-floating-notes.mft-minimized,
#mateo-floating-calculator.mft-minimized,
#mateo-universal-timer.mft-minimized {
width: 130px !important;
}
.mft-minimized .mft-body {
display: none;
}
#mateo-floating-notes {
width: 280px;
}
#mfn-textarea {
width: 100%;
height: 160px;
box-sizing: border-box;
resize: vertical;
border: none;
border-radius: 8px;
background: #000;
color: #00ff88;
font-size: 14px;
padding: 10px;
outline: none;
font-family: Arial, sans-serif;
user-select: text;
}
#mfn-footer {
margin-top: 8px;
display: flex;
justify-content: space-between;
align-items: center;
font-size: 12px;
color: #aaa;
}
#mfn-clear {
padding: 5px 10px;
font-size: 12px;
}
#mateo-floating-calculator {
width: 260px;
min-width: 260px;
}
#mateo-floating-calculator.mft-minimized {
min-width: 130px;
}
#mfc-display {
width: 100%;
box-sizing: border-box;
height: 42px;
margin-bottom: 10px;
border: none;
border-radius: 8px;
background: #000;
color: #00ff88;
font-size: 22px;
text-align: right;
padding: 8px;
outline: none;
user-select: none;
cursor: default;
}
#mfc-buttons {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 7px;
}
#mfc-buttons button {
height: 38px;
border-radius: 8px;
font-size: 16px;
}
#mfc-buttons button[data-action="equals"] {
background: #0078ff;
}
#mfc-buttons button[data-action="clear"] {
background: #c0392b;
}
#mateo-universal-timer {
width: 280px;
}
#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-radius: 8px;
font-size: 14px;
}
#mut-start {
background: #137333 !important;
}
#mut-stop {
background: #9a3412 !important;
}
#mut-reset {
background: #c0392b !important;
}
.mut-timer-mode #mut-timer-inputs {
display: flex;
}
`;
document.head.appendChild(style);
function makeDraggable(widget, header, toggleButton, saveState) {
let dragging = false;
let offsetX = 0;
let offsetY = 0;
header.addEventListener("mousedown", (event) => {
if (event.target === toggleButton) return;
dragging = true;
const rect = widget.getBoundingClientRect();
widget.style.left = `${rect.left}px`;
widget.style.top = `${rect.top}px`;
widget.style.right = "auto";
widget.style.transform = "none";
offsetX = event.clientX - rect.left;
offsetY = event.clientY - rect.top;
});
document.addEventListener("mousemove", (event) => {
if (!dragging) return;
widget.style.left = `${event.clientX - offsetX}px`;
widget.style.top = `${event.clientY - offsetY}px`;
});
document.addEventListener("mouseup", () => {
if (dragging) {
dragging = false;
saveState();
}
});
}
function setupNotes() {
if (document.getElementById("mateo-floating-notes")) return;
const notesStorageKey = "mateo-floating-notes-" + location.hostname;
const stateStorageKey = "mateo-floating-notes-state-v1";
const saved = loadJSON(stateStorageKey);
const minimized = saved.minimized !== undefined ? saved.minimized : true;
const notes = document.createElement("div");
notes.id = "mateo-floating-notes";
notes.className = "mft-widget";
if (minimized) notes.classList.add("mft-minimized");
notes.style.top = typeof saved.top === "number" ? `${saved.top}px` : "120px";
notes.style.left = typeof saved.left === "number" ? `${saved.left}px` : "30px";
notes.innerHTML = `
<div id="mfn-header" class="mft-header">
<span>📝 Notes</span>
<button id="mfn-toggle" class="mft-toggle" type="button">${minimized ? "+" : "−"}</button>
</div>
<div id="mfn-body" class="mft-body">
<textarea id="mfn-textarea" placeholder="Type notes for this website..."></textarea>
<div id="mfn-footer">
<span id="mfn-status">Saved</span>
<button id="mfn-clear" type="button">Clear</button>
</div>
</div>
`;
document.body.appendChild(notes);
const header = document.getElementById("mfn-header");
const toggle = document.getElementById("mfn-toggle");
const textarea = document.getElementById("mfn-textarea");
const status = document.getElementById("mfn-status");
const clear = document.getElementById("mfn-clear");
textarea.value = localStorage.getItem(notesStorageKey) || "";
let lastSavedValue = textarea.value;
function saveState() {
const rect = notes.getBoundingClientRect();
saveJSON(stateStorageKey, {
minimized: notes.classList.contains("mft-minimized"),
left: rect.left,
top: rect.top
});
}
function saveNotes() {
if (textarea.value !== lastSavedValue) {
localStorage.setItem(notesStorageKey, textarea.value);
lastSavedValue = textarea.value;
status.textContent = "Saved";
}
saveState();
}
textarea.addEventListener("input", () => {
status.textContent = "Typing...";
});
clear.addEventListener("click", () => {
textarea.value = "";
localStorage.setItem(notesStorageKey, "");
lastSavedValue = "";
status.textContent = "Cleared";
});
toggle.addEventListener("click", () => {
notes.classList.toggle("mft-minimized");
toggle.textContent = notes.classList.contains("mft-minimized") ? "+" : "−";
saveState();
});
setInterval(saveNotes, 1000);
window.addEventListener("beforeunload", saveNotes);
makeDraggable(notes, header, toggle, saveState);
}
function setupCalculator() {
if (document.getElementById("mateo-floating-calculator")) return;
const stateKey = "mateo-floating-calculator-state-v1";
const saved = loadJSON(stateKey);
const minimized = saved.minimized !== undefined ? saved.minimized : true;
const calc = document.createElement("div");
calc.id = "mateo-floating-calculator";
calc.className = "mft-widget";
if (minimized) calc.classList.add("mft-minimized");
calc.style.width = minimized ? "130px" : "260px";
calc.style.top = typeof saved.top === "number" ? `${saved.top}px` : "100px";
if (typeof saved.left === "number") {
const safeLeft = Math.max(0, Math.min(saved.left, window.innerWidth - 260));
calc.style.left = `${safeLeft}px`;
} else {
calc.style.right = "30px";
}
calc.innerHTML = `
<div id="mfc-header" class="mft-header">
<span>Calculator</span>
<button id="mfc-minimize" class="mft-toggle" type="button">${minimized ? "+" : "−"}</button>
</div>
<div id="mfc-body" class="mft-body">
<input id="mfc-display" type="text" value="" placeholder="0" readonly>
<div id="mfc-buttons">
<button type="button" data-action="clear">C</button>
<button type="button" data-action="back">⌫</button>
<button type="button" data-insert="(">(</button>
<button type="button" data-insert=")">)</button>
<button type="button" data-insert="√(">√</button>
<button type="button" data-insert="^">xʸ</button>
<button type="button" data-insert="e">e</button>
<button type="button" data-insert="/">÷</button>
<button type="button" data-insert="7">7</button>
<button type="button" data-insert="8">8</button>
<button type="button" data-insert="9">9</button>
<button type="button" data-insert="*">×</button>
<button type="button" data-insert="4">4</button>
<button type="button" data-insert="5">5</button>
<button type="button" data-insert="6">6</button>
<button type="button" data-insert="-">−</button>
<button type="button" data-insert="1">1</button>
<button type="button" data-insert="2">2</button>
<button type="button" data-insert="3">3</button>
<button type="button" data-insert="+">+</button>
<button type="button" data-insert="0">0</button>
<button type="button" data-insert=".">.</button>
<button type="button" data-action="negate">±</button>
<button type="button" data-action="equals">=</button>
</div>
</div>
`;
document.body.appendChild(calc);
const display = document.getElementById("mfc-display");
const header = document.getElementById("mfc-header");
const minimize = document.getElementById("mfc-minimize");
const buttons = document.querySelectorAll("#mfc-buttons button");
display.value = saved.display || "";
function saveState() {
const rect = calc.getBoundingClientRect();
saveJSON(stateKey, {
minimized: calc.classList.contains("mft-minimized"),
display: display.value,
left: rect.left,
top: rect.top
});
}
function insertText(text) {
if (["Error", "NaN", "∞", "-∞"].includes(display.value)) {
display.value = "";
}
display.value += text;
saveState();
}
function formatResult(value) {
if (value === Infinity) return "∞";
if (value === -Infinity) return "-∞";
if (Number.isNaN(value)) return "NaN";
return String(value);
}
function tokenize(expression) {
const tokens = [];
let i = 0;
while (i < expression.length) {
const char = expression[i];
if (char === " ") {
i++;
continue;
}
if (/[0-9.]/.test(char)) {
let number = char;
i++;
while (i < expression.length && /[0-9.]/.test(expression[i])) {
number += expression[i];
i++;
}
tokens.push({ type: "number", value: parseFloat(number) });
continue;
}
if (char === "e") {
tokens.push({ type: "number", value: Math.E });
i++;
continue;
}
if (char === "√") {
tokens.push({ type: "function", value: "sqrt" });
i++;
continue;
}
if ("+-*/^()".includes(char)) {
tokens.push({ type: "operator", value: char });
i++;
continue;
}
throw new Error("Bad character");
}
return tokens;
}
function toRPN(tokens) {
const output = [];
const operators = [];
const precedence = {
"u-": 5,
"sqrt": 5,
"^": 4,
"*": 3,
"/": 3,
"+": 2,
"-": 2
};
const rightAssociative = {
"^": true,
"u-": true,
"sqrt": true
};
let previous = null;
for (const token of tokens) {
if (token.type === "number") {
output.push(token);
previous = token;
continue;
}
if (token.type === "function") {
operators.push(token);
previous = token;
continue;
}
if (token.value === "(") {
operators.push(token);
previous = token;
continue;
}
if (token.value === ")") {
while (operators.length && operators[operators.length - 1].value !== "(") {
output.push(operators.pop());
}
if (!operators.length) throw new Error("Mismatched parentheses");
operators.pop();
if (operators.length && operators[operators.length - 1].type === "function") {
output.push(operators.pop());
}
previous = token;
continue;
}
let op = token.value;
if (
op === "-" &&
(!previous ||
previous.value === "(" ||
["+", "-", "*", "/", "^"].includes(previous.value))
) {
op = "u-";
}
const current = { type: "operator", value: op };
while (operators.length) {
const top = operators[operators.length - 1];
if (top.value === "(") break;
const topPrec = precedence[top.value];
const currentPrec = precedence[current.value];
if (
topPrec > currentPrec ||
(topPrec === currentPrec && !rightAssociative[current.value])
) {
output.push(operators.pop());
} else {
break;
}
}
operators.push(current);
previous = current;
}
while (operators.length) {
const op = operators.pop();
if (op.value === "(" || op.value === ")") throw new Error("Mismatched parentheses");
output.push(op);
}
return output;
}
function evalRPN(rpn) {
const stack = [];
for (const token of rpn) {
if (token.type === "number") {
stack.push(token.value);
continue;
}
if (token.value === "u-") {
if (stack.length < 1) throw new Error("Bad unary");
stack.push(-stack.pop());
continue;
}
if (token.value === "sqrt") {
if (stack.length < 1) throw new Error("Bad sqrt");
stack.push(Math.sqrt(stack.pop()));
continue;
}
if (stack.length < 2) throw new Error("Bad expression");
const b = stack.pop();
const a = stack.pop();
if (token.value === "+") stack.push(a + b);
else if (token.value === "-") stack.push(a - b);
else if (token.value === "*") stack.push(a * b);
else if (token.value === "/") stack.push(a / b);
else if (token.value === "^") stack.push(Math.pow(a, b));
else throw new Error("Unknown operator");
}
if (stack.length !== 1) throw new Error("Bad result");
return stack[0];
}
function calculate() {
let expression = display.value.trim();
if (!expression) return;
expression = expression.replace(/÷/g, "/").replace(/×/g, "*");
const tokens = tokenize(expression);
const rpn = toRPN(tokens);
const result = evalRPN(rpn);
display.value = formatResult(result);
saveState();
}
buttons.forEach((button) => {
button.addEventListener("mousedown", (event) => {
event.preventDefault();
});
button.addEventListener("click", () => {
const insert = button.dataset.insert;
const action = button.dataset.action;
try {
if (insert) {
insertText(insert);
return;
}
if (action === "clear") {
display.value = "";
saveState();
return;
}
if (action === "back") {
display.value = display.value.slice(0, -1);
saveState();
return;
}
if (action === "negate") {
display.value = display.value ? `-(${display.value})` : "-";
saveState();
return;
}
if (action === "equals") {
calculate();
}
} catch {
display.value = "Error";
saveState();
}
});
});
minimize.addEventListener("click", () => {
const wasMinimized = calc.classList.contains("mft-minimized");
const rectBefore = calc.getBoundingClientRect();
const rightEdge = rectBefore.right;
const isMinimized = calc.classList.toggle("mft-minimized");
const newWidth = isMinimized ? 130 : 260;
calc.style.width = `${newWidth}px`;
calc.style.minWidth = `${newWidth}px`;
const safeLeft = Math.max(0, Math.min(rightEdge - newWidth, window.innerWidth - newWidth));
calc.style.left = `${safeLeft}px`;
calc.style.right = "auto";
minimize.textContent = isMinimized ? "+" : "−";
saveState();
});
window.addEventListener("beforeunload", saveState);
makeDraggable(calc, header, minimize, saveState);
}
function setupTimer() {
if (document.getElementById("mateo-universal-timer")) return;
const stateKey = "mateo-universal-floating-timer-state-v2";
const saved = loadJSON(stateKey);
const minimized = saved.minimized !== undefined ? saved.minimized : true;
const timer = document.createElement("div");
timer.id = "mateo-universal-timer";
timer.className = "mft-widget";
if (minimized) timer.classList.add("mft-minimized");
timer.style.top = typeof saved.top === "number" ? `${saved.top}px` : "20px";
timer.style.left = typeof saved.left === "number" ? `${saved.left}px` : "50%";
timer.style.transform = typeof saved.left === "number" ? "none" : "translateX(-50%)";
timer.innerHTML = `
<div id="mut-header" class="mft-header">
<span>⏱ Timer</span>
<button id="mut-toggle" class="mft-toggle" type="button">${minimized ? "+" : "−"}</button>
</div>
<div id="mut-body" class="mft-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 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();
saveJSON(stateKey, {
minimized: timer.classList.contains("mft-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("mft-minimized");
toggle.textContent = timer.classList.contains("mft-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();
}
});
});
window.addEventListener("beforeunload", saveState);
updateDisplay();
resumeIfNeeded();
makeDraggable(timer, header, toggle, saveState);
}
setupNotes();
setupCalculator();
setupTimer();
})();