Universal Macro Pro v0.1

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

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey, Greasemonkey или Violentmonkey.

За да инсталирате този скрипт, трябва да инсталирате разширение, като например Tampermonkey .

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey или Violentmonkey.

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey или Userscripts.

За да инсталирате скрипта, трябва да инсталирате разширение като Tampermonkey.

За да инсталирате този скрипт, трябва да имате инсталиран скриптов мениджър.

(Вече имам скриптов мениджър, искам да го инсталирам!)

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

(Вече имам инсталиран мениджър на стиловете, искам да го инсталирам!)

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