// ==UserScript==
// @name 脚本查找大师 (三大脚本库合一显数版)
// @namespace doveboy_js
// @version 6.6
// @description 聚合 GreasyFork、ScriptCat、GitHub Gist。支持自动链路检测:翻墙绿球默认GF,不翻墙橙球默认SC,无脚本黑球。
// @author 白鸽男孩
// @icon https://scriptcat.org/favicon.ico?favicon.12.kxvew6xulu.ico
// @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;
let isWallAccessible = false; // 是否能科学上网
// --- UI 样式 ---
const style = document.createElement('style');
style.innerHTML = `
#ag-ball {
position: fixed; bottom: 150px; right: 20px;
width: 48px; height: 48px;
background: #666; color: #fff; /* 默认灰色 */
border-radius: 50%; box-shadow: 0 4px 15px rgba(0,0,0,0.3);
z-index: 2147483647; cursor: pointer;
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;
transition: transform 0.1s, box-shadow 0.1s, background-color 0.3s;
}
#ag-ball:active { transform: scale(0.9); }
#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; overflow-x: auto; scrollbar-width: none; }
#ag-switcher::-webkit-scrollbar { display: none; }
.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-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') || "未知",
created: node.getAttribute('data-script-created-date') || "未知",
score: node.getAttribute('data-script-rating-score') || "0.0",
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; 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; resolve(true);
}
});
});
}
function fetchGist(page = 1) {
if (ghLoading || !ghHasMore) return;
ghLoading = true;
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');
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) 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 (!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; updateUI();
}
});
}
// --- 核心:界面更新与变色逻辑 ---
function updateUI() {
const total = gfData.length + scData.length + ghTotal;
ball.innerHTML = (gfLoaded || scLoaded || ghLoaded) ? total : '...';
// 颜色判定逻辑
if (total === 0 && (gfLoaded && scLoaded)) {
ball.style.backgroundColor = '#000000'; // 黑球:一个脚本都没有
} else if (isWallAccessible) {
ball.style.backgroundColor = '#28a745'; // 绿球:翻墙正常
} else {
ball.style.backgroundColor = '#ff8c00'; // 橙球:没翻墙(仅SC/本地)
}
// 标签切换按钮顺序
const order = isWallAccessible ? ['gf', 'gh', 'sc'] : ['sc', 'gf', 'gh'];
const labels = { gf: `GreasyFork (${gfData.length})`, gh: `GitHub (${ghTotal})`, sc: `ScriptCat (${scData.length})` };
switcher.innerHTML = order.map(type => `
${labels[type]}
`).join('');
switcher.querySelectorAll('.switch-btn').forEach(btn => {
btn.onclick = (e) => {
e.stopPropagation();
currentSource = btn.getAttribute('data-type');
render();
};
});
}
function render() {
updateUI();
const data = currentSource === 'gf' ? gfData : (currentSource === 'sc' ? scData : ghData);
statusText.style.display = (currentSource === 'gh' && (ghHasMore || ghLoading)) ? 'block' : 'none';
if (data.length === 0) {
listContainer.innerHTML = `该平台暂未发现脚本
`;
return;
}
listContainer.innerHTML = data.map(s => {
const theme = currentSource;
let metaHtml = '';
if (theme === 'gf') {
metaHtml = `🔄 更新: ${s.updated}|📅 发布: ${s.created}|⭐ ${s.score}|📥 今日: ${s.daily}`;
} else if (theme === 'sc') {
metaHtml = `📅 发布于: ${s.date}|📥 下载: ${s.downloads}`;
} else {
metaHtml = `★ ${s.stars}|🕒 更新: ${s.updated}`;
}
return ``;
}).join('');
}
function esc(s) { return String(s || '').replace(/[&<>"']/g, m => ({'&':'&','<':'<','>':'>','"':'"',"'":"'"}[m])); }
// --- 交互逻辑 ---
let isMoving = false, startX, startY, ballX, ballY;
ball.onmousedown = (e) => {
isMoving = false; startX = e.clientX; startY = e.clientY;
ballX = ball.offsetLeft; ballY = ball.offsetTop;
const onMouseMove = (ev) => {
if (Math.sqrt(Math.pow(ev.clientX - startX, 2) + Math.pow(ev.clientY - startY, 2)) > 10) {
isMoving = true; ball.style.left = (ballX + ev.clientX - startX) + 'px';
ball.style.top = (ballY + ev.clientY - startY) + 'px';
ball.style.bottom = 'auto'; ball.style.right = 'auto';
}
};
const onMouseUp = () => {
document.removeEventListener('mousemove', onMouseMove);
document.removeEventListener('mouseup', onMouseUp);
if (!isMoving) {
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();
}
}
};
document.addEventListener('mousemove', onMouseMove);
document.addEventListener('mouseup', onMouseUp);
};
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();
isWallAccessible = gfOk; // 核心判定:GF能通就是绿球,不通就是橙球
if (isWallAccessible) {
currentSource = 'gf';
} else {
currentSource = 'sc';
}
// 并行加载其他库
Promise.all([fetchScriptCat(), fetchGist(1)]).then(() => {
updateUI();
});
}
init();
})();