Greasy Fork

Greasy Fork is available in English.

ScriptHub - Available Scripts Finder

Shows available userscripts for the current website from Greasy Fork

当前为 2024-12-11 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         ScriptHub - Available Scripts Finder
// @name:zh      ScriptHub - 🧲 高效查找当前网站可用油猴脚本 🔍
// @namespace    http://tampermonkey.net/
// @version      1.1
// @description  Shows available userscripts for the current website from Greasy Fork
// @description:zh 在页面右下角显示当前网站可用的油猴脚本数量,点击查看详情
// @author       Musk
// @match        *://*/*
// @grant        GM_addStyle
// @grant        GM_xmlhttpRequest
// @grant        GM_getResourceText
// @resource     SITE_DATA http://greasyfork.icu/scripts/by-site.json
// @connect      greasyfork.org
// @connect      www.greasyfork.org
// @connect      *
// @run-at       document-end
// @noframes
// @license MIT
// ==/UserScript==

(function() {
    'use strict';

    GM_addStyle(`
        .script-hub-button {
            position: fixed;
            right: 20px;
            bottom: 20px;
            padding: 6px 12px;
            background: rgba(255, 255, 255, 0.15);
            border-radius: 20px;
            box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
            cursor: move;
            user-select: none;
            z-index: 9999;
            display: flex;
            align-items: center;
            gap: 8px;
            font-size: 12px;
            transition: all 0.2s ease;
        }

        .script-hub-button:hover {
            background: rgba(255, 255, 255, 0.7);
            box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
        }

        .script-hub-button .close {
            margin-left: 4px;
            cursor: pointer;
            opacity: 0.6;
            font-size: 12px;
            padding: 2px;
        }

        .script-hub-button .close:hover {
            opacity: 1;
        }

        .script-hub-sidebar {
            position: fixed;
            top: 0;
            right: -400px;
            width: 400px;
            height: 100vh;
            background: #f8f9fa;
            box-shadow: -2px 0 5px rgba(0,0,0,0.1);
            transition: right 0.3s ease;
            z-index: 10000;
            overflow-y: auto;
        }

        .script-hub-sidebar.active {
            right: 0;
        }

        .script-list {
            padding: 12px;
        }

        .script-item {
            margin: 0 0 12px;
            padding: 12px;
            border-radius: 8px;
            background: #fff;
            box-shadow: 0 1px 3px rgba(0,0,0,0.1);
            transition: all 0.2s ease;
        }

        .script-item:hover {
            box-shadow: 0 2px 8px rgba(0,0,0,0.15);
            transform: translateY(-1px);
        }

        .script-item h3 {
            margin: 0 0 6px 0;
            font-size: 16px;
        }

        .script-item h3 a {
            color: #1a73e8;
            text-decoration: none;
        }

        .script-item h3 a:hover {
            text-decoration: underline;
        }

        .script-description {
            color: #666;
            font-size: 0.9em;
            line-height: 1.4;
            margin: 6px 0;
        }

        .script-meta {
            display: flex;
            justify-content: flex-start;
            align-items: center;
            padding: 6px 0 0;
            color: #666;
            font-size: 0.9em;
            border-top: 1px solid #eee;
            margin-top: 6px;
            gap: 16px;
            white-space: nowrap;
            overflow: hidden;
        }

        .script-meta span {
            flex-shrink: 0;
            display: flex;
            align-items: center;
            gap: 4px;
            background: #f5f7fa;
            padding: 2px 8px;
            border-radius: 4px;
            font-size: 0.9em;
        }

        .script-meta .author {
            flex-shrink: 1;
            overflow: hidden;
            text-overflow: ellipsis;
            min-width: 0;
        }

        .script-meta .author a {
            color: inherit;
            text-decoration: none;
            overflow: hidden;
            text-overflow: ellipsis;
            display: block;
        }

        .script-meta .author a:hover {
            text-decoration: underline;
            color: #1a73e8;
        }

        .sidebar-header {
            display: flex;
            justify-content: space-between;
            align-items: center;
            padding: 12px 15px;
            border-bottom: 1px solid #eee;
            background: #f8f9fa;
        }

        .sidebar-header-tools {
            display: flex;
            align-items: center;
            gap: 12px;
            font-size: 14px;
            color: #666;
        }

        .sidebar-header-tools a {
            color: #666;
            text-decoration: none;
            display: flex;
            align-items: center;
            gap: 4px;
            padding: 4px 8px;
            border-radius: 4px;
            transition: all 0.2s ease;
            white-space: nowrap;
        }

        .sidebar-header-tools a:hover {
            background: #f5f7fa;
            color: #1a73e8;
        }

        .close-button {
            cursor: pointer;
            padding: 4px 8px;
            color: #666;
            font-size: 16px;
            border-radius: 4px;
            transition: all 0.2s ease;
        }

        .close-button:hover {
            background: #f5f7fa;
            color: #1a73e8;
        }

        .loading, .error, .no-scripts {
            padding: 20px;
            text-align: center;
            color: #666;
        }

        .error {
            color: #f44336;
        }
    `);

    function extractTLD(domain) {
        domain = domain.replace(/^(https?:\/\/)?(www\.)?/, '').split('/')[0];
        const parts = domain.split('.');
        if (parts.length >= 2) {
            return parts.slice(-2).join('.').toLowerCase();
        }
        return domain.toLowerCase();
    }

    function formatDate(dateString) {
        const date = new Date(dateString);
        const now = new Date();
        const month = (date.getMonth() + 1).toString().padStart(2, '0');
        const day = date.getDate().toString().padStart(2, '0');
        
        if (date.getFullYear() === now.getFullYear()) {
            return `${month}月${day}日`;
        } else {
            return `${date.getFullYear().toString().slice(-2)}年${month}月${day}日`;
        }
    }

    function createUI() {
        const button = document.createElement('div');
        button.className = 'script-hub-button';
        
        const text = document.createElement('span');
        text.textContent = '0';
        
        const close = document.createElement('span');
        close.className = 'close';
        close.textContent = '×';
        close.onclick = (e) => {
            e.stopPropagation();
            const currentDomain = window.location.hostname;
            if (typeof GM_getValue !== 'undefined' && typeof GM_setValue !== 'undefined') {
                const excludedDomains = GM_getValue('excludedDomains', []);
                if (!excludedDomains.includes(currentDomain)) {
                    excludedDomains.push(currentDomain);
                    GM_setValue('excludedDomains', excludedDomains);
                }
            }
            button.remove();
        };
        
        button.appendChild(text);
        button.appendChild(close);
        
        let isDragging = false;
        let startX = 0;
        let startY = 0;
        let startLeft = 0;
        let startTop = 0;

        function handleMouseDown(e) {
            if (e.target === close) return;
            
            isDragging = true;
            startX = e.clientX;
            startY = e.clientY;
            
            const rect = button.getBoundingClientRect();
            startLeft = rect.left;
            startTop = rect.top;
            
            button.style.transition = 'none';
            button.style.cursor = 'grabbing';
        }

        function handleMouseMove(e) {
            if (!isDragging) return;
            
            const deltaX = e.clientX - startX;
            const deltaY = e.clientY - startY;
            
            const newLeft = startLeft + deltaX;
            const newTop = startTop + deltaY;
            
            const buttonWidth = button.offsetWidth;
            const buttonHeight = button.offsetHeight;
            const viewportWidth = window.innerWidth;
            const viewportHeight = window.innerHeight;
            
            const finalLeft = Math.min(Math.max(0, newLeft), viewportWidth - buttonWidth);
            const finalTop = Math.min(Math.max(0, newTop), viewportHeight - buttonHeight);
            
            button.style.left = finalLeft + 'px';
            button.style.top = finalTop + 'px';
            button.style.right = 'auto';
            button.style.bottom = 'auto';
        }

        function handleMouseUp() {
            if (!isDragging) return;
            
            isDragging = false;
            button.style.transition = '';
            button.style.cursor = 'move';
        }

        button.addEventListener('mousedown', handleMouseDown);
        document.addEventListener('mousemove', handleMouseMove);
        document.addEventListener('mouseup', handleMouseUp);
        
        document.body.appendChild(button);

        const sidebar = document.createElement('div');
        sidebar.className = 'script-hub-sidebar';
        document.body.appendChild(sidebar);

        document.addEventListener('click', (e) => {
            if (!sidebar.contains(e.target) && !button.contains(e.target) && sidebar.classList.contains('show')) {
                sidebar.classList.remove('show');
                button.classList.remove('active');
            }
        });

        sidebar.addEventListener('click', (e) => {
            e.stopPropagation();
        });

        button.addEventListener('click', (e) => {
            if (e.target === close) return;
            sidebar.classList.toggle('show');
            button.classList.toggle('active');
            e.stopPropagation();
        });

        sidebar.innerHTML = `
            <div class="sidebar-header">
                <div>
                    <div class="sidebar-header-tools">
                        <a href="https://chromewebstore.google.com/detail/jdopbpkjbknppilnpjmceinnpkaigaem" target="_blank">
                           ScriptHub插件
                        </a>
                        <a href="https://likofree.pages.dev/projects/" target="_blank">
                            <svg viewBox="0 0 24 24" width="14" height="14" fill="currentColor">
                                <path d="M4 8h4V4H4v4zm6 12h4v-4h-4v4zm-6 0h4v-4H4v4zm0-6h4v-4H4v4zm6 0h4v-4h-4v4zm6-10v4h4V4h-4zm-6 4h4V4h-4v4zm6 6h4v-4h-4v4zm0 6h4v-4h-4v4z"/>
                            </svg>
                            更多工具
                        </a>
                        <a href="https://x.com/liko2049" target="_blank">
                            <svg viewBox="0 0 24 24" width="14" height="14" fill="currentColor">
                                <path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"/>
                            </svg>
                            作者
                        </a>
                    </div>
                </div>
                <div class="close-button">✕</div>
            </div>
            <div class="script-list"></div>
        `;
        document.body.appendChild(sidebar);

        button.addEventListener('click', async () => {
            sidebar.classList.add('active');
            const scriptList = sidebar.querySelector('.script-list');
            
            if (!scriptList.children.length) {
                const rawDomain = document.location.hostname;
                const domain = extractTLD(rawDomain);
                await loadScriptDetails(domain, scriptList);
            }
        });

        sidebar.querySelector('.close-button').addEventListener('click', () => {
            sidebar.classList.remove('active');
        });

        return { button, sidebar };
    }

    async function loadScriptDetails(domain, container, retryCount = 0) {
        container.innerHTML = '<div class="loading">Loading scripts...</div>';

        try {
            const encodedDomain = encodeURIComponent(domain);
            const apiUrl = `http://greasyfork.icu/scripts/by-site/${domain}?filter_locale=0&page=1`;
            const response = await fetch(apiUrl);
            const html = await response.text();
            const parser = new DOMParser();
            const doc = parser.parseFromString(html, "text/html");
            const scripts = doc.querySelector("#browse-script-list")?.querySelectorAll('[data-script-id]');
            let scriptsInfo = [];
            
            if (!scripts) {
                scriptsInfo = errorMessage;
            } else {
                scripts.forEach(script => {
                    scriptsInfo.push({
                        id: script.getAttribute('data-script-id'),
                        name: script.getAttribute('data-script-name'),
                        author: script.querySelector("dd.script-list-author")?.textContent || '',
                        description: script.querySelector(".script-description")?.textContent || '',
                        version: script.getAttribute('data-script-version'),
                        url: 'http://greasyfork.icu/scripts/' + script.getAttribute('data-script-id'),
                        createDate: script.getAttribute('data-script-created-date'),
                        updateDate: script.getAttribute('data-script-updated-date'),
                        installs: script.getAttribute('data-script-total-installs'),
                        dailyInstalls: script.getAttribute('data-script-daily-installs'),
                        ratingScore: script.getAttribute('data-script-rating-score')
                    });
                });
            }

            container.innerHTML = '';
            
            if (!scriptsInfo.length) {
                container.innerHTML = '<div class="no-scripts">No scripts found</div>';
                return;
            }

            scriptsInfo.forEach(script => {
                const scriptElement = document.createElement('div');
                scriptElement.className = 'script-item';
                
                scriptElement.innerHTML = `
                    <h3><a href="${script.url}" target="_blank">${script.name}</a></h3>
                    <div class="script-description">${script.description}</div>
                    <div class="script-meta">
                        <span title="总安装量">📥 ${script.installs || 0}</span>
                        <span title="日安装量">📈 ${script.dailyInstalls || 0}</span>
                        <span title="更新时间">🕐 ${formatDate(script.updateDate)}</span>
                        <span class="author" title="${script.author || 'Unknown'}">
                            <a href="${script.url}" target="_blank">
                                👨‍💻 ${script.author || 'Unknown'}
                            </a>
                        </span>
                    </div>
                `;
                container.appendChild(scriptElement);
            });
        } catch (error) {
            if (retryCount < 3) {
                setTimeout(() => {
                    loadScriptDetails(domain, container, retryCount + 1);
                }, 1000 * (retryCount + 1));
            } else {
                container.innerHTML = `
                    <div class="error">
                        Failed to load scripts. Please try again later.<br>
                        <small>Error: ${error.message}</small>
                    </div>
                `;
            }
        }
    }

    async function init() {
        const { button, sidebar } = createUI();

        const rawDomain = document.location.hostname;
        const domain = extractTLD(rawDomain);
        
        try {
            const siteData = JSON.parse(GM_getResourceText('SITE_DATA'));
            const count = siteData[domain] || 0;

            if (count === 0) {
                button.style.display = 'none';
                return;
            }

            const text = button.querySelector('span:nth-child(1)');
            text.textContent = count.toString();
        } catch (error) {
            button.style.display = 'none';
        }
    }

    init();
})();