Record/play macros with 1-5 keys. Fixed infinite loop, Save/Load, and Draggable UI.
// ==UserScript==
// @name Universal Macro Pro v0.1
// @namespace http://tampermonkey.net/
// @version 0.1
// @description Record/play macros with 1-5 keys. Fixed infinite loop, Save/Load, and Draggable UI.
// @author @Denysmilano94®
// @license MIT
// @match *://*/*
// @grant none
// ==/UserScript==
(function() {
"use strict";
let isRecording = false;
let isPlaying = false;
let macro = [];
let startTime = 0;
let pauseOffset = 0;
let timeouts = [];
let loopLimit = 1;
let currentLoop = 0;
// --- UI SETUP ---
const toggleBtn = document.createElement("div");
toggleBtn.id = "macro-toggle";
toggleBtn.innerHTML = "⚙️";
toggleBtn.style = "position: fixed; top: 20px; right: 20px; z-index: 10002; background: #58cc02; color: white; width: 40px; height: 40px; display: flex; align-items: center; justify-content: center; border-radius: 50%; cursor: move; font-size: 20px; box-shadow: 0 4px 10px rgba(0,0,0,0.5); user-select: none;";
document.body.appendChild(toggleBtn);
const container = document.createElement("div");
container.id = "macro-container";
container.style = "position: fixed; top: 70px; right: 20px; z-index: 10001; background: #1a1a1a; color: white; padding: 15px; border-radius: 12px; font-family: 'Segoe UI', sans-serif; box-shadow: 0 10px 25px rgba(0,0,0,0.7); width: 250px; border: 1px solid #333; user-select: none; display: none;";
container.innerHTML = `
<div id="macro-header" style="margin-bottom: 12px; font-weight: bold; color: #58cc02; text-align: center; border-bottom: 1px solid #333; padding-bottom: 8px; cursor: move; font-size: 15px;">
UNIVERSAL MACRO PRO v0.1
<div style="margin-top: 12px; display: flex; justify-content: center; gap: 20px; align-items: center;">
<a href="https://www.threads.net/@denys_894" target="_blank" title="Threads">
<svg width="24" height="24" viewBox="0 0 24 24" fill="#ffffff"><path d="M14.886 12.269c.004.577-.04 1.15-.13 1.714-.143.896-.466 1.701-.947 2.34-.482.639-1.125 1.054-1.886 1.217-.36.077-.733.115-1.107.114-.73-.002-1.446-.168-2.096-.486-.65-.318-1.206-.777-1.625-1.342-.42-.565-.71-1.22-.848-1.916-.139-.696-.16-1.411-.061-2.119.1-.708.31-1.393.619-2.03.31-.637.717-1.212 1.207-1.69.49-.478 1.06-.856 1.685-1.11.625-.254 1.295-.386 1.97-.389.873.004 1.72.247 2.449.704.73.457 1.326 1.101 1.722 1.861.16-.38.358-.742.593-1.082.49-.705 1.127-1.29 1.875-1.72.748-.43 1.584-.66 2.456-.677.371-.007.742.025 1.109.096 1.053.203 2.016.732 2.768 1.519.752.787 1.258 1.8 1.455 2.91.197 1.111.128 2.249-.197 3.32-.325 1.07-.903 2.053-1.685 2.863-.782.81-1.745 1.432-2.805 1.815-1.06.383-2.193.565-3.329.533-1.614-.045-3.18-.535-4.575-1.424-1.396-.89-2.545-2.147-3.355-3.669-.81-1.522-1.233-3.23-1.234-4.965.001-1.734.426-3.441 1.239-4.96.812-1.52 1.964-2.774 3.363-3.66.194-.124.4-.233.614-.326C10.74 2.27 12.35 1.98 13.974 1.986c1.55-.006 3.087.24 4.544.727 1.457.487 2.802 1.237 3.971 2.213l-1.452 1.565c-.968-.809-2.083-1.43-3.284-1.83-1.2-.4-2.464-.601-3.737-.594-1.332-.005-2.653.233-3.896.702-1.243.469-2.373 1.186-3.333 2.112-.96.926-1.737 2.036-2.293 3.275-.556 1.24-.838 2.578-.832 3.93.003 1.35.289 2.686.845 3.923.556 1.237 1.334 2.346 2.295 3.27.962.925 2.091 1.642 3.334 2.113 1.243.471 2.563.712 3.895.711.954.024 1.908-.124 2.806-.437.898-.313 1.722-.816 2.423-1.482.7-.665 1.252-1.478 1.623-2.392.37-.913.563-1.895.568-2.887 0-.74-.13-1.474-.384-2.164-.253-.69-.63-1.314-1.112-1.843-.482-.528-1.07-.95-1.728-1.239-.658-.289-1.374-.437-2.1-.435-.615.004-1.223.111-1.792.316-.57.204-1.084.516-1.516.918-.432.401-.78.887-1.025 1.432-.244.544-.38 1.13-.398 1.726l-.01.422-.003.425-.01.422z"/></svg>
</a>
<a href="https://paypal.me/DenysBixio" target="_blank" title="Donate via PayPal">
<svg width="24" height="24" viewBox="0 0 24 24" fill="#003087"><path d="M20.067 6.947c.49 2.94-.36 5.61-3.23 6.08h-2.14c-.67 0-1.22.48-1.34 1.14l-.84 5.34c-.05.34-.35.59-.69.59H8.647c-.44 0-.76-.41-.69-.84l2.12-13.43c.1-.66.67-1.14 1.34-1.14h3.9c2.85 0 4.26 1.31 4.75 4.26z"/><path d="M15.427 6.947c.49 2.94-.36 5.61-3.23 6.08h-2.14c-.67 0-1.22.48-1.34 1.14l-.84 5.34c-.05.34-.35.59-.69.59H4.007c-.44 0-.76-.41-.69-.84l2.12-13.43c.1-.66.67-1.14 1.34-1.14h3.9c2.85 0 4.26 1.31 4.75 4.26z" fill="#009cde" opacity="0.6"/></svg>
</a>
</div>
</div>
<button id="recBtn" style="background: #ff4444; color: white; border: 2px solid #fff; padding: 12px; cursor: pointer; border-radius: 8px; font-weight: 900; width: 100%; margin-bottom: 8px; font-size: 11px; letter-spacing: 0.5px; text-shadow: 1px 1px 2px rgba(0,0,0,0.5);">REC (1)</button>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin-bottom: 8px;">
<button id="stopBtn" style="background: #ff9800; color: white; border: none; padding: 12px; cursor: pointer; border-radius: 8px; font-weight: bold; font-size: 11px;">STOP (2)</button>
<button id="playBtn" style="background: #44bb44; color: white; border: none; padding: 12px; cursor: pointer; border-radius: 8px; font-weight: bold; font-size: 11px;">PLAY (3)</button>
</div>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin-bottom: 8px;">
<button id="saveBtn" style="background: #2196F3; color: white; border: none; padding: 10px; cursor: pointer; border-radius: 8px; font-weight: bold; font-size: 10px;">💾 SAVE (4)</button>
<button id="loadBtn" style="background: #9C27B0; color: white; border: none; padding: 10px; cursor: pointer; border-radius: 8px; font-weight: bold; font-size: 10px;">📂 LOAD (5)</button>
</div>
<div style="background: #252525; padding: 10px; border-radius: 8px; margin-bottom: 8px; border: 1px solid #444; display: flex; justify-content: space-between; align-items: center;">
<span style="font-size: 11px; font-weight: bold; color: #aaa;">LOOPS (0=∞):</span>
<input type="number" id="loopInput" value="1" min="0" style="width: 55px; background: #000; color: #58cc02; border: 1px solid #555; border-radius: 4px; padding: 3px; font-size: 12px; text-align: center;">
</div>
<div id="status" style="margin-bottom: 10px; font-size: 13px; color: #58cc02; text-align: center; font-weight: bold; min-height: 16px; text-transform: uppercase; letter-spacing: 1px;">Ready</div>
<details style="background: #252525; border-radius: 8px; border: 1px solid #444; cursor: pointer;">
<summary style="padding: 8px; font-size: 11px; color: #aaa; text-align: center; list-style: none;">HOTKEYS GUIDE</summary>
<div style="padding: 10px; border-top: 1px solid #333; font-size: 12px; line-height: 1.8;">
1: Rec/Resume | 2: Stop/Pause | 3: Play | 4: Save | 5: Load | 6: Clear | ESC: Kill
</div>
</details>
`;
document.body.appendChild(container);
const recBtn = document.getElementById("recBtn");
const playBtn = document.getElementById("playBtn");
const stopBtn = document.getElementById("stopBtn");
const saveBtn = document.getElementById("saveBtn");
const loadBtn = document.getElementById("loadBtn");
const status = document.getElementById("status");
const loopInput = document.getElementById("loopInput");
// --- TOGGLE & DRAG LOGIC ---
let moved = false;
toggleBtn.onclick = () => { if(!moved) container.style.display = container.style.display === "none" ? "block" : "none"; };
function makeDraggable(el, handle) {
let pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0;
handle.onmousedown = (e) => {
moved = false;
pos3 = e.clientX; pos4 = e.clientY;
document.onmouseup = () => { document.onmouseup = null; document.onmousemove = null; };
document.onmousemove = (e) => {
moved = true;
pos1 = pos3 - e.clientX; pos2 = pos4 - e.clientY;
pos3 = e.clientX; pos4 = e.clientY;
el.style.top = (el.offsetTop - pos2) + "px";
el.style.left = (el.offsetLeft - pos1) + "px";
el.style.right = "auto";
};
};
}
makeDraggable(container, document.getElementById("macro-header"));
makeDraggable(toggleBtn, toggleBtn);
// --- CORE MACRO LOGIC ---
const saveMacro = () => {
if (macro.length === 0) { status.innerText = "Empty!"; status.style.color = "#f44"; return; }
localStorage.setItem('universal_macro_v2', JSON.stringify(macro));
status.innerText = "Saved!"; status.style.color = "#2196F3";
};
const loadMacro = () => {
const data = localStorage.getItem('universal_macro_v2');
if (data) {
macro = JSON.parse(data);
status.innerText = "Loaded: " + macro.length; status.style.color = "#9C27B0";
} else { status.innerText = "No data!"; status.style.color = "#f44"; }
};
const stopAll = () => {
timeouts.forEach(clearTimeout); timeouts = [];
isPlaying = false; currentLoop = 0;
if (isRecording) {
pauseOffset = Date.now() - startTime;
isRecording = false;
status.innerText = "Paused";
} else { status.innerText = "Stopped"; }
status.style.color = "#ff9800";
recBtn.innerText = "RESUME (1)";
};
const toggleRecording = () => {
if (isPlaying) return;
isRecording = !isRecording;
if (isRecording) {
if (pauseOffset === 0) { macro = []; startTime = Date.now(); }
else startTime = Date.now() - pauseOffset;
recBtn.innerText = "STOP REC";
status.innerText = "🔴 Rec..."; status.style.color = "#ff4444";
} else {
pauseOffset = 0; recBtn.innerText = "REC (1)";
status.innerText = "Saved: " + macro.length; status.style.color = "#58cc02";
}
};
const runPlaybackCycle = () => {
if (loopLimit !== 0 && currentLoop >= loopLimit) {
isPlaying = false;
status.innerText = "Finished";
return;
}
status.innerText = `Loop ${currentLoop + 1} / ${loopLimit === 0 ? "∞" : loopLimit}`;
macro.forEach((action, index) => {
const t = setTimeout(() => {
if (!isPlaying) return;
const el = document.querySelector(action.selector);
if (el) el.click();
if (index === macro.length - 1) {
currentLoop++;
setTimeout(runPlaybackCycle, 500);
}
}, action.time);
timeouts.push(t);
});
};
const startPlayback = () => {
if (macro.length === 0 || isPlaying) return;
loopLimit = parseInt(loopInput.value);
if (isNaN(loopLimit)) loopLimit = 1;
currentLoop = 0;
isPlaying = true;
isRecording = false;
runPlaybackCycle();
};
const getPathTo = (element) => {
if (element.id !== "") return "#" + CSS.escape(element.id);
if (element === document.body) return element.tagName;
let ix = 0, siblings = element.parentNode.childNodes;
for (let i = 0; i < siblings.length; i++) {
let sibling = siblings[i];
if (sibling === element) return getPathTo(element.parentNode) + " > " + element.tagName + ":nth-of-type(" + (ix + 1) + ")";
if (sibling.nodeType === 1 && sibling.tagName === element.tagName) ix++;
}
};
window.addEventListener("click", (e) => {
if (!isRecording || e.target.closest("#macro-container") || e.target.closest("#macro-toggle")) return;
macro.push({ time: Date.now() - startTime, selector: getPathTo(e.target) });
status.innerText = "Clicks: " + macro.length;
}, true);
window.addEventListener("keydown", (e) => {
if (document.activeElement.tagName === "INPUT") return;
switch(e.key) {
case '1': e.preventDefault(); toggleRecording(); break;
case '2': e.preventDefault(); stopAll(); break;
case '3': e.preventDefault(); startPlayback(); break;
case '4': e.preventDefault(); saveMacro(); break;
case '5': e.preventDefault(); loadMacro(); break;
case '6': e.preventDefault(); stopAll(); macro = []; pauseOffset = 0; status.innerText = "Cleared"; break;
case 'Escape': stopAll(); break;
}
});
saveBtn.onclick = saveMacro; loadBtn.onclick = loadMacro;
recBtn.onclick = toggleRecording; playBtn.onclick = startPlayback; stopBtn.onclick = stopAll;
})();