Greasy Fork - Copy Code Button

Adds a split Copy/Download button to every Greasy Fork script code page. One click copies, the arrow lets you download as .js, .md, or .txt. Shows line count and supports Ctrl+Shift+C shortcut.

Verzia zo dňa 08.03.2026. Pozri najnovšiu verziu.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey, Greasemonkey alebo Violentmonkey.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie, ako napríklad Tampermonkey.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey, % alebo Violentmonkey.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey alebo Userscripts.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie, ako napríklad Tampermonkey.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie správcu používateľských skriptov.

(Už mám správcu používateľských skriptov, nechajte ma ho nainštalovať!)

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

(Už mám správcu používateľských štýlov, nechajte ma ho nainštalovať!)

// ==UserScript==
// @name         Greasy Fork - Copy Code Button
// @namespace    https://greasyfork.org/
// @version      2.0
// @description  Adds a split Copy/Download button to every Greasy Fork script code page. One click copies, the arrow lets you download as .js, .md, or .txt. Shows line count and supports Ctrl+Shift+C shortcut.
// @author       achma with claude-AI
// @license MIT
// @match        https://greasyfork.org/*/scripts/*/code
// @match        https://greasyfork.org/scripts/*/code
// @icon         https://greasyfork.org/vite/assets/blacklogo96-CxYTSM_T.png
// @grant        none
// @run-at       document-idle
// ==/UserScript==

(() => {
    'use strict';

    // ─── Styles ───────────────────────────────────────────────────────────────────

    const style = document.createElement('style');
    style.textContent = `
        #gf-copy-toolbar {
            display: flex;
            align-items: center;
            flex-wrap: wrap;
            gap: 10px;
            margin-bottom: 10px;
        }

        /* ── Split button wrapper ── */
        #gf-split-btn-wrap {
            display: inline-flex;
            align-items: stretch;
            border-radius: 7px;
            position: relative;
            box-shadow: 0 2px 8px rgba(0,0,0,0.22);
        }

        /* ── Main copy button ── */
        #gf-copy-btn {
            display: inline-flex;
            align-items: center;
            gap: 7px;
            padding: 7px 14px;
            background: #23272e;
            color: #f3f4f6;
            border: 1.5px solid #3a3f4a;
            border-right: none;
            border-radius: 7px 0 0 7px;
            font-size: 13px;
            font-weight: 600;
            font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
            cursor: pointer;
            transition: background 0.15s, color 0.15s;
            user-select: none;
            line-height: 1;
            white-space: nowrap;
        }
        #gf-copy-btn:hover {
            background: #2d3340;
            color: #fff;
        }
        #gf-copy-btn.gf-copied {
            background: #10b981;
            border-color: #059669;
            color: #fff;
        }
        #gf-copy-btn.gf-error {
            background: #ef4444;
            border-color: #dc2626;
            color: #fff;
        }

        /* ── Divider between copy and chevron ── */
        #gf-btn-divider {
            width: 1px;
            background: #3a3f4a;
            align-self: stretch;
            flex-shrink: 0;
        }

        /* ── Chevron / dropdown trigger ── */
        #gf-chevron-btn {
            display: inline-flex;
            align-items: center;
            justify-content: center;
            padding: 7px 9px;
            background: #23272e;
            color: #9ca3af;
            border: 1.5px solid #3a3f4a;
            border-left: none;
            border-radius: 0 7px 7px 0;
            cursor: pointer;
            transition: background 0.15s, color 0.15s;
            user-select: none;
        }
        #gf-chevron-btn:hover {
            background: #2d3340;
            color: #f3f4f6;
        }
        #gf-chevron-btn svg {
            transition: transform 0.2s cubic-bezier(0.4,0,0.2,1);
            display: block;
        }
        #gf-chevron-btn.open svg {
            transform: rotate(180deg);
        }

        /* ── Dropdown menu ── */
        #gf-dropdown {
            display: none;
            position: absolute;
            top: calc(100% + 6px);
            left: 0;
            min-width: 210px;
            background: #1c2028;
            border: 1.5px solid #3a3f4a;
            border-radius: 8px;
            box-shadow: 0 8px 28px rgba(0,0,0,0.40);
            z-index: 999999;
            overflow: hidden;
            animation: gf-drop-in 0.15s cubic-bezier(0.4,0,0.2,1);
        }
        #gf-dropdown.open {
            display: block;
        }
        @keyframes gf-drop-in {
            from { opacity: 0; transform: translateY(-6px) scale(0.98); }
            to   { opacity: 1; transform: translateY(0) scale(1); }
        }

        .gf-drop-header {
            padding: 9px 14px 5px;
            font-size: 10px;
            font-weight: 700;
            letter-spacing: 0.09em;
            text-transform: uppercase;
            color: #6b7280;
            font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
            user-select: none;
        }

        .gf-drop-divider {
            height: 1px;
            background: #2d3340;
            margin: 3px 10px;
        }

        .gf-drop-item {
            display: flex;
            align-items: center;
            gap: 10px;
            padding: 9px 14px;
            font-size: 13px;
            font-weight: 500;
            color: #d1d5db;
            cursor: pointer;
            font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
            transition: background 0.12s, color 0.12s;
            user-select: none;
        }
        .gf-drop-item:hover {
            background: #2d3340;
            color: #fff;
        }
        .gf-drop-item:last-child {
            margin-bottom: 5px;
        }

        .gf-drop-badge {
            display: inline-flex;
            align-items: center;
            justify-content: center;
            min-width: 34px;
            padding: 2px 7px;
            background: #2d3340;
            color: #9ca3af;
            border-radius: 4px;
            font-size: 11px;
            font-weight: 700;
            font-family: monospace;
            transition: background 0.12s, color 0.12s;
            flex-shrink: 0;
        }
        .gf-drop-item:hover .gf-drop-badge {
            background: #3b82f6;
            color: #fff;
        }

        /* ── Code stats ── */
        #gf-code-stats {
            font-size: 12px;
            color: #6b7280;
            font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
            user-select: none;
            display: flex;
            align-items: center;
            gap: 6px;
        }

        /* ── Shortcut hint ── */
        #gf-shortcut-hint {
            font-size: 11px;
            color: #9ca3af;
            font-family: monospace;
            background: #f3f4f6;
            border: 1px solid #e5e7eb;
            border-radius: 4px;
            padding: 2px 7px;
            user-select: none;
        }

        @media (prefers-color-scheme: dark) {
            #gf-code-stats { color: #9ca3af; }
            #gf-shortcut-hint { background: #2d3340; border-color: #3a3f4a; color: #9ca3af; }
        }
        html.dark #gf-code-stats { color: #9ca3af; }
        html.dark #gf-shortcut-hint { background: #2d3340; border-color: #3a3f4a; color: #9ca3af; }
    `;
    document.head.appendChild(style);

    // ─── Icons ────────────────────────────────────────────────────────────────────

    const ICON_COPY = `<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24"
        fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round">
        <rect x="9" y="9" width="13" height="13" rx="2"/>
        <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/>
    </svg>`;

    const ICON_CHECK = `<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24"
        fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
        <polyline points="20 6 9 17 4 12"/>
    </svg>`;

    const ICON_ERROR = `<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24"
        fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
        <circle cx="12" cy="12" r="10"/>
        <line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/>
    </svg>`;

    const ICON_CHEVRON = `<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24"
        fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
        <polyline points="6 9 12 15 18 9"/>
    </svg>`;

    const ICON_DOWNLOAD = `<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24"
        fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round">
        <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
        <polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/>
    </svg>`;

    // ─── Helpers ──────────────────────────────────────────────────────────────────

    function getCodeText() {
        const pre = document.querySelector('.code-container pre');
        if (!pre) return null;
        return pre.innerText || pre.textContent || null;
    }

    function getScriptName() {
        const h2 = document.querySelector('#script-info h2');
        const raw = h2
            ? h2.textContent.trim()
            : document.title.replace(/\s*[-|].*$/, '').trim();
        return raw.replace(/[^\w\s\-().]/g, '').replace(/\s+/g, '_').substring(0, 80) || 'script';
    }

    function formatNumber(n) { return n.toLocaleString(); }
    function isMac() { return /Mac|iPhone|iPod|iPad/.test(navigator.platform); }
    function modKey() { return isMac() ? '⌘' : 'Ctrl'; }

    // ─── Download ─────────────────────────────────────────────────────────────────

    function downloadAs(ext) {
        const code = getCodeText();
        if (!code) return;

        const name = getScriptName();
        let content = code;
        let mime = 'text/plain';

        if (ext === 'js')  { mime = 'text/javascript'; }
        if (ext === 'md')  {
            const title = name.replace(/_/g, ' ');
            content = `# ${title}\n\n\`\`\`javascript\n${code}\n\`\`\`\n`;
            mime = 'text/markdown';
        }

        const blob = new Blob([content], { type: mime });
        const url  = URL.createObjectURL(blob);
        const a    = Object.assign(document.createElement('a'), { href: url, download: `${name}.${ext}` });
        document.body.appendChild(a);
        a.click();
        a.remove();
        setTimeout(() => URL.revokeObjectURL(url), 1000);
    }

    // ─── Copy ─────────────────────────────────────────────────────────────────────

    let resetTimer = null;

    async function copyCode(btn) {
        const code = getCodeText();
        if (!code) { setButtonState(btn, 'error', 'Nothing to copy'); return; }

        try {
            await navigator.clipboard.writeText(code);
            setButtonState(btn, 'success', 'Copied!');
        } catch {
            try {
                const ta = document.createElement('textarea');
                ta.value = code;
                Object.assign(ta.style, { position: 'fixed', opacity: '0', pointerEvents: 'none' });
                document.body.appendChild(ta);
                ta.select();
                const ok = document.execCommand('copy');
                ta.remove();
                if (ok) setButtonState(btn, 'success', 'Copied!');
                else    setButtonState(btn, 'error',   'Failed');
            } catch {
                setButtonState(btn, 'error', 'Failed');
            }
        }
    }

    function setButtonState(btn, state, label) {
        clearTimeout(resetTimer);
        btn.classList.remove('gf-copied', 'gf-error');
        if (state === 'success') {
            btn.classList.add('gf-copied');
            btn.innerHTML = `${ICON_CHECK}<span>${label}</span>`;
        } else if (state === 'error') {
            btn.classList.add('gf-error');
            btn.innerHTML = `${ICON_ERROR}<span>${label}</span>`;
        }
        resetTimer = setTimeout(() => {
            btn.classList.remove('gf-copied', 'gf-error');
            btn.innerHTML = `${ICON_COPY}<span>Copier</span>`;
        }, 2500);
    }

    // ─── Build UI ─────────────────────────────────────────────────────────────────

    function injectToolbar() {
        if (document.getElementById('gf-copy-toolbar')) return;

        const codeContainer = document.querySelector('.code-container');
        if (!codeContainer) return;

        const wrapDiv = codeContainer.previousElementSibling;
        const hasWrapDiv = wrapDiv && wrapDiv.querySelector('#wrap-lines');

        const toolbar = document.createElement('div');
        toolbar.id = 'gf-copy-toolbar';

        if (hasWrapDiv) toolbar.appendChild(wrapDiv);

        // ── Split button wrapper
        const splitWrap = document.createElement('div');
        splitWrap.id = 'gf-split-btn-wrap';

        // ── Copy button
        const copyBtn = document.createElement('button');
        copyBtn.id = 'gf-copy-btn';
        copyBtn.innerHTML = `${ICON_COPY}<span>Copier</span>`;
        copyBtn.title = `Copy full script source (${modKey()}+Shift+C)`;
        copyBtn.addEventListener('click', () => copyCode(copyBtn));

        // ── Visual divider
        const divider = document.createElement('div');
        divider.id = 'gf-btn-divider';

        // ── Chevron button
        const chevronBtn = document.createElement('button');
        chevronBtn.id = 'gf-chevron-btn';
        chevronBtn.innerHTML = ICON_CHEVRON;
        chevronBtn.title = 'Download options';
        chevronBtn.setAttribute('aria-haspopup', 'true');
        chevronBtn.setAttribute('aria-expanded', 'false');

        // ── Dropdown
        const dropdown = document.createElement('div');
        dropdown.id = 'gf-dropdown';
        dropdown.setAttribute('role', 'menu');
        dropdown.innerHTML = `
            <div class="gf-drop-header">Download as</div>
            <div class="gf-drop-divider"></div>
            <div class="gf-drop-item" data-ext="js" role="menuitem">
                ${ICON_DOWNLOAD}
                <span style="flex:1">JavaScript file</span>
                <span class="gf-drop-badge">.js</span>
            </div>
            <div class="gf-drop-item" data-ext="txt" role="menuitem">
                ${ICON_DOWNLOAD}
                <span style="flex:1">Plain text</span>
                <span class="gf-drop-badge">.txt</span>
            </div>
            <div class="gf-drop-item" data-ext="md" role="menuitem">
                ${ICON_DOWNLOAD}
                <span style="flex:1">Markdown (code block)</span>
                <span class="gf-drop-badge">.md</span>
            </div>
        `;

        dropdown.querySelectorAll('.gf-drop-item').forEach(item => {
            item.addEventListener('click', () => {
                downloadAs(item.dataset.ext);
                closeDropdown();
            });
        });

        // ── Dropdown toggle logic
        function openDropdown() {
            dropdown.classList.add('open');
            chevronBtn.classList.add('open');
            chevronBtn.setAttribute('aria-expanded', 'true');
        }
        function closeDropdown() {
            dropdown.classList.remove('open');
            chevronBtn.classList.remove('open');
            chevronBtn.setAttribute('aria-expanded', 'false');
        }
        function isOpen() { return dropdown.classList.contains('open'); }

        chevronBtn.addEventListener('click', (e) => {
            e.stopPropagation();
            isOpen() ? closeDropdown() : openDropdown();
        });

        document.addEventListener('click', (e) => {
            if (!splitWrap.contains(e.target)) closeDropdown();
        });

        document.addEventListener('keydown', (e) => {
            if (e.key === 'Escape' && isOpen()) closeDropdown();
        });

        splitWrap.appendChild(copyBtn);
        splitWrap.appendChild(divider);
        splitWrap.appendChild(chevronBtn);
        splitWrap.appendChild(dropdown);
        toolbar.appendChild(splitWrap);

        // ── Code stats
        const code = getCodeText();
        if (code) {
            const lines = code.split('\n').length;
            const chars = code.length;
            const stats = document.createElement('span');
            stats.id = 'gf-code-stats';
            stats.title = 'Lines / Characters in this script';
            stats.innerHTML = `
                <svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24"
                    fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
                    <line x1="8" y1="6" x2="21" y2="6"/><line x1="8" y1="12" x2="21" y2="12"/>
                    <line x1="8" y1="18" x2="21" y2="18"/><line x1="3" y1="6" x2="3.01" y2="6"/>
                    <line x1="3" y1="12" x2="3.01" y2="12"/><line x1="3" y1="18" x2="3.01" y2="18"/>
                </svg>
                ${formatNumber(lines)} lines &nbsp;·&nbsp; ${formatNumber(chars)} chars
            `;
            toolbar.appendChild(stats);
        }

        // ── Keyboard shortcut hint
        const hint = document.createElement('span');
        hint.id = 'gf-shortcut-hint';
        hint.textContent = `${modKey()}+Shift+C`;
        hint.title = 'Keyboard shortcut to copy the code';
        toolbar.appendChild(hint);

        codeContainer.parentNode.insertBefore(toolbar, codeContainer);
    }

    // ─── Keyboard shortcut ────────────────────────────────────────────────────────

    document.addEventListener('keydown', (e) => {
        const mod = isMac() ? e.metaKey : e.ctrlKey;
        if (mod && e.shiftKey && e.key.toLowerCase() === 'c') {
            e.preventDefault();
            const btn = document.getElementById('gf-copy-btn');
            if (btn) copyCode(btn);
        }
    });

    // ─── Init ─────────────────────────────────────────────────────────────────────

    function tryInit(attempts = 0) {
        if (document.querySelector('.code-container pre')) {
            injectToolbar();
        } else if (attempts < 20) {
            setTimeout(() => tryInit(attempts + 1), 300);
        }
    }

    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', () => tryInit());
    } else {
        tryInit();
    }
})();