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.

2026/03/08のページです。最新版はこちら

スクリプトをインストールするには、Tampermonkey, GreasemonkeyViolentmonkey のような拡張機能のインストールが必要です。

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

スクリプトをインストールするには、TampermonkeyViolentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、TampermonkeyUserscripts のような拡張機能のインストールが必要です。

このスクリプトをインストールするには、Tampermonkeyなどの拡張機能をインストールする必要があります。

このスクリプトをインストールするには、ユーザースクリプト管理ツールの拡張機能をインストールする必要があります。

(ユーザースクリプト管理ツールは設定済みなのでインストール!)

このスタイルをインストールするには、Stylusなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus などの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus tなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

(ユーザースタイル管理ツールは設定済みなのでインストール!)

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください
// ==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();
    }
})();