Universal Macro Pro v0.1

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

Vous devrez installer une extension telle que Tampermonkey, Greasemonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Userscripts pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey pour installer ce script.

Vous devrez installer une extension de gestionnaire de script utilisateur pour installer ce script.

(J'ai déjà un gestionnaire de scripts utilisateur, laissez-moi l'installer !)

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

(J'ai déjà un gestionnaire de style utilisateur, laissez-moi l'installer!)

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