Universal Macro Pro v0.1

Record/play macros with 1-5 keys. Fixed infinite loop, Save/Load, and Draggable UI.

Dovrai installare un'estensione come Tampermonkey, Greasemonkey o Violentmonkey per installare questo script.

You will need to install an extension such as Tampermonkey to install this script.

Dovrai installare un'estensione come Tampermonkey o Violentmonkey per installare questo script.

Dovrai installare un'estensione come Tampermonkey o Userscripts per installare questo script.

Dovrai installare un'estensione come ad esempio Tampermonkey per installare questo script.

Dovrai installare un gestore di script utente per installare questo script.

(Ho già un gestore di script utente, lasciamelo installare!)

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

(Ho già un gestore di stile utente, lasciamelo installare!)

// ==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;
 
})();