// ==UserScript== // @name UserScript Finder // @namespace http://tampermonkey.net/ // @version 1.0.0 // @description Finds GreasyFork/SleazyFork/GitHub scripts for the current domain // @author SysAdminDoc // @match *://*/* // @grant GM_xmlhttpRequest // @grant GM_openInTab // @grant GM_addStyle // @grant GM_registerMenuCommand // @grant GM_unregisterMenuCommand // @grant GM_getValue // @grant GM_setValue // @grant GM_deleteValue // @icon https://raw.githubusercontent.com/SysAdminDoc/UserScript-Finder/main/img/icon.png // @connect greasyfork.org // @connect sleazyfork.org // @connect api.github.com // @connect raw.githubusercontent.com // @license WTFPL // @run-at document-idle // @downloadURL https://raw.githubusercontent.com/SysAdminDoc/UserScript-Finder/main/UserScript-Finder.user.js // @updateURL https://raw.githubusercontent.com/SysAdminDoc/UserScript-Finder/main/UserScript-Finder.user.js // @homepageURL https://github.com/SysAdminDoc/UserScript-Finder // @supportURL https://github.com/SysAdminDoc/UserScript-Finder/issues // ==/UserScript== (function() { "use strict"; try { if (window.self !== window.top) return; } catch(e) { return; } // ── TrustedHTML policy ────────────────────────────────────────────── const _ttPolicy = (typeof trustedTypes !== 'undefined' && trustedTypes.createPolicy) ? trustedTypes.createPolicy('gf-script-finder', { createHTML: s => s }) : { createHTML: s => s }; function _safeHTML(el, html) { el.innerHTML = _ttPolicy.createHTML(html); } // ── Default Settings ──────────────────────────────────────────────── const DEFAULT_SETTINGS = { cacheDuration: 5 * 60 * 1000, defaultSort: "daily", denseMode: false, lastService: "greasyfork" }; // ── Catppuccin Mocha + OLED palette ───────────────────────────────── const THEME = { base: '#0a0a0f', mantle: '#0f0f17', crust: '#06060a', surface0: '#14141f', surface1: '#1a1a2a', surface2: '#232336', overlay0: '#2e2e44', overlay1: '#3a3a55', text: '#cdd6f4', subtext1: '#bac2de', subtext0: '#a6adc8', overlay: '#7f849c', green: '#a6e3a1', greenDim: '#40b65e', teal: '#94e2d5', purple: '#cba6f7', purpleDim:'#a855c7', mauve: '#b4befe', red: '#f38ba8', peach: '#fab387', yellow: '#f9e2af', blue: '#89b4fa', flamingo: '#f2cdcd', rosewater:'#f5e0dc', glass: 'rgba(14, 14, 22, 0.82)', glassBorder: 'rgba(255, 255, 255, 0.06)', glassHover: 'rgba(255, 255, 255, 0.03)', glow: 'rgba(166, 227, 161, 0.15)', glowPurple: 'rgba(203, 166, 247, 0.15)', github: '#f0883e', githubDim: '#d2691e', glowGithub: 'rgba(240, 136, 62, 0.15)', shadow: 'rgba(0, 0, 0, 0.5)' }; // ── Icons (Phosphor) ──────────────────────────────────────────────── const ICONS = { moon: '', search: '', scales: '', user: '', gitBranch: '', download: '', chartBar: '', star: '', flame: '', clockwise: '', calendarPlus: '', install: '', gear: '', x: '', eyeSlash: '', arrowsHorizontal: '', rows: '', undo: '', githubLogo: '', gitFork: '' }; function getIcon(name) { return ICONS[name] || ''; } // ── Utility ───────────────────────────────────────────────────────── function escapeHtml(text) { const div = document.createElement("div"); div.textContent = text || ""; return div.innerHTML; } function relativeTime(iso) { if (!iso) return null; const d = new Date(iso); if (isNaN(d.getTime())) return null; const now = Date.now(); const diff = now - d.getTime(); const mins = Math.floor(diff / 60000); if (mins < 1) return 'just now'; if (mins < 60) return `${mins}m ago`; const hrs = Math.floor(mins / 60); if (hrs < 24) return `${hrs}h ago`; const days = Math.floor(hrs / 24); if (days < 30) return `${days}d ago`; const months = Math.floor(days / 30); if (months < 12) return `${months}mo ago`; return `${Math.floor(months / 12)}y ago`; } function formatNumber(num) { const n = Number(num); if (!Number.isFinite(n)) return null; if (n >= 1000000) return (n / 1000000).toFixed(1).replace('.0', '') + 'M'; if (n >= 1000) return (n / 1000).toFixed(1).replace('.0', '') + 'k'; return n.toString(); } // ── Settings Service ──────────────────────────────────────────────── class SettingsService { constructor() { this.settings = this.loadSettings(); } loadSettings() { return { ...DEFAULT_SETTINGS, ...GM_getValue("sf_settings_v4", {}) }; } saveSettings() { GM_setValue("sf_settings_v4", this.settings); } get(key) { return this.settings[key]; } set(key, value) { this.settings[key] = value; this.saveSettings(); } } // ── Host Service ──────────────────────────────────────────────────── class HostService { static getCurrentHost() { return window.location.hostname.replace(/^(www\.|m\.|mobile\.)/, ""); } static extractRootDomain(host) { const parts = host.split('.'); if (parts.length <= 2) return host; const ccTLDs = ['com','net','org','edu','gov','mil','co','ac']; if (ccTLDs.includes(parts[parts.length - 2])) return parts.slice(-3).join('.'); return parts.slice(-2).join('.'); } } // ── Script Service ────────────────────────────────────────────────── class ScriptService { constructor(baseUrl, serviceName) { this.baseUrl = baseUrl; this.serviceName = serviceName; this.cache = new Map(); } async searchScriptsByHost(host, settings) { let scripts = await this._searchWithDomain(host, settings); if (scripts.length === 0) { const root = HostService.extractRootDomain(host); if (root !== host) scripts = await this._searchWithDomain(root, settings); } return scripts; } async _searchWithDomain(domain, settings) { const cacheKey = `${this.serviceName}_${domain}`; const cacheDuration = settings.get("cacheDuration"); const cached = this.cache.get(cacheKey); if (cached && Date.now() - cached.timestamp < cacheDuration) return cached.data; let scripts = []; try { scripts = await this._fetchBySite(domain); } catch { try { scripts = await this._fetchSearch(domain); } catch { scripts = []; } } const filtered = this._filter(scripts, domain); this.cache.set(cacheKey, { data: filtered, timestamp: Date.now() }); return filtered; } _fetch(url) { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: "GET", url, headers: { Accept: "application/json" }, onload: r => { if (r.status === 200) { try { resolve(JSON.parse(r.responseText)); } catch(e) { reject(e); } } else if (r.status === 404) resolve([]); else reject(new Error(`HTTP ${r.status}`)); }, onerror: reject }); }); } _fetchBySite(domain) { return this._fetch(`${this.baseUrl}/scripts/by-site/${domain}.json`); } _fetchSearch(domain) { return this._fetch(`${this.baseUrl}/scripts.json?q=${encodeURIComponent(domain)}&sort=updated`); } _filter(scripts, domain) { const root = HostService.extractRootDomain(domain); return scripts.filter(s => { if (!s.domains) return true; return s.domains.some(d => d === domain || d === `*.${domain}` || d === root || d === `*.${root}` || domain.includes(d.replace('*.','')) || d.replace('*.','').includes(domain) ); }).slice(0, 200); } getDirectSearchUrl(domain) { return `${this.baseUrl}/scripts/by-site/${domain}`; } } // ── GitHub Script Service ─────────────────────────────────────────── class GitHubScriptService { constructor() { this.serviceName = "github"; this.cache = new Map(); } async searchScriptsByHost(host, settings) { const cacheKey = `github_${host}`; const cacheDuration = settings.get("cacheDuration"); const cached = this.cache.get(cacheKey); if (cached && Date.now() - cached.timestamp < cacheDuration) return cached.data; let results = []; try { // Search repos mentioning the domain + userscript keywords const queries = [ `${host} userscript`, `${host} tampermonkey`, `${host} greasemonkey` ]; const seen = new Set(); for (const q of queries) { try { const data = await this._fetchAPI( `https://api.github.com/search/repositories?q=${encodeURIComponent(q)}+language:javascript&sort=stars&per_page=20` ); if (data?.items) { for (const repo of data.items) { if (!seen.has(repo.full_name)) { seen.add(repo.full_name); results.push(this._normalize(repo)); } } } } catch { /* rate limit or network — continue */ } } } catch { results = []; } this.cache.set(cacheKey, { data: results, timestamp: Date.now() }); return results; } _fetchAPI(url) { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: "GET", url, headers: { Accept: "application/vnd.github.v3+json", 'User-Agent': 'ScriptFinder/4' }, onload: r => { if (r.status === 200) { try { resolve(JSON.parse(r.responseText)); } catch(e) { reject(e); } } else if (r.status === 403) reject(new Error("GitHub rate limit — try again in a minute")) else reject(new Error(`GitHub API ${r.status}`)); }, onerror: reject }); }); } _normalize(repo) { return { _source: "github", name: repo.name, description: repo.description || "", url: repo.html_url, code_url: null, version: null, license: repo.license?.spdx_id || null, users: [{ name: repo.owner?.login }], daily_installs: null, total_installs: null, good_ratings: repo.stargazers_count || 0, fan_score: null, code_updated_at: repo.updated_at, created_at: repo.created_at, _stars: repo.stargazers_count || 0, _forks: repo.forks_count || 0, _language: repo.language, _topics: repo.topics || [], _owner: repo.owner?.login, _full_name: repo.full_name }; } getDirectSearchUrl(domain) { return `https://github.com/search?q=${encodeURIComponent(domain + ' userscript')}&type=repositories&l=JavaScript`; } } // ── Toast Service ─────────────────────────────────────────────────── class ToastService { constructor(shadowRoot) { this.root = shadowRoot; this.el = null; this.timer = null; this.undoCallback = null; } show(message, undoCallback = null) { this.hide(); this.undoCallback = undoCallback; this.el = document.createElement("div"); this.el.className = "sf-toast"; const msgSpan = document.createElement("span"); msgSpan.textContent = message; this.el.appendChild(msgSpan); if (undoCallback) { const btn = document.createElement("button"); btn.className = "sf-toast-undo"; btn.textContent = "Undo"; btn.addEventListener("click", () => { undoCallback(); this.hide(); }); this.el.appendChild(btn); } this.root.appendChild(this.el); requestAnimationFrame(() => requestAnimationFrame(() => this.el.classList.add("show"))); this.timer = setTimeout(() => this.hide(), undoCallback ? 5000 : 3000); } hide() { if (this.timer) { clearTimeout(this.timer); this.timer = null; } if (this.el) { this.el.classList.remove("show"); const old = this.el; setTimeout(() => old.remove(), 350); this.el = null; } } } // ── CSS ───────────────────────────────────────────────────────────── const CSS = ` /* ── HOST ── */ :host { all: initial !important; display: block !important; position: fixed !important; bottom: 0 !important; right: 0 !important; z-index: 2147483647 !important; font-family: -apple-system,BlinkMacSystemFont,system-ui,sans-serif !important; pointer-events: none !important; width: 0 !important; height: 0 !important; overflow: visible !important; } /* ── ANIMATIONS ── */ @keyframes sfSpin { to { transform: rotate(360deg); } } @keyframes sfSlideUp { from { opacity: 0; transform: translateY(12px) scale(0.98); } to { opacity: 1; transform: translateY(0) scale(1); } } @keyframes sfFadeIn { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } } @keyframes sfShimmer { 0% { background-position: -200% center; } 100% { background-position: 200% center; } } @keyframes sfModalIn { from { opacity: 0; transform: translateY(16px) scale(0.96); } to { opacity: 1; transform: translateY(0) scale(1); } } @keyframes sfModalOut { from { opacity: 1; transform: translateY(0) scale(1); } to { opacity: 0; transform: translateY(10px) scale(0.97); } } /* ── TOAST ── */ .sf-toast { position: fixed; top: 20px; left: 50%; transform: translateX(-50%) translateY(-20px); background: ${THEME.surface1}; color: ${THEME.text}; padding: 12px 20px; border-radius: 12px; font: 600 13px/1.4 -apple-system,BlinkMacSystemFont,system-ui,sans-serif; box-shadow: 0 12px 40px ${THEME.shadow}; border: 1px solid ${THEME.glassBorder}; backdrop-filter: blur(16px); -webkit-backdrop-filter: blur(16px); opacity: 0; transition: all 0.35s cubic-bezier(0.34,1.56,0.64,1); z-index: 2147483647; pointer-events: auto; display: flex; align-items: center; gap: 12px; max-width: 440px; width: max-content; } .sf-toast.show { opacity: 1; transform: translateX(-50%) translateY(0); } .sf-toast-undo { background: ${THEME.green}; color: ${THEME.base}; border: none; padding: 4px 12px; border-radius: 6px; font: 700 12px/1 inherit; cursor: pointer; transition: all 0.15s ease; white-space: nowrap; } .sf-toast-undo:hover { filter: brightness(1.1); transform: scale(1.04); } /* ── MODAL ── */ .sf-modal { position: fixed; bottom: 14px; right: 14px; width: 500px; max-height: min(84vh, 800px); background: ${THEME.base}; border-radius: 16px; border: 1px solid ${THEME.glassBorder}; box-shadow: 0 32px 80px ${THEME.shadow}, 0 0 0 1px rgba(255,255,255,0.02); overflow: hidden; display: flex; flex-direction: column; opacity: 0; pointer-events: none; transform: translateY(10px) scale(0.97); transition: all 0.3s cubic-bezier(0.34,1.56,0.64,1); } .sf-modal.visible { opacity: 1; pointer-events: auto; transform: translateY(0) scale(1); } /* Header */ .sf-modal-header { padding: 16px 20px; position: relative; background: linear-gradient(180deg, ${THEME.surface0} 0%, ${THEME.base} 100%); border-bottom: 1px solid ${THEME.glassBorder}; } .sf-modal-header::before { content: ''; position: absolute; top: 0; left: 0; right: 0; height: 3px; background: linear-gradient(90deg, ${THEME.green}, ${THEME.teal}); transition: background 0.3s ease; } .sf-modal-header.sleazyfork::before { background: linear-gradient(90deg, ${THEME.purple}, ${THEME.mauve}); } .sf-modal-header.github::before { background: linear-gradient(90deg, ${THEME.github}, ${THEME.peach}); } .sf-header-row { display: flex; align-items: center; gap: 12px; } .sf-header-left { flex: 1; min-width: 0; } .sf-modal-title { font: 700 16px/1.3 -apple-system,BlinkMacSystemFont,system-ui,sans-serif; color: ${THEME.text}; margin: 0 0 4px 0; letter-spacing: -0.3px; background: linear-gradient(90deg, ${THEME.text}, ${THEME.subtext1}, ${THEME.text}); background-size: 200% auto; -webkit-background-clip: text; background-clip: text; -webkit-text-fill-color: transparent; animation: sfShimmer 4s linear infinite; } .sf-modal-subtitle { font: 600 12px/1 inherit; color: ${THEME.subtext0}; margin: 0; display: flex; align-items: center; gap: 6px; } .sf-subtitle-count { display: inline-flex; align-items: center; justify-content: center; min-width: 20px; height: 20px; padding: 0 6px; background: ${THEME.surface2}; border-radius: 6px; font: 700 11px/1 inherit; color: ${THEME.text}; } .sf-header-btn { width: 32px; height: 32px; border-radius: 8px; border: 1px solid ${THEME.glassBorder}; cursor: pointer; background: ${THEME.surface1}; color: ${THEME.subtext0}; display: grid; place-items: center; transition: all 0.2s ease; flex-shrink: 0; } .sf-header-btn:hover { background: ${THEME.surface2}; color: ${THEME.text}; transform: scale(1.06); } .sf-header-btn:active { transform: scale(0.94); } .sf-header-btn svg { width: 14px; height: 14px; } /* Search bar */ .sf-search-wrap { padding: 0 20px 12px; background: transparent; margin-top: -2px; } .sf-search-box { display: flex; align-items: center; gap: 8px; background: ${THEME.surface0}; border: 1px solid ${THEME.glassBorder}; border-radius: 10px; padding: 0 12px; height: 36px; transition: all 0.2s ease; } .sf-search-box:focus-within { border-color: ${THEME.green}33; box-shadow: 0 0 0 3px ${THEME.green}11; } .sf-search-box.sleazyfork:focus-within { border-color: ${THEME.purple}33; box-shadow: 0 0 0 3px ${THEME.purple}11; } .sf-search-box.github:focus-within { border-color: ${THEME.github}33; box-shadow: 0 0 0 3px ${THEME.github}11; } .sf-search-box svg { width: 14px; height: 14px; color: ${THEME.overlay}; flex-shrink: 0; } .sf-search-input { flex: 1; border: none; background: transparent; outline: none; font: 500 13px/1 -apple-system,BlinkMacSystemFont,system-ui,sans-serif; color: ${THEME.text}; padding: 0; } .sf-search-input::placeholder { color: ${THEME.overlay}; } .sf-search-count { font: 600 11px/1 inherit; color: ${THEME.overlay}; white-space: nowrap; } /* Tabs */ .sf-tabs { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 6px; padding: 10px 20px; background: ${THEME.surface0}44; border-bottom: 1px solid ${THEME.glassBorder}; } .sf-tab { padding: 9px 10px; border: 1px solid ${THEME.glassBorder}; border-radius: 8px; cursor: pointer; background: ${THEME.surface0}; font: 600 12px/1 inherit; color: ${THEME.subtext0}; transition: all 0.2s ease; position: relative; text-align: center; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .sf-tab:hover { background: ${THEME.surface1}; color: ${THEME.text}; } .sf-tab.active { background: linear-gradient(135deg, ${THEME.greenDim}, ${THEME.green}33); color: ${THEME.green}; border-color: ${THEME.green}33; box-shadow: 0 0 16px ${THEME.glow}; } .sf-tab.sleazyfork.active { background: linear-gradient(135deg, ${THEME.purpleDim}44, ${THEME.purple}22); color: ${THEME.purple}; border-color: ${THEME.purple}33; box-shadow: 0 0 16px ${THEME.glowPurple}; } .sf-tab.github.active { background: linear-gradient(135deg, ${THEME.githubDim}44, ${THEME.github}22); color: ${THEME.github}; border-color: ${THEME.github}33; box-shadow: 0 0 16px ${THEME.glowGithub}; } /* Sort bar */ .sf-sort-bar { padding: 10px 20px; background: ${THEME.surface0}22; border-bottom: 1px solid ${THEME.glassBorder}; display: flex; align-items: center; gap: 10px; } .sf-sort-label { font: 600 12px/1 inherit; color: ${THEME.subtext0}; flex-shrink: 0; } .sf-sort-select { flex: 1; padding: 7px 10px; border-radius: 8px; border: 1px solid ${THEME.glassBorder}; background: ${THEME.surface0}; color: ${THEME.text}; font: 500 12px/1 inherit; cursor: pointer; outline: none; transition: border-color 0.2s ease; } .sf-sort-select option { background: ${THEME.surface1}; color: ${THEME.text}; } .sf-sort-select:focus { border-color: ${THEME.green}44; } .sf-sort-select.sleazyfork:focus { border-color: ${THEME.purple}44; } .sf-sort-select.github:focus { border-color: ${THEME.github}44; } /* Content */ .sf-content { flex: 1; overflow-y: auto; background: ${THEME.base}; scrollbar-width: thin; scrollbar-color: ${THEME.surface2} transparent; } .sf-content::-webkit-scrollbar { width: 6px; } .sf-content::-webkit-scrollbar-track { background: transparent; } .sf-content::-webkit-scrollbar-thumb { background: ${THEME.surface2}; border-radius: 3px; } .sf-content::-webkit-scrollbar-thumb:hover { background: ${THEME.overlay0}; } /* Script items */ .sf-item { padding: 14px 20px; border-bottom: 1px solid ${THEME.glassBorder}; cursor: pointer; position: relative; background: transparent; transition: all 0.2s ease; animation: sfFadeIn 0.3s ease both; } .sf-item:hover { background: ${THEME.glassHover}; transform: translateY(-1px); } .sf-item:hover::after { content: ''; position: absolute; left: 0; top: 0; bottom: 0; width: 2px; background: linear-gradient(180deg, ${THEME.green}, ${THEME.teal}); } .sf-item.sleazyfork:hover::after { background: linear-gradient(180deg, ${THEME.purple}, ${THEME.mauve}); } .sf-item.github:hover::after { background: linear-gradient(180deg, ${THEME.github}, ${THEME.peach}); } .sf-item:last-child { border-bottom: none; } /* Dense mode */ :host(.dense) .sf-item { padding: 10px 20px; } :host(.dense) .sf-script-title { font-size: 13px; } :host(.dense) .sf-script-desc { display: none; } :host(.dense) .sf-script-meta { gap: 4px; } :host(.dense) .sf-badge { padding: 3px 7px; font-size: 10px; } .sf-script-top { display: flex; align-items: flex-start; justify-content: space-between; gap: 10px; margin-bottom: 6px; } .sf-script-info { flex: 1; min-width: 0; } .sf-script-title { display: block; text-decoration: none; color: ${THEME.text}; font: 700 14px/1.4 inherit; transition: color 0.15s ease; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .sf-script-title:hover { color: ${THEME.green}; } .sf-item.sleazyfork .sf-script-title:hover { color: ${THEME.purple}; } .sf-item.github .sf-script-title:hover { color: ${THEME.github}; } .sf-script-sub { display: flex; align-items: center; flex-wrap: wrap; gap: 6px; font: 500 11px/1.2 inherit; color: ${THEME.overlay}; margin-top: 3px; } .sf-script-sub svg { width: 12px; height: 12px; margin-right: 2px; vertical-align: -1px; } .sf-dot { opacity: 0.3; font-size: 8px; } .sf-install-btn { flex-shrink: 0; display: flex; align-items: center; gap: 4px; padding: 6px 12px; border-radius: 8px; border: none; background: ${THEME.green}22; color: ${THEME.green}; font: 700 11px/1 inherit; cursor: pointer; transition: all 0.2s ease; white-space: nowrap; } .sf-install-btn:hover { background: ${THEME.green}44; transform: scale(1.04); } .sf-install-btn:active { transform: scale(0.96); } .sf-install-btn svg { width: 14px; height: 14px; } .sf-item.sleazyfork .sf-install-btn { background: ${THEME.purple}22; color: ${THEME.purple}; } .sf-item.sleazyfork .sf-install-btn:hover { background: ${THEME.purple}44; } .sf-item.github .sf-install-btn { background: ${THEME.github}22; color: ${THEME.github}; } .sf-item.github .sf-install-btn:hover { background: ${THEME.github}44; } .sf-script-desc { color: ${THEME.subtext0}; font: 400 12px/1.5 inherit; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; margin-bottom: 8px; } .sf-script-meta { display: flex; flex-wrap: wrap; gap: 5px; } .sf-badge { display: inline-flex; align-items: center; gap: 4px; padding: 4px 8px; border-radius: 6px; font: 700 10px/1 inherit; background: ${THEME.surface1}; color: ${THEME.subtext1}; border: 1px solid ${THEME.glassBorder}; transition: all 0.15s ease; } .sf-badge svg { width: 12px; height: 12px; } .sf-badge:hover { background: ${THEME.surface2}; border-color: ${THEME.overlay0}; } .sf-badge.score-high { background: ${THEME.green}18; color: ${THEME.green}; border-color: ${THEME.green}33; } .sf-badge.score-mid { background: ${THEME.yellow}18; color: ${THEME.yellow}; border-color: ${THEME.yellow}33; } .sf-badge.score-low { background: ${THEME.red}18; color: ${THEME.red}; border-color: ${THEME.red}33; } /* Loading / empty / error */ .sf-loading { padding: 50px 20px; text-align: center; display: grid; gap: 14px; place-items: center; } .sf-spinner { width: 36px; height: 36px; border-radius: 50%; border: 3px solid ${THEME.surface2}; border-top-color: ${THEME.green}; animation: sfSpin 0.7s linear infinite; } .sf-spinner.sleazyfork { border-top-color: ${THEME.purple}; } .sf-spinner.github { border-top-color: ${THEME.github}; } .sf-loading-text { font: 500 13px/1 inherit; color: ${THEME.subtext0}; } .sf-empty, .sf-error { padding: 50px 28px; text-align: center; } .sf-empty-title, .sf-error-title { font: 700 15px/1.3 inherit; color: ${THEME.text}; margin-bottom: 8px; } .sf-error-title { color: ${THEME.red}; } .sf-empty-text, .sf-error-text { color: ${THEME.subtext0}; font: 400 13px/1.5 inherit; margin-bottom: 18px; } .sf-action-btn { display: inline-flex; align-items: center; justify-content: center; padding: 10px 18px; border-radius: 10px; background: linear-gradient(135deg, ${THEME.greenDim}, ${THEME.green}88); color: ${THEME.base}; font: 700 13px/1 inherit; border: none; cursor: pointer; transition: all 0.2s ease; } .sf-action-btn:hover { transform: translateY(-1px); filter: brightness(1.1); } .sf-action-btn.sleazyfork { background: linear-gradient(135deg, ${THEME.purpleDim}, ${THEME.purple}88); } .sf-action-btn.github { background: linear-gradient(135deg, ${THEME.githubDim}, ${THEME.github}88); } /* Footer */ .sf-footer { padding: 12px 20px; border-top: 1px solid ${THEME.glassBorder}; background: ${THEME.surface0}44; display: flex; align-items: center; justify-content: space-between; gap: 10px; font: 600 11px/1 inherit; } .sf-footer-text { color: ${THEME.overlay}; } .sf-footer a { color: ${THEME.green}; text-decoration: none; font-weight: 700; } .sf-footer a:hover { text-decoration: underline; } .sf-footer a.sleazyfork { color: ${THEME.purple}; } .sf-footer a.github { color: ${THEME.github}; } /* Settings panel */ .sf-settings { display: none; padding: 16px 20px; border-top: 1px solid ${THEME.glassBorder}; background: ${THEME.surface0}44; } .sf-settings.visible { display: block; } .sf-settings-title { font: 700 13px/1 inherit; color: ${THEME.text}; margin-bottom: 12px; } .sf-setting-row { display: flex; align-items: center; justify-content: space-between; padding: 8px 0; border-bottom: 1px solid ${THEME.glassBorder}; } .sf-setting-row:last-child { border-bottom: none; } .sf-setting-label { font: 500 12px/1.3 inherit; color: ${THEME.subtext1}; } .sf-toggle { position: relative; width: 36px; height: 20px; border-radius: 10px; background: ${THEME.surface2}; cursor: pointer; transition: background 0.2s ease; border: none; padding: 0; } .sf-toggle::after { content: ''; position: absolute; top: 2px; left: 2px; width: 16px; height: 16px; border-radius: 50%; background: ${THEME.subtext0}; transition: all 0.2s ease; } .sf-toggle.on { background: ${THEME.green}55; } .sf-toggle.on::after { left: 18px; background: ${THEME.green}; } .sf-setting-select { padding: 4px 8px; border-radius: 6px; border: 1px solid ${THEME.glassBorder}; background: ${THEME.surface0}; color: ${THEME.text}; font: 500 12px/1 inherit; cursor: pointer; outline: none; } .sf-setting-select option { background: ${THEME.surface1}; } /* Responsive */ @media (max-width: 520px) { .sf-modal { width: calc(100vw - 24px); right: 12px; max-height: min(88vh, 700px); } .sf-modal-header { padding: 14px 16px; } .sf-tabs { padding: 8px 16px; } .sf-sort-bar, .sf-search-wrap { padding-left: 16px; padding-right: 16px; } .sf-item { padding: 12px 16px; } .sf-footer { padding: 10px 16px; flex-wrap: wrap; justify-content: center; } } `; // ── Main Controller ───────────────────────────────────────────────── class ScriptFinder { constructor() { this.settings = new SettingsService(); this.services = { greasyfork: new ScriptService("https://greasyfork.org", "greasyfork"), sleazyfork: new ScriptService("https://sleazyfork.org", "sleazyfork"), github: new GitHubScriptService() }; this.currentService = this.settings.get("lastService") || "greasyfork"; this.currentSort = this.settings.get("defaultSort"); this.currentDomain = HostService.extractRootDomain(HostService.getCurrentHost()); this.isOpen = false; this.isLoading = false; this.allScripts = []; this.searchQuery = ""; this.settingsOpen = false; this.uiBuilt = false; } init() { this._setupMenuCommands(); } _ensureUI() { if (this.uiBuilt) return; this._buildUI(); this._setupEvents(); this.uiBuilt = true; } // ── UI Build ──────────────────────────────────────────────────── _buildUI() { this.host = document.createElement("div"); this.shadow = this.host.attachShadow({ mode: 'open' }); const style = document.createElement('style'); style.textContent = CSS; this.shadow.appendChild(style); this.toast = new ToastService(this.shadow); // Modal this.modal = document.createElement("div"); this.modal.className = "sf-modal"; _safeHTML(this.modal, `

Scripts for this site

0 scripts found

Sort by
Searching...
Settings
Dense mode
Default sort
Cache (minutes)
`); this.shadow.appendChild(this.modal); // Refs this.content = this.modal.querySelector(".sf-content"); this.searchInput = this.modal.querySelector(".sf-search-input"); this.searchCount = this.modal.querySelector(".sf-search-count"); this.searchBox = this.modal.querySelector(".sf-search-box"); this.sortSelect = this.modal.querySelector(".sf-sort-select"); if (this.settings.get("denseMode")) this.host.classList.add("dense"); document.body.appendChild(this.host); } // ── Events ────────────────────────────────────────────────────── _setupEvents() { // Modal buttons this.modal.querySelector(".sf-btn-close").addEventListener("click", () => this._close()); this.modal.querySelector(".sf-btn-settings").addEventListener("click", () => this._toggleSettings()); // Tabs this.modal.querySelectorAll(".sf-tab").forEach(tab => { tab.addEventListener("click", () => { const svc = tab.dataset.service; if (svc !== this.currentService) { this.currentService = svc; this.settings.set("lastService", svc); this._updateTabs(); this._loadScripts(); } }); }); // Sort this.sortSelect.addEventListener("change", e => { this.currentSort = e.target.value; this._displayScripts(); }); // Search filter this.searchInput.addEventListener("input", e => { this.searchQuery = e.target.value.toLowerCase().trim(); this._displayScripts(); }); // Settings toggles this.modal.querySelectorAll(".sf-toggle").forEach(btn => { btn.addEventListener("click", () => { const key = btn.dataset.key; const val = !this.settings.get(key); this.settings.set(key, val); btn.classList.toggle("on", val); if (key === "denseMode") this.host.classList.toggle("dense", val); }); }); // Settings selects this.modal.querySelectorAll(".sf-setting-select").forEach(sel => { sel.addEventListener("change", () => { const key = sel.dataset.key; let val = sel.value; if (key === "cacheDuration") val = parseInt(val); this.settings.set(key, val); if (key === "defaultSort") { this.currentSort = val; this.sortSelect.value = val; this._displayScripts(); } }); }); // Outside click / Escape document.addEventListener("click", e => { if (this.isOpen && !this.host.contains(e.target)) this._close(); }); document.addEventListener("keydown", e => { if (e.key === "Escape" && this.isOpen) this._close(); }); } // ── Menu commands ─────────────────────────────────────────────── _setupMenuCommands() { if (typeof GM_registerMenuCommand !== "function") return; if (window._sfMenuIds) window._sfMenuIds.forEach(id => { try { GM_unregisterMenuCommand(id); } catch {} }); window._sfMenuIds = []; const domain = HostService.extractRootDomain(this.currentDomain); window._sfMenuIds.push(GM_registerMenuCommand(`Find Scripts for ${domain} (GreasyFork)`, () => { this._ensureUI(); this.currentService = "greasyfork"; this._open(); })); window._sfMenuIds.push(GM_registerMenuCommand(`Find Scripts for ${domain} (SleazyFork)`, () => { this._ensureUI(); this.currentService = "sleazyfork"; this._open(); })); window._sfMenuIds.push(GM_registerMenuCommand(`Find Scripts for ${domain} (GitHub)`, () => { this._ensureUI(); this.currentService = "github"; this._open(); })); window._sfMenuIds.push(GM_registerMenuCommand("Reset Script Finder Settings", () => { GM_deleteValue("sf_settings_v4"); location.reload(); })); } // ── Modal control ─────────────────────────────────────────────── _open() { this.isOpen = true; this._updateTabs(); this._updateServiceColors(); this.sortSelect.value = this.currentSort; this.modal.classList.add("visible"); this.searchInput.value = ""; this.searchQuery = ""; this._loadScripts(); } _close() { this.isOpen = false; this.settingsOpen = false; this.modal.querySelector(".sf-settings").classList.remove("visible"); this.modal.classList.remove("visible"); } _toggleSettings() { this.settingsOpen = !this.settingsOpen; this.modal.querySelector(".sf-settings").classList.toggle("visible", this.settingsOpen); } // ── Tab/color updates ─────────────────────────────────────────── _updateTabs() { this.modal.querySelectorAll(".sf-tab").forEach(t => t.classList.toggle("active", t.dataset.service === this.currentService)); this._updateServiceColors(); } _updateServiceColors() { const svc = this.currentService; const svcNames = ["greasyfork", "sleazyfork", "github"]; const header = this.modal.querySelector(".sf-modal-header"); svcNames.forEach(s => header.classList.toggle(s, s === svc)); svcNames.forEach(s => this.sortSelect.classList.toggle(s, s === svc)); svcNames.forEach(s => this.searchBox.classList.toggle(s, s === svc)); const footerLink = this.modal.querySelector(".sf-footer a"); if (footerLink) { svcNames.forEach(s => footerLink.classList.toggle(s, s === svc)); const labels = { greasyfork: "GreasyFork", sleazyfork: "SleazyFork", github: "GitHub" }; const urls = { greasyfork: "https://greasyfork.org", sleazyfork: "https://sleazyfork.org", github: "https://github.com" }; footerLink.textContent = labels[svc]; footerLink.href = urls[svc]; } } _setResultCount(count) { const countEl = this.modal.querySelector(".sf-subtitle-count"); const textEl = this.modal.querySelector(".sf-subtitle-text"); const isGH = this.currentService === "github"; const unit = isGH ? "repo" : "script"; if (countEl) countEl.textContent = count || 0; if (textEl) textEl.textContent = count === 1 ? `${unit} found` : `${unit}s found`; } // ── Data ──────────────────────────────────────────────────────── async _loadScripts() { if (this.isLoading) return; this.isLoading = true; const svc = this.services[this.currentService]; const svcClass = this.currentService === "greasyfork" ? "" : this.currentService; const svcLabels = { greasyfork: "GreasyFork", sleazyfork: "SleazyFork", github: "GitHub" }; const svcLabel = svcLabels[this.currentService]; _safeHTML(this.content, `
Searching ${svcLabel}...
`); try { const host = HostService.getCurrentHost(); this.currentDomain = HostService.extractRootDomain(host); this.allScripts = await svc.searchScriptsByHost(this.currentDomain, this.settings); this._setResultCount(this.allScripts.length); this._displayScripts(); } catch(err) { this._setResultCount(0); _safeHTML(this.content, `
Something went wrong
${escapeHtml(err?.message || 'Unknown error')}
`); this.content.querySelector(".sf-action-btn")?.addEventListener("click", () => this._loadScripts()); } finally { this.isLoading = false; } } _sortScripts(scripts) { const copy = [...scripts]; switch (this.currentSort) { case "daily": return copy.sort((a,b) => (b.daily_installs || b._stars || 0) - (a.daily_installs || a._stars || 0)); case "total": return copy.sort((a,b) => (b.total_installs || b._stars || 0) - (a.total_installs || a._stars || 0)); case "good": return copy.sort((a,b) => (b.good_ratings || 0) - (a.good_ratings || 0)); case "fanscore": return copy.sort((a,b) => (b.fan_score || b._forks || 0) - (a.fan_score || a._forks || 0)); case "updatedate": return copy.sort((a,b) => new Date(b.code_updated_at||0) - new Date(a.code_updated_at||0)); case "createdate": return copy.sort((a,b) => new Date(b.created_at||0) - new Date(a.created_at||0)); default: return copy.sort((a,b) => (b.daily_installs || b._stars || 0) - (a.daily_installs || a._stars || 0)); } } _displayScripts() { let scripts = this.allScripts || []; const svcClass = this.currentService === "greasyfork" ? "" : this.currentService; const svcLabels = { greasyfork: "GreasyFork", sleazyfork: "SleazyFork", github: "GitHub" }; const svcLabel = svcLabels[this.currentService]; const displayHost = HostService.extractRootDomain(this.currentDomain); // Update title this.modal.querySelector(".sf-modal-title").textContent = `Scripts for ${displayHost}`; // Filter by search if (this.searchQuery) { scripts = scripts.filter(s => (s.name || "").toLowerCase().includes(this.searchQuery) || (s.description || "").toLowerCase().includes(this.searchQuery) || (s.users?.[0]?.name || "").toLowerCase().includes(this.searchQuery) || (s._full_name || "").toLowerCase().includes(this.searchQuery) || (s._topics || []).some(t => t.toLowerCase().includes(this.searchQuery)) ); } this.searchCount.textContent = this.searchQuery ? `${scripts.length}/${this.allScripts.length}` : ""; if (!scripts.length) { const directUrl = this.services[this.currentService].getDirectSearchUrl(this.currentDomain); if (this.searchQuery) { _safeHTML(this.content, `
No matches
No scripts match "${escapeHtml(this.searchQuery)}"
`); } else { _safeHTML(this.content, `
No scripts found
Nothing matched ${escapeHtml(displayHost)} on ${svcLabel}.
Search manually
`); } this._setResultCount(this.allScripts.length); return; } const sorted = this._sortScripts(scripts); this._setResultCount(this.allScripts.length); // Build items _safeHTML(this.content, ""); sorted.forEach((script, i) => { const item = this._createScriptItem(script, svcClass, i); this.content.appendChild(item); }); } _createScriptItem(script, svcClass, index) { const item = document.createElement("div"); item.className = `sf-item ${svcClass}`; item.style.animationDelay = `${Math.min(index * 30, 300)}ms`; const isGH = script._source === "github"; const daily = formatNumber(script.daily_installs); const total = formatNumber(script.total_installs); const good = formatNumber(script.good_ratings); const fanScore = script.fan_score != null ? Number(script.fan_score) : null; const fanText = Number.isFinite(fanScore) ? fanScore.toFixed(1) : null; const updated = relativeTime(script.code_updated_at); const created = relativeTime(script.created_at); const author = script.users?.[0]?.name || null; const baseUrls = { sleazyfork: "https://sleazyfork.org", greasyfork: "https://greasyfork.org" }; const scriptUrl = isGH ? script.url : (script.url?.startsWith("http") ? script.url : (baseUrls[svcClass] || baseUrls.greasyfork) + (script.url || "")); const installUrl = script.code_url || null; const fanClass = fanScore >= 8 ? "score-high" : fanScore >= 6 ? "score-mid" : fanScore >= 0 ? "score-low" : ""; const badge = (icon, text, title, cls = "") => { if (!text) return ""; return `${getIcon(icon)} ${escapeHtml(text)}`; }; // GitHub: show stars + forks; GreasyFork/SleazyFork: show installs + ratings let metaHtml; if (isGH) { const stars = formatNumber(script._stars); const forks = formatNumber(script._forks); const lang = script._language; metaHtml = ` ${badge("star", stars, "Stars")} ${badge("gitFork", forks, "Forks")} ${lang ? badge("gitBranch", lang, "Language") : ""} ${badge("clockwise", updated, "Updated")} ${badge("calendarPlus", created, "Created")} `; } else { metaHtml = ` ${badge("download", daily ? `${daily}/day` : null, "Daily installs")} ${badge("chartBar", total, "Total installs")} ${badge("star", good, "Ratings")} ${badge("flame", fanText, "Fan score", fanText ? fanClass : "")} ${badge("clockwise", updated, "Updated")} ${badge("calendarPlus", created, "Created")} `; } // GitHub repos get a "View" button; GreasyFork/SleazyFork get "Install" let actionBtn; if (isGH) { actionBtn = ``; } else if (installUrl) { actionBtn = ``; } else { actionBtn = ""; } _safeHTML(item, `
${escapeHtml(script.name || "Untitled")}
${author ? `${getIcon('user')} ${escapeHtml(author)}` : ""} ${author && script.version ? `` : ""} ${script.version ? `${getIcon('gitBranch')} v${escapeHtml(script.version)}` : ""} ${(author || script.version) && script.license ? `` : ""} ${script.license ? `${getIcon('scales')} ${escapeHtml(script.license)}` : ""}
${actionBtn}
${escapeHtml(script.description || "No description")}
${metaHtml}
`); // Action button handler const actionEl = item.querySelector(".sf-install-btn"); if (actionEl) { actionEl.addEventListener("click", (e) => { e.stopPropagation(); GM_openInTab(actionEl.dataset.url, { active: true }); }); } // Click-to-open script page item.addEventListener("click", (e) => { if (e.target.closest("a") || e.target.closest(".sf-install-btn")) return; GM_openInTab(scriptUrl, { active: true }); }); return item; } } // ── Init ──────────────────────────────────────────────────────────── function boot() { try { new ScriptFinder().init(); } catch(e) { console.error("[Script Finder v4]", e); } } if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", boot); else setTimeout(boot, 50); })();