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

})();