// ==UserScript==
// @name ScriptHub - 🧲 高效查找当前网站可用油猴脚本 🔍
// @name:zh ScriptHub - 🧲 高效查找当前网站可用油猴脚本 🔍
// @name:en ScriptHub - Available Scripts Finder
// @name:ja ScriptHub - 🧲 現在のサイトで利用可能なユーザースクリプトを検索 🔍
// @name:ru ScriptHub - 🧲 Поиск доступных скриптов для текущего сайта 🔍
// @name:ko ScriptHub - 🧲 현재 웹사이트에서 사용 가능한 유저스크립트 찾기 🔍
// @namespace http://tampermonkey.net/
// @version 1.4
// @description 在页面右下角显示当前网站可用的油猴脚本, Shows available userscripts for the current website from Greasy Fork镜像
// @description:zh 在页面右下角显示当前网站可用的油猴脚本数量,点击查看详情
// @description:en Shows available userscripts for the current website from Greasy Fork镜像
// @description:ja 現在のウェブサイトで利用可能なユーザースクリプトの数を右下に表示し、クリックで詳細を確認できます
// @description:ru Показывает количество доступных пользовательских скриптов для текущего сайта в правом нижнем углу
// @description:ko 현재 웹사이트에서 사용 가능한 유저스크립트 수를 우측 하단에 표시하고 클릭하여 자세히 보기
// @author Musk
// @keywords userscript, tampermonkey, greasyfork, script finder, utilities, productivity
// @keywords:zh 用户脚本, 油猴脚本, 脚本查找器, 实用工具, 效率工具, 油猴
// @keywords:en userscript, tampermonkey, greasyfork, script finder, utilities, productivity
// @keywords:ja ユーザースクリプト, タンパーモンキー, スクリプト検索, ユーティリティ, 生産性向上
// @keywords:ru пользовательский скрипт, тампермонки, поиск скриптов, утилиты, продуктивность
// @keywords:ko 유저스크립트, 템퍼멍키, 스크립트 파인더, 유틸리티, 생산성
// @match *://*/*
// @grant GM_addStyle
// @grant GM_xmlhttpRequest
// @grant GM_getResourceText
// @resource SITE_DATA https://gf.qytechs.cn/scripts/by-site.json
// @connect gf.qytechs.cn
// @connect www.gf.qytechs.cn
// @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 = `https://gf.qytechs.cn/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: 'https://gf.qytechs.cn/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();
})();