// ==UserScript== // @name 脚本查找大师 (三大脚本库合一显数版) // @namespace doveboy_js // @version 6.2 // @description 聚合 GreasyFork、ScriptCat、GitHub Gist 脚本。悬浮球显示三站总和。 // @author 白鸽男孩 // @license Zlib/Libpng License // @match *://*/* // @exclude https://greasyfork.org/* // @exclude https://sleazyfork.org/* // @exclude https://scriptcat.org/* // @exclude https://github.com/* // @exclude https://gist.github.com/* // @grant GM_xmlhttpRequest // @connect greasyfork.org // @connect scriptcat.org // @connect gist.github.com // @run-at document-end // ==/UserScript== (function() { 'use strict'; const host = window.location.hostname; const domain = host.split('.').slice(-2).join('.'); // 状态管理 let currentSource = 'gf'; let gfData = [], scData = [], ghData = []; let gfLoaded = false, scLoaded = false, ghLoaded = false; let ghPage = 1, ghLoading = false, ghHasMore = true, ghTotal = 0; // --- UI 样式 --- const style = document.createElement('style'); style.innerHTML = ` #ag-ball { position: fixed; bottom: 150px; right: 20px; width: 48px; height: 48px; background: #007aff; color: #fff; border-radius: 50%; box-shadow: 0 4px 15px rgba(0,0,0,0.3); z-index: 2147483647; cursor: move; display: flex; align-items: center; justify-content: center; font-family: sans-serif; font-size: 13px; font-weight: bold; border: 2px solid #fff; user-select: none; touch-action: none; } #ag-panel { position: fixed; bottom: 210px; right: 20px; width: 420px; max-height: 550px; background: #fff; border-radius: 14px; box-shadow: 0 10px 40px rgba(0,0,0,0.2); z-index: 2147483646; display: none; flex-direction: column; border: 1px solid #eaeaea; overflow: hidden; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; } #ag-head { padding: 12px 16px; background: #f9f9f9; border-bottom: 1px solid #eee; display: flex; align-items: center; } #ag-switcher { display: flex; gap: 4px; flex: 1; } .switch-btn { padding: 5px 8px; border-radius: 6px; font-size: 11px; cursor: pointer; border: 1px solid #ddd; background: #fff; font-weight: 500; color: #666; white-space: nowrap; } .switch-btn.active[data-type="gf"] { background: #007aff; color: #fff; border-color: #007aff; } .switch-btn.active[data-type="gh"] { background: #24292f; color: #fff; border-color: #24292f; } .switch-btn.active[data-type="sc"] { background: #1677ff; color: #fff; border-color: #1677ff; } #ag-body { overflow-y: auto; flex: 1; background: #fff; scrollbar-width: thin; } .ag-item { padding: 15px; border-bottom: 1px solid #f5f5f5; } .ag-name { text-decoration: none; font-size: 15px; font-weight: bold; display: block; margin-bottom: 5px; line-height: 1.3; } .gf-color { color: #007aff; } .gh-color { color: #0969da; } .sc-color { color: #1677ff; } .ag-desc { font-size: 13px; color: #444; line-height: 1.5; margin-bottom: 8px; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; word-break: break-all; } .ag-meta-row { display: flex; flex-wrap: wrap; align-items: center; row-gap: 4px; column-gap: 8px; font-size: 11px; color: #888; } .ag-dot { color: #eee; } .ag-score { color: #e09015; font-weight: bold; } .ag-installs { color: #28a745; font-weight: 600; } .ag-star { color: #9a6700; font-weight: bold; } .ag-btn { margin-top: 10px; color: white; border: none; padding: 7px 16px; border-radius: 6px; cursor: pointer; font-size: 12px; font-weight: bold; text-decoration: none; display: inline-block; } .gf-btn { background: #34c759; } .gh-btn { background: #2da44e; } .sc-btn { background: #1677ff; } #gh-load-status { padding: 15px; text-align: center; color: #999; font-size: 12px; } `; document.head.appendChild(style); const ball = document.createElement('div'); ball.id = 'ag-ball'; ball.innerHTML = '...'; document.body.appendChild(ball); const panel = document.createElement('div'); panel.id = 'ag-panel'; panel.innerHTML = `
×
`; document.body.appendChild(panel); const listContainer = panel.querySelector('#ag-list'); const statusText = panel.querySelector('#gh-load-status'); const bodyPanel = panel.querySelector('#ag-body'); const switcher = panel.querySelector('#ag-switcher'); // --- 数据抓取 --- function fetchGreasyFork() { return new Promise((resolve) => { GM_xmlhttpRequest({ method: 'GET', url: `https://greasyfork.org/zh-CN/scripts?q=${domain}&sort=updated&_t=${Date.now()}`, timeout: 3500, onload: (r) => { const doc = new DOMParser().parseFromString(r.responseText, "text/html"); const nodes = doc.querySelectorAll('li[data-script-id]'); gfData = Array.from(nodes).map(node => ({ name: node.querySelector('.script-link')?.innerText.trim(), url: 'https://greasyfork.org' + node.querySelector('.script-link')?.getAttribute('href'), desc: node.querySelector('.script-description')?.innerText.split('Ver.')[0].trim() || "暂无描述", updated: node.getAttribute('data-script-updated-date'), score: node.getAttribute('data-script-rating-score'), daily: parseInt(node.getAttribute('data-script-daily-installs') || "0").toLocaleString(), total: parseInt(node.getAttribute('data-script-total-installs') || "0").toLocaleString(), installUrl: node.querySelector('.install-link')?.getAttribute('href') || `https://greasyfork.org/scripts/${node.getAttribute('data-script-id')}/code/script.user.js` })).filter(i => i.name); gfLoaded = true; updateUI(); resolve(true); }, onerror: () => resolve(false), ontimeout: () => resolve(false) }); }); } function fetchScriptCat() { return new Promise((resolve) => { GM_xmlhttpRequest({ method: 'GET', url: `https://scriptcat.org/zh-CN/search?domain=${domain}&sort=createtime`, onload: (r) => { const doc = new DOMParser().parseFromString(r.responseText, "text/html"); const cards = doc.querySelectorAll('.ant-card-body'); scData = Array.from(cards).map(card => { const titleLink = card.querySelector('a[href*="/script-show-page/"]'); if (!titleLink) return null; const scriptId = titleLink.getAttribute('href').match(/\d+/)[0]; const scriptName = (titleLink.getAttribute('title') || titleLink.innerText).trim(); return { name: scriptName, url: 'https://scriptcat.org' + titleLink.getAttribute('href'), installUrl: `https://scriptcat.org/scripts/code/${scriptId}/${encodeURIComponent(scriptName)}.user.js`, desc: card.querySelector('.line-clamp-2')?.getAttribute('title') || "暂无描述", date: card.querySelector('.anticon-calendar')?.closest('.flex')?.innerText || "未知", downloads: card.querySelector('.anticon-download')?.closest('.flex')?.innerText.trim() || "0" }; }).filter(i => i); scLoaded = true; updateUI(); resolve(true); } }); }); } function fetchGist(page = 1) { if (ghLoading || !ghHasMore) return; ghLoading = true; if (currentSource === 'gh') statusText.style.display = 'block'; GM_xmlhttpRequest({ method: 'GET', url: `https://gist.github.com/search?o=desc&p=${page}&q=%22%3D%3DUserScript%3D%3D%22+${domain}&s=updated`, onload: (res) => { const doc = new DOMParser().parseFromString(res.responseText, 'text/html'); // --- 精准提取 GitHub 脚本总数 --- const h3 = Array.from(doc.querySelectorAll('h3')).find(el => el.innerText.includes('gist results')); if (h3) { const match = h3.innerText.match(/(\d[\d,]*)/); if (match) { // 将 "1,024" 转换为 1024 数字 ghTotal = parseInt(match[1].replace(/,/g, '')) || 0; } } const snippets = doc.querySelectorAll('.gist-snippet'); if (snippets.length === 0) { ghHasMore = false; } else { snippets.forEach(snippet => { const fileStrong = snippet.querySelector('strong.css-truncate-target'); const fileName = fileStrong ? fileStrong.innerText.trim() : 'script.user.js'; const gistPath = fileStrong ? fileStrong.closest('a').getAttribute('href') : ''; let description = '暂无描述', installUrl = ''; snippet.querySelectorAll('.blob-code-inner').forEach(line => { const text = line.innerText; if (text.includes('@description')) description = text.split('@description')[1].trim(); if (text.includes('@downloadURL') || text.includes('@updateURL')) { const found = text.split(/@downloadURL|@updateURL/)[1].trim(); if (found.startsWith('http')) installUrl = found; } }); if (!installUrl && gistPath) installUrl = `https://gist.github.com${gistPath}/raw/${encodeURIComponent(fileName)}`; ghData.push({ name: fileName, url: `https://gist.github.com${gistPath}`, installUrl, desc: description, stars: snippet.querySelector('a[href$="/stargazers"]')?.innerText.trim() || '0', updated: snippet.querySelector('relative-time')?.innerText || '未知' }); }); ghHasMore = !!doc.querySelector('.next_page'); ghPage++; } ghLoaded = true; ghLoading = false; if (currentSource === 'gh') render(); updateUI(); } }); } // --- 逻辑与渲染 --- function updateUI() { // 计算三个平台的脚本总和 const totalScripts = gfData.length + scData.length + ghTotal; ball.innerHTML = totalScripts || (gfLoaded || scLoaded || ghLoaded ? 0 : '...'); // 动态构建按钮排序 (如果 GF 加载失败,把 GH/SC 往前排) const order = gfLoaded ? ['gf', 'gh', 'sc'] : ['gh', 'gf', 'sc']; const labels = { gf: `GreasyFork (${gfLoaded ? gfData.length : '...'})`, gh: `GitHub (${ghTotal})`, sc: `ScriptCat (${scLoaded ? scData.length : '...'})` }; switcher.innerHTML = order.map(type => `
${labels[type]}
` ).join(''); // 重新绑定事件 switcher.querySelectorAll('.switch-btn').forEach(btn => { btn.onclick = () => { currentSource = btn.getAttribute('data-type'); render(); }; }); // 悬浮球颜色跟随当前面板选择 const colors = { gf: '#007aff', gh: '#24292f', sc: '#1677ff' }; ball.style.background = colors[currentSource]; } function render() { updateUI(); const data = currentSource === 'gf' ? gfData : (currentSource === 'sc' ? scData : ghData); statusText.style.display = (currentSource === 'gh' && (ghHasMore || ghLoading)) ? 'block' : 'none'; statusText.innerText = ghLoading ? "正在加载..." : "向下滚动加载更多"; if (data.length === 0 && !ghLoading) { listContainer.innerHTML = `
该平台暂未发现脚本
`; return; } listContainer.innerHTML = data.map(s => { const theme = currentSource; return `
${esc(s.name)} ${esc(s.desc)}
${theme === 'gf' ? `🔄 ${s.updated}|⭐ ${s.score}|📥 今日: ${s.daily}` : ''} ${theme === 'sc' ? `📅 ${s.date}|📥 下载: ${s.downloads}` : ''} ${theme === 'gh' ? `★ ${s.stars}|🕒 ${s.updated}` : ''}
`; }).join(''); } function esc(s) { return String(s || '').replace(/[&<>"']/g, m => ({'&':'&','<':'<','>':'>','"':'"',"'":"'"}[m])); } // --- 交互 --- let isDragging = false, sx, sy, ix, iy; ball.onmousedown = (e) => { isDragging = false; sx = e.clientX; sy = e.clientY; ix = ball.offsetLeft; iy = ball.offsetTop; document.onmousemove = (ev) => { if (Math.abs(ev.clientX - sx) > 5 || Math.abs(ev.clientY - sy) > 5) { isDragging = true; ball.style.left = (ix + ev.clientX - sx) + 'px'; ball.style.top = (iy + ev.clientY - sy) + 'px'; ball.style.bottom = 'auto'; ball.style.right = 'auto'; } }; document.onmouseup = () => document.onmousemove = null; }; ball.onclick = () => { if (!isDragging) { const show = panel.style.display !== 'flex'; panel.style.display = show ? 'flex' : 'none'; if (show) { const rect = ball.getBoundingClientRect(); panel.style.left = Math.min(window.innerWidth - 440, Math.max(20, rect.left - 340)) + 'px'; panel.style.top = Math.max(20, rect.top - 560) + 'px'; render(); } } }; bodyPanel.onscroll = () => { if (currentSource === 'gh' && bodyPanel.scrollTop + bodyPanel.clientHeight >= bodyPanel.scrollHeight - 50) { fetchGist(ghPage); } }; document.getElementById('ag-close').onclick = () => panel.style.display = 'none'; // --- 初始化 --- async function init() { const gfOk = await fetchGreasyFork(); currentSource = gfOk ? 'gf' : 'gh'; // 优先 GF,否则 GH fetchScriptCat(); fetchGist(1); updateUI(); } init(); })();