Universal Macro Pro v0.1

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

Du musst eine Erweiterung wie Tampermonkey, Greasemonkey oder Violentmonkey installieren, um dieses Skript zu installieren.

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

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

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

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

Sie müssten eine Skript Manager Erweiterung installieren damit sie dieses Skript installieren können

(Ich habe schon ein Skript Manager, Lass mich es installieren!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

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