Lemonade Information Panel

Add information panel button to Lemonade interface

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==UserScript==
// @name         Lemonade Information Panel
// @namespace    http://tampermonkey.net/
// @version      2.0.3
// @description  Add information panel button to Lemonade interface
// @author       flag & Silver
// @match        https://lemonade.gg/code/*
// @match        https://*.lemonade.gg/code/*
// @grant        none
// @run-at       document-end
// ==/UserScript==

(function() {
    'use strict';

    // Script configuration - Add your Greasyfork script URLs here
    const SCRIPTS_CONFIG = [
        'https://greasyfork.org/en/scripts/561100-lemonade-project-planner',
        'https://greasyfork.org/en/scripts/560681-lemonade-css-injector',
        'https://greasyfork.org/en/scripts/557182-lemonade-prompt-builder'
        // Add more script URLs here as needed
    ];

    // Inject base styles
    function injectStyles() {
        if (document.getElementById('lpb-info-styles')) return;

        const style = document.createElement('style');
        style.id = 'lpb-info-styles';
        style.textContent = `
            #lpb-info-overlay {
                display: none;
                position: fixed;
                inset: 0;
                background: rgba(0, 0, 0, 0.75);
                backdrop-filter: blur(8px);
                -webkit-backdrop-filter: blur(8px);
                z-index: 99999;
                justify-content: center;
                align-items: center;
            }

            #lpb-info-overlay.open {
                display: flex;
            }

            #lpb-info-modal {
                width: 95%;
                max-width: 900px;
                max-height: 85vh;
                overflow: hidden;
                display: flex;
                flex-direction: column;
                animation: lpbModalIn 0.2s ease;
            }

            @keyframes lpbModalIn {
                from { opacity: 0; transform: scale(0.95) translateY(10px); }
                to { opacity: 1; transform: scale(1) translateY(0); }
            }

            #lpb-info-modal .modal-content {
                flex: 1;
                overflow-y: auto;
                padding: 20px;
            }

            #lpb-info-modal .info-section {
                margin-bottom: 24px;
                padding: 20px;
                background: rgba(0,0,0,0.3);
                border: 1px solid rgba(255,255,255,0.1);
                border-radius: 10px;
            }

            #lpb-info-modal .info-section:last-child {
                margin-bottom: 0;
            }

            #lpb-info-modal .section-header {
                display: flex;
                align-items: center;
                gap: 12px;
                margin-bottom: 16px;
                padding-bottom: 12px;
                border-bottom: 1px solid rgba(255,255,255,0.1);
            }

            #lpb-info-modal .section-icon {
                width: 40px;
                height: 40px;
                border-radius: 8px;
                display: flex;
                align-items: center;
                justify-content: center;
                flex-shrink: 0;
            }

            #lpb-info-modal .section-icon.purple {
                background: rgba(147, 51, 234, 0.2);
                color: #9333ea;
            }

            #lpb-info-modal .section-icon.green {
                background: rgba(34, 197, 94, 0.2);
                color: #22c55e;
            }

            #lpb-info-modal .section-icon.blue {
                background: rgba(59, 130, 246, 0.2);
                color: #3b82f6;
            }

            #lpb-info-modal .section-icon.red {
                background: rgba(239, 68, 68, 0.2);
                color: #ef4444;
            }

            #lpb-info-modal .section-icon.yellow {
                background: rgba(234, 179, 8, 0.2);
                color: #eab308;
            }

            #lpb-info-modal .section-icon.pink {
                background: rgba(236, 72, 153, 0.2);
                color: #ec4899;
            }

            #lpb-info-modal .section-title {
                font-size: 16px;
                font-weight: 600;
            }

            #lpb-info-modal .section-body {
                font-size: 14px;
                line-height: 1.6;
                opacity: 0.9;
            }

            #lpb-info-modal .info-item {
                margin-bottom: 12px;
            }

            #lpb-info-modal .info-item:last-child {
                margin-bottom: 0;
            }

            #lpb-info-modal .info-label {
                font-weight: 500;
                opacity: 0.7;
                font-size: 12px;
                text-transform: uppercase;
                letter-spacing: 0.5px;
                margin-bottom: 4px;
            }

            #lpb-info-modal .info-value {
                font-size: 14px;
            }

            #lpb-info-modal .info-value a {
                word-break: break-all;
            }

            .lpb-btn {
                display: inline-flex !important;
                align-items: center !important;
                justify-content: center !important;
                gap: 6px;
                padding: 0 14px !important;
                height: 36px !important;
                font-size: 13px;
                font-weight: 500;
                border-radius: 8px;
                cursor: pointer;
                transition: all 0.15s ease;
                border: 1px solid rgba(255,255,255,0.12);
                background: rgba(255,255,255,0.05);
                color: inherit;
                box-sizing: border-box !important;
            }

            .lpb-btn:hover {
                background: rgba(255,255,255,0.1);
            }

            .lpb-btn-sm {
                height: 32px !important;
                padding: 0 12px !important;
                font-size: 12px;
            }

            .lpb-btn-icon {
                width: 36px !important;
                min-width: 36px !important;
                max-width: 36px !important;
                padding: 0 !important;
            }

            .lpb-btn-icon.lpb-btn-sm {
                width: 32px !important;
                min-width: 32px !important;
                max-width: 32px !important;
            }

            .lpb-btn svg {
                flex-shrink: 0 !important;
                width: 12px !important;
                height: 12px !important;
            }

            .loading-pulse {
                animation: pulse 1.5s ease-in-out infinite;
            }

            @keyframes pulse {
                0%, 100% { opacity: 0.6; }
                50% { opacity: 1; }
            }

            @media (max-width: 768px) {
                #lpb-info-modal {
                    height: 90vh;
                }
            }
        `;
        document.head.appendChild(style);
    }

    // Get color and icon based on script index
    function getScriptColor(index) {
        const colors = ['purple', 'green', 'blue', 'red', 'yellow', 'pink'];
        return colors[index % colors.length];
    }

    function getScriptIcon(index) {
        const icons = [
            `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
                <path d="M12 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/>
                <path d="M18.375 2.625a2.121 2.121 0 1 1 3 3L12 15l-4 1 1-4Z"/>
            </svg>`,
            `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
                <path d="M12 20h9"/>
                <path d="M16.5 3.5a2.12 2.12 0 0 1 3 3L7 19l-4 1 1-4Z"/>
            </svg>`,
            `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
                <polyline points="16 18 22 12 16 6"/>
                <polyline points="8 6 2 12 8 18"/>
            </svg>`,
            `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
                <path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/>
                <polyline points="3.27 6.96 12 12.01 20.73 6.96"/>
                <line x1="12" y1="22.08" x2="12" y2="12"/>
            </svg>`,
            `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
                <path d="M12 2v20M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"/>
            </svg>`,
            `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
                <path d="M20.42 4.58a5.4 5.4 0 0 0-7.65 0l-.77.78-.77-.78a5.4 5.4 0 0 0-7.65 0C1.46 6.7 1.33 10.28 4 13l8 8 8-8c2.67-2.72 2.54-6.3.42-8.42z"/>
            </svg>`
        ];
        return icons[index % icons.length];
    }

    // Extract script ID from Greasyfork URL
    function extractScriptId(url) {
        const match = url.match(/scripts\/(\d+)/);
        return match ? match[1] : null;
    }

    // Create the information modal
    function createModal() {
        if (document.getElementById('lpb-info-overlay')) return;

        const overlay = document.createElement('div');
        overlay.id = 'lpb-info-overlay';
        overlay.innerHTML = `
            <div id="lpb-info-modal" class="border shadow-xl dark:bg-background light:bg-background gray:bg-card rounded-xl border-border">
                <div class="h-14 shrink-0 px-4 w-full flex items-center justify-between border-b border-border">
                    <div class="flex items-center gap-3">
                        <span class="text-sm font-semibold">Information</span>
                    </div>
                    <button id="lpb-info-close" class="lpb-btn lpb-btn-sm lpb-btn-icon" title="Close (Esc)">
                        <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">
                            <path d="M18 6 6 18"/><path d="m6 6 12 12"/>
                        </svg>
                    </button>
                </div>

                <div class="modal-content" id="scripts-container">
                    <!-- Scripts will be dynamically loaded here -->
                </div>
            </div>
        `;

        document.body.appendChild(overlay);

        // Setup event listeners
        const closeBtn = document.getElementById('lpb-info-close');
        closeBtn.addEventListener('click', () => overlay.classList.remove('open'));

        overlay.addEventListener('click', (e) => {
            if (e.target === overlay) overlay.classList.remove('open');
        });

        document.addEventListener('keydown', (e) => {
            if (e.key === 'Escape' && overlay.classList.contains('open')) {
                overlay.classList.remove('open');
            }
        });
    }

    // Fetch script data from Greasyfork API
    async function fetchScriptData(url) {
        const scriptId = extractScriptId(url);
        if (!scriptId) {
            console.error('[Info Panel] Invalid Greasyfork URL:', url);
            return null;
        }

        try {
            const response = await fetch(`https://greasyfork.org/en/scripts/${scriptId}.json`);
            if (!response.ok) throw new Error(`HTTP ${response.status}`);
            const data = await response.json();
            return data;
        } catch (error) {
            console.error('[Info Panel] Failed to fetch script data:', error);
            return null;
        }
    }

    // Generate HTML for a script section
    function generateScriptSection(data, index, url) {
        const color = getScriptColor(index);
        const icon = getScriptIcon(index);
        const scriptUrl = url || `https://greasyfork.org${data.url}`;

        return `
            <div class="info-section">
                <div class="section-header">
                    <div class="section-icon ${color}">
                        ${icon}
                    </div>
                    <span class="section-title">${data.name || 'Unknown Script'}</span>
                </div>
                <div class="section-body">
                    <div class="info-item">
                        <div class="info-label">Author</div>
                        <div class="info-value">${data.users && data.users[0] ? data.users[0].name : 'Unknown'}</div>
                    </div>
                    <div class="info-item">
                        <div class="info-label">Version</div>
                        <div class="info-value">${data.version || 'Unknown'}</div>
                    </div>
                    <div class="info-item">
                        <div class="info-label">Total Installs</div>
                        <div class="info-value">${data.total_installs ? data.total_installs.toLocaleString() : '0'}</div>
                    </div>
                    <div class="info-item">
                        <div class="info-label">Description</div>
                        <div class="info-value">${data.description || 'No description available'}</div>
                    </div>
                    <div class="info-item">
                        <div class="info-label">Link</div>
                        <div class="info-value">
                            <a href="${scriptUrl}" target="_blank" style="color: var(--color-${color}, #${color === 'purple' ? '9333ea' : color === 'green' ? '22c55e' : '3b82f6'}); text-decoration: underline;">${scriptUrl}</a>
                        </div>
                    </div>
                </div>
            </div>
        `;
    }

    // Populate information with all configured scripts
    async function populateInfo() {
        const container = document.getElementById('scripts-container');
        if (!container) return;

        // Show loading state
        container.innerHTML = `
            <div class="info-section">
                <div class="section-body" style="text-align: center; padding: 40px;">
                    <div class="loading-pulse" style="font-size: 14px;">Loading scripts...</div>
                </div>
            </div>
        `;

        // Fetch all script data
        const scriptDataPromises = SCRIPTS_CONFIG.map(url => fetchScriptData(url));
        const scriptsData = await Promise.all(scriptDataPromises);

        // Generate HTML for all scripts
        let htmlContent = '';
        scriptsData.forEach((data, index) => {
            if (data) {
                htmlContent += generateScriptSection(data, index, SCRIPTS_CONFIG[index]);
            }
        });

        // Add credits section
        htmlContent += `
            <div class="info-section">
                <div class="section-header">
                    <div class="section-icon blue">
                        <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
                            <path d="M4 15s1-1 4-1 5 2 8 2 4-1 4-1V3s-1 1-4 1-5-2-8-2-4 1-4 1z"/>
                            <line x1="4" x2="4" y1="22" y2="15"/>
                        </svg>
                    </div>
                    <span class="section-title">Credits</span>
                </div>
                <div class="section-body">
                    <div class="info-item">
                        <div class="info-label">Created By</div>
                        <div class="info-value">flag & Silver</div>
                    </div>
                    <div class="info-item">
                        <div class="info-label">Script Name</div>
                        <div class="info-value">Lemonade Information Panel</div>
                    </div>
                    <div class="info-item">
                        <div class="info-label">Version</div>
                        <div class="info-value">2.0.0</div>
                    </div>
                    <div class="info-item">
                        <div class="info-label">Status</div>
                        <div class="info-value" style="color: #22c55e;">Active</div>
                    </div>
                </div>
            </div>
        `;

        container.innerHTML = htmlContent;
    }

    // Inject the information button into the user menu
    function injectInfoButton() {
        // Look for the user menu dropdown
        const menuContent = document.querySelector('[role="menu"][data-radix-menu-content]');
        if (!menuContent || document.getElementById('lpb-info-menu-item')) {
            return;
        }

        // Find the separator before "Log Out"
        const separators = menuContent.querySelectorAll('[role="separator"]');
        const lastSeparator = separators[separators.length - 2]; // Second to last separator
        
        if (!lastSeparator) return;

        // Create the menu item
        const infoMenuItem = document.createElement('div');
        infoMenuItem.id = 'lpb-info-menu-item';
        infoMenuItem.setAttribute('role', 'menuitem');
        infoMenuItem.className = 'relative flex select-none items-center gap-2 rounded-sm px-2 py-1.5 outline-none transition-colors data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&>svg]:size-4 [&>svg]:shrink-0 text-sm cursor-pointer focus:bg-accent focus:text-accent-foreground';
        infoMenuItem.setAttribute('tabindex', '-1');
        infoMenuItem.setAttribute('data-orientation', 'vertical');
        infoMenuItem.setAttribute('data-radix-collection-item', '');
        
        infoMenuItem.innerHTML = `
            <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-flame mr-2 size-4" aria-hidden="true">
                <path d="M12 3q1 4 4 6.5t3 5.5a1 1 0 0 1-14 0 5 5 0 0 1 1-3 1 1 0 0 0 5 0c0-2-1.5-3-1.5-5q0-2 2.5-4"/>
            </svg>
            <span>Information Panel</span>
        `;

        infoMenuItem.addEventListener('click', () => {
            const overlay = document.getElementById('lpb-info-overlay');
            if (overlay) {
                populateInfo(); // Refresh info when opening
                overlay.classList.add('open');
            }
        });

        // Insert after the last separator (before Log Out)
        lastSeparator.parentNode.insertBefore(infoMenuItem, lastSeparator.nextSibling);
        
        // Move the separator to be after the Information Panel item
        infoMenuItem.parentNode.insertBefore(lastSeparator, infoMenuItem.nextSibling);
        
        console.log('[Info Panel] Menu item injected successfully');
    }

    // Watch for menu opening
    function watchForMenu() {
        const observer = new MutationObserver((mutations) => {
            for (const mutation of mutations) {
                for (const node of mutation.addedNodes) {
                    if (node.nodeType === 1) {
                        const menu = node.querySelector ? node.querySelector('[role="menu"][data-radix-menu-content]') : null;
                        if (menu || (node.getAttribute && node.getAttribute('role') === 'menu')) {
                            setTimeout(() => injectInfoButton(), 50);
                        }
                    }
                }
            }
        });

        observer.observe(document.body, {
            childList: true,
            subtree: true
        });
    }

    // Initialize
    function init() {
        injectStyles();
        createModal();
        watchForMenu();

        console.log('[Info Panel] UserScript loaded');
    }

    // Run on load
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', init);
    } else {
        init();
    }

})();