Finds GreasyFork/SleazyFork scripts for the current domain
// ==UserScript== // @name GreasyFork/SleazyFork Script Finder // @namespace http://tampermonkey.net/ // @version 3.3.1 // @description Finds GreasyFork/SleazyFork scripts for the current domain // @author Pipos_ // @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 data:image/svg+xml;base64,PCFET0NUWVBFIHN2ZyBQVUJMSUMgIi0vL1czQy8vRFREIFNWRyAxLjEvL0VOIiAiaHR0cDovL3d3dy53My5vcmcvR3JhcGhpY3MvU1ZHLzEuMS9EVEQvc3ZnMTEuZHRkIj4KPCEtLSBVcGxvYWRlZCB0bzogU1ZHIFJlcG8sIHd3dy5zdmdyZXBvLmNvbSwgVHJhbnNmb3JtZWQgYnk6IFNWRyBSZXBvIE1peGVyIFRvb2xzIC0tPgo8c3ZnIHdpZHRoPSI4MDBweCIgaGVpZ2h0PSI4MDBweCIgdmlld0JveD0iMCAwIDY0IDY0IiBpZD0iTGF5ZXJfMSIgdmVyc2lvbj0iMS4xIiB4bWw6c3BhY2U9InByZXNlcnZlIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiBmaWxsPSIjMDAwMDAwIj4KDTxnIGlkPSJTVkdSZXBvX2JnQ2FycmllciIgc3Ryb2tlLXdpZHRoPSIwIi8+Cg08ZyBpZD0iU1ZHUmVwb190cmFjZXJDYXJyaWVyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiLz4KDTxnIGlkPSJTVkdSZXBvX2ljb25DYXJyaWVyIj4gPHN0eWxlIHR5cGU9InRleHQvY3NzIj4gLnN0MHtmaWxsOiNCNEU2REQ7fSAuc3Qxe2ZpbGw6IzgwRDRDNDt9IC5zdDJ7ZmlsbDojRDJGMEVBO30gLnN0M3tmaWxsOiNGRkZGRkY7fSAuc3Q0e2ZpbGw6I0ZCRDg3Mjt9IC5zdDV7ZmlsbDojREI3NzY3O30gLnN0NntmaWxsOiNGMzhFN0E7fSAuc3Q3e2ZpbGw6I0Y2QUY2Mjt9IC5zdDh7ZmlsbDojMzJBNDhFO30gLnN0OXtmaWxsOiM1YmIxNWQ7fSAuc3QxMHtmaWxsOiM1YmIxNWQ7fSAuc3QxMXtmaWxsOiNFQUExNTc7fSAuc3QxMntmaWxsOiM1YmIxNWQ7fSAuc3QxM3tmaWxsOiNGOUM0NkE7fSAuc3QxNHtmaWxsOiNDRTZCNjE7fSA8L3N0eWxlPiA8Zz4gPHBhdGggY2xhc3M9InN0MTAiIGQ9Ik0yMy41NiwxNy45N2MyLjIsMCw0LjA2LDEuNDEsNC43NywzLjM3YzAuMTMtMC4zOSwwLjIyLTAuOCwwLjIyLTEuMjNjMC0yLjEzLTEuNzMtMy44Ni0zLjg2LTMuODYgYy0xLjYsMC0yLjk3LDAuOTctMy41NiwyLjM2QzIxLjg1LDE4LjIsMjIuNjgsMTcuOTcsMjMuNTYsMTcuOTd6Ii8+IDxwYXRoIGNsYXNzPSJzdDEwIiBkPSJNNDAuNDQsMTcuOTdjLTIuMiwwLTQuMDYsMS40MS00Ljc3LDMuMzdjLTAuMTMtMC4zOS0wLjIyLTAuOC0wLjIyLTEuMjNjMC0yLjEzLDEuNzMtMy44NiwzLjg2LTMuODYgYzEuNiwwLDIuOTcsMC45NywzLjU2LDIuMzZDNDIuMTUsMTguMiw0MS4zMiwxNy45Nyw0MC40NCwxNy45N3oiLz4gPHBhdGggY2xhc3M9InN0OSIgZD0iTTUzLjcyLDMyYy0wLjItMC4yOS04Ljc0LTExLjM4LTguOTctMTEuNjJjLTAuOS0xLjQ0LTIuNDktMi40MS00LjMxLTIuNDFjLTIuNDcsMC00LjUzLDEuNzYtNC45OSw0LjEgYy0yLjI1LTAuNjQtNC42NC0wLjY0LTYuODksMGMtMC40Ni0yLjM0LTIuNTItNC4xLTQuOTktNC4xYy0xLjgyLDAtMy40MSwwLjk3LTQuMzEsMi40MWMtMC4yMywwLjI1LTguNzgsMTEuMzMtOC45NywxMS42MiBDOC44NiwzMy42Nyw4LDM1LjgzLDgsMzguMTljMCw1LjI4LDQuMjgsOS41Niw5LjU2LDkuNTZjNS4xNCwwLDkuMzEtNC4wNSw5LjU0LTkuMTNjMC4wMy0wLjIxLDAuNTgtNS41OSwwLjk5LTkuNzYgYzIuMzEtMS43Miw1LjUtMS43MSw3LjgxLDBjMC40MSw0LjE2LDAuOTUsOS41NCwwLjk5LDkuNzVjMC4yMyw1LjA4LDQuNDEsOS4xMyw5LjU0LDkuMTNjNS4yOCwwLDkuNTYtNC4yOCw5LjU2LTkuNTYgQzU2LDM1LjgzLDU1LjE0LDMzLjY3LDUzLjcyLDMyeiBNMzIsMjYuMzdjLTAuOTMsMC0xLjY4LTAuNzUtMS42OC0xLjY4czAuNzUtMS42OCwxLjY4LTEuNjhzMS42OCwwLjc1LDEuNjgsMS42OCBTMzIuOTMsMjYuMzcsMzIsMjYuMzd6Ii8+IDxjaXJjbGUgY2xhc3M9InN0MTAiIGN4PSIxNy41NiIgY3k9IjM4LjE5IiByPSI5LjU2Ii8+IDxjaXJjbGUgY2xhc3M9InN0MyIgY3g9IjE3LjU2IiBjeT0iMzguMTkiIHI9IjcuNTYiLz4gPGNpcmNsZSBjbGFzcz0ic3QxMCIgY3g9IjQ2LjQ0IiBjeT0iMzguMTkiIHI9IjkuNTYiLz4gPGNpcmNsZSBjbGFzcz0ic3QzIiBjeD0iNDYuNDQiIGN5PSIzOC4xOSIgcj0iNy41NiIvPiA8cGF0aCBjbGFzcz0ic3QyIiBkPSJNMTcuNTYsMzIuNjNjMy44MywwLDcsMi44Nyw3LjQ5LDYuNTZjMC4wNC0wLjMzLDAuMDctMC42NiwwLjA3LTFjMC00LjE3LTMuMzktNy41Ni03LjU2LTcuNTYgUzEwLDM0LjAyLDEwLDM4LjE5YzAsMC4zNCwwLjAzLDAuNjcsMC4wNywxQzEwLjU3LDM1LjQ5LDEzLjczLDMyLjYzLDE3LjU2LDMyLjYzeiIvPiA8cGF0aCBjbGFzcz0ic3QyIiBkPSJNNDYuNDQsMzIuNjNjMy44MywwLDcsMi44Nyw3LjQ5LDYuNTZjMC4wNC0wLjMzLDAuMDctMC42NiwwLjA3LTFjMC00LjE3LTMuMzktNy41Ni03LjU2LTcuNTYgcy03LjU2LDMuMzktNy41Niw3LjU2YzAsMC4zNCwwLjAzLDAuNjcsMC4wNywxQzM5LjQ0LDM1LjQ5LDQyLjYsMzIuNjMsNDYuNDQsMzIuNjN6Ii8+IDwvZz4gPC9nPgoNPC9zdmc+ // @require https://cdn.jsdelivr.net/npm/[email protected]/src/index.min.js // @connect greasyfork.org // @connect sleazyfork.org // @license WTFPL // ==/UserScript== (function() { "use strict"; try { if (window.self !== window.top) return; } catch (e) { return; } const DEFAULT_SETTINGS = { autoCompact: true, autoCompactDelay: 2200, cacheDuration: 5 * 60 * 1000, hiddenDomains: [], defaultSort: "daily", theme: "light" }; class ThemeService { constructor() { this.settings = new SettingsService(); this.currentTheme = this.settings.getSetting("theme"); this.applyTheme(this.currentTheme); } toggleTheme() { const newTheme = this.currentTheme === "light" ? "dark" : "light"; this.currentTheme = newTheme; this.settings.setSetting("theme", newTheme); this.applyTheme(newTheme); } applyTheme(theme) { const root = document.documentElement; if (theme === "dark") { // Dark mode root.style.setProperty("--gf-toast-bg", "#0f172a"); root.style.setProperty("--gf-toast-text", "#f8fafc"); root.style.setProperty("--gf-color-white", "#f8fafc"); root.style.setProperty("--gf-color-dark-blue", "#f8fafc"); root.style.setProperty("--gf-color-gray-500", "#9ca3af"); root.style.setProperty("--gf-color-gray-600", "#d1d5db"); root.style.setProperty("--gf-color-gray-800", "#f3f4f6"); root.style.setProperty("--gf-color-gray-900", "#f9fafb"); root.style.setProperty("--gf-color-shadow-light", "rgba(255, 255, 255, 0.12)"); root.style.setProperty("--gf-color-shadow-medium", "rgba(255, 255, 255, 0.14)"); root.style.setProperty("--gf-color-shadow-dark", "rgba(255, 255, 255, 0.18)"); root.style.setProperty("--gf-color-background-blur", "rgba(15, 23, 42, 0.92)"); root.style.setProperty("--gf-background-primary", "#0f172a"); root.style.setProperty("--gf-background-secondary", "#1e293b"); root.style.setProperty("--gf-background-tertiary", "#334155"); root.style.setProperty("--gf-background-modal", "#0f172a"); root.style.setProperty("--gf-background-header", "#0f172a"); root.style.setProperty("--gf-background-footer", "#1e293b"); root.style.setProperty("--gf-text-primary", "#f8fafc"); root.style.setProperty("--gf-text-secondary", "#cbd5e1"); root.style.setProperty("--gf-text-tertiary", "#94a3b8"); root.style.setProperty("--gf-border-light", "rgba(255, 255, 255, 0.08)"); root.style.setProperty("--gf-border-medium", "rgba(255, 255, 255, 0.12)"); root.style.setProperty("--gf-border-heavy", "rgba(255, 255, 255, 0.16)"); } else { // Light mode root.style.setProperty("--gf-toast-bg", "#0f172a"); root.style.setProperty("--gf-toast-text", "#ffffff"); root.style.setProperty("--gf-color-white", "#ffffff"); root.style.setProperty("--gf-color-dark-blue", "#0f172a"); root.style.setProperty("--gf-color-gray-500", "#6b7280"); root.style.setProperty("--gf-color-gray-600", "#64748b"); root.style.setProperty("--gf-color-gray-800", "#111827"); root.style.setProperty("--gf-color-gray-900", "#0f172a"); root.style.setProperty("--gf-color-shadow-light", "rgba(0, 0, 0, 0.12)"); root.style.setProperty("--gf-color-shadow-medium", "rgba(0, 0, 0, 0.14)"); root.style.setProperty("--gf-color-shadow-dark", "rgba(0, 0, 0, 0.18)"); root.style.setProperty("--gf-color-background-blur", "rgba(255, 255, 255, 0.92)"); root.style.setProperty("--gf-background-primary", "#ffffff"); root.style.setProperty("--gf-background-secondary", "#f5f5f7"); root.style.setProperty("--gf-background-tertiary", "#f3f4f6"); root.style.setProperty("--gf-background-modal", "#ffffff"); root.style.setProperty("--gf-background-header", "#ffffff"); root.style.setProperty("--gf-background-footer", "#f5f5f7"); root.style.setProperty("--gf-text-primary", "#111827"); root.style.setProperty("--gf-text-secondary", "#6b7280"); root.style.setProperty("--gf-text-tertiary", "#64748b"); root.style.setProperty("--gf-border-light", "rgba(0, 0, 0, 0.08)"); root.style.setProperty("--gf-border-medium", "rgba(0, 0, 0, 0.12)"); root.style.setProperty("--gf-border-heavy", "rgba(0, 0, 0, 0.16)"); } } getThemeIcon() { return this.currentTheme === "light" ? "moon" : "sun"; } } class ToastService { constructor() { this.toast = null; this.timeout = null; } show(message) { this.hide(); this.toast = document.createElement("div"); this.toast.className = "gf-toast"; this.toast.textContent = message; document.body.appendChild(this.toast); setTimeout(() => { this.toast.classList.add("show"); }, 10); this.timeout = setTimeout(() => { this.hide(); }, 3000); } hide() { if (this.timeout) { clearTimeout(this.timeout); this.timeout = null; } if (this.toast) { this.toast.classList.remove("show"); setTimeout(() => { if (this.toast && this.toast.parentNode) { this.toast.parentNode.removeChild(this.toast); this.toast = null; } }, 300); } } } class SettingsService { constructor() { this.settings = this.loadSettings(); } loadSettings() { const saved = GM_getValue("user_settings", {}); return { ...DEFAULT_SETTINGS, ...saved }; } saveSettings() { GM_setValue("user_settings", this.settings); } getSetting(key) { return this.settings[key]; } setSetting(key, value) { this.settings[key] = value; this.saveSettings(); } isDomainHidden(domain) { return this.settings.hiddenDomains.includes(domain); } hideDomain(domain) { if (!this.isDomainHidden(domain)) { this.settings.hiddenDomains.push(domain); this.saveSettings(); } } showDomain(domain) { this.settings.hiddenDomains = this.settings.hiddenDomains.filter(d => d !== domain); this.saveSettings(); } } class HostService { getCurrentHost() { const hostname = window.location.hostname; return hostname.replace(/^(www\.|m\.|mobile\.)/, ""); } extractRootDomain(host) { const parts = host.split("."); if (parts.length > 2) return parts.slice(-2).join("."); return host; } formatHostForDisplay(host) { return this.extractRootDomain(host); } } class ScriptService { constructor(baseUrl, serviceName) { this.baseUrl = baseUrl; this.serviceName = serviceName; this.cache = new Map(); this.settings = new SettingsService(); } async searchScriptsByHost(host) { const rootDomain = new HostService().extractRootDomain(host); const cacheKey = `${this.serviceName}_${rootDomain}`; const cached = this.cache.get(cacheKey); const cacheDuration = this.settings.getSetting("cacheDuration"); if (cached && Date.now() - cached.timestamp < cacheDuration) { return cached.data; } let scripts = []; try { scripts = await this._fetchFromBySiteAPI(rootDomain); } catch (error1) { try { scripts = await this._fetchFromSearchAPI(rootDomain); } catch (error2) { scripts = []; } } const filtered = this._filterRelevantScripts(scripts, rootDomain); this.cache.set(cacheKey, { data: filtered, timestamp: Date.now() }); return filtered; } async _fetchFromBySiteAPI(domain) { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: "GET", url: `${this.baseUrl}/scripts/by-site/${domain}.json`, headers: { Accept: "application/json" }, onload: (response) => { if (response.status === 200) { try { resolve(JSON.parse(response.responseText)); } catch (e) { reject(e); } } else if (response.status === 404) { resolve([]); } else { reject(new Error(`API error: ${response.status}`)); } }, onerror: reject, }); }); } async _fetchFromSearchAPI(domain) { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: "GET", url: `${this.baseUrl}/scripts.json?q=${encodeURIComponent(domain)}&sort=updated`, headers: { Accept: "application/json" }, onload: (response) => { if (response.status === 200) { try { resolve(JSON.parse(response.responseText)); } catch (e) { reject(e); } } else { reject(new Error(`Search error: ${response.status}`)); } }, onerror: reject, }); }); } _filterRelevantScripts(scripts, domain) { return scripts .filter((script) => { if (script.domains) { return script.domains.some((d) => d.includes(domain) || domain.includes(d)); } return true; }) .slice(0, 150); } getDirectSearchUrl(domain) { return `${this.baseUrl}/scripts/by-site/${domain}`; } } class UIService { constructor() { this.themeService = new ThemeService(); this.styles = ` :root { --gf-color-white: #fff; --gf-color-dark-blue: #0f172a; --gf-color-green: #4CAF50; --gf-color-purple: #9C27B0; --gf-color-dark-purple: #7b1fa2; --gf-color-gray-500: #6b7280; --gf-color-gray-600: #64748b; --gf-color-gray-800: #111827; --gf-color-gray-900: #0f172a; --gf-color-error-red: #b91c1c; --gf-color-shadow-light: rgba(0, 0, 0, 0.12); --gf-color-shadow-medium: rgba(0, 0, 0, 0.14); --gf-color-shadow-dark: rgba(0, 0, 0, 0.18); --gf-color-background-blur: rgba(255, 255, 255, 0.92); --gf-color-green-highlight: rgba(76, 175, 80, 0.16); --gf-background-primary: #ffffff; --gf-background-secondary: #f5f5f7; --gf-background-tertiary: #f3f4f6; --gf-background-modal: #ffffff; --gf-background-header: #ffffff; --gf-background-footer: #f5f5f7; --gf-text-primary: #111827; --gf-text-secondary: #6b7280; --gf-text-tertiary: #64748b; --gf-border-light: rgba(0, 0, 0, 0.08); --gf-border-medium: rgba(0, 0, 0, 0.12); --gf-border-heavy: rgba(0, 0, 0, 0.16); } .gf-toast { position: fixed; top: 24px; left: 50%; transform: translateX(-50%) translateY(-30px); background: var(--gf-toast-bg); color: var(--gf-toast-text); padding: 14px 28px; border-radius: 12px; font-size: 14px; font-weight: 600; box-shadow: 0 12px 32px rgba(0, 0, 0, 0.25); opacity: 0; transition: opacity 0.3s cubic-bezier(0.4, 0, 0.2, 1), transform 0.3s cubic-bezier(0.4, 0, 0.2, 1); z-index: 2147483647; pointer-events: none; text-align: center; max-width: 480px; width: max-content; line-height: 1.5; backdrop-filter: blur(8px); border: 1px solid rgba(255, 255, 255, 0.1); } .gf-toast.show { opacity: 1; transform: translateX(-50%) translateY(0); } .gf-script-finder { all: initial; position: fixed !important; bottom: 14px !important; right: 0 !important; z-index: 2147483647 !important; font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif !important; transition: right 0.3s cubic-bezier(0.4, 0, 0.2, 1); } .gf-pill { display: flex; align-items: center; gap: 8px; height: 34px; padding: 0 10px 0 8px; border-radius: 999px; border: 1px solid var(--gf-border-light); background: var(--gf-color-background-blur); backdrop-filter: blur(8px); box-shadow: 0 10px 26px var(--gf-color-shadow-medium); cursor: pointer; transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); opacity: .55; position: relative; overflow: hidden; right: 14px !important; } .gf-pill:hover { opacity: 1; box-shadow: 0 14px 34px var(--gf-color-shadow-dark); } .gf-pill:active { transform: scale(.98); } .gf-pill-icon { width: 22px; height: 22px; border-radius: 999px; flex: 0 0 auto; background-image: url(data:image/svg+xml;base64,PCFET0NUWVBFIHN2ZyBQVUJMSUMgIi0vL1czQy8vRFREIFNWRyAxLjEvL0VOIiAiaHR0cDovL3d3dy53My5vcmcvR3JhcGhpY3MvU1ZHLzEuMS9EVEQvc3ZnMTEuZHRkIj4KPCEtLSBVcGxvYWRlZCB0bzogU1ZHIFJlcG8sIHd3dy5zdmdyZXBvLmNvbSwgVHJhbnNmb3JtZWQgYnk6IFNWRyBSZXBvIE1peGVyIFRvb2xzIC0tPgo8c3ZnIHdpZHRoPSI4MDBweCIgaGVpZ2h0PSI4MDBweCIgdmlld0JveD0iMCAwIDY0IDY0IiBpZD0iTGF5ZXJfMSIgdmVyc2lvbj0iMS4xIiB4bWx6c3BhY2U9InByZXNlcnZlIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiBmaWxsPSIjMDAwMDAwIj4KDTxnIGlkPSJTVkdSZXBvX2JnQ2FycmllciIgc3Ryb2tlLXdpZHRoPSIwIi8+Cg08ZyBpZD0iU1ZHUmVwb190cmFjZXJDYXJyaWVyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiLz4KDTxnIGlkPSJTVkdSZXBvX2ljb25DYXJyaWVyIj4gPHN0eWxlIHR5cGU9InRleHQvY3NzIj4gLnN0MHtmaWxsOiNCNEU2REQ7fSAuc3Qxe2ZpbGw6IzgwRDRDNDt9IC5zdDJ7ZmlsbDojRDJGMEVBO30gLnN0M3tmaWxsOiNGRkZGRkY7fSAuc3Q0e2ZpbGw6I0ZCRDg3Mjt9IC5zdDV7ZmlsbDojREI3NzY3O30gLnN0NntmaWxsOiNGMzhFN0E7fSAuc3Q3e2ZpbGw6I0Y2QUY2Mjt9IC5zdDh7ZmlsbDojMzJBNDhFO30gLnN0OXtmaWxsOiM1YmIxNWQ7fSAuc3QxMHtmaWxsOiM1YmIxNWQ7fSAuc3QxMXtmaWxsOiNFQUExNTc7fSAuc3QxMntmaWxsOiM1YmIxNWQ7fSAuc3QxM3tmaWxsOiNGOUM0NkE7fSAuc3QxNHtmaWxsOiNDRTZCNjE7fSA8L3N0eWxlPiA8Zz4gPHBhdGggY2xhc3M9InN0MTAiIGQ9Ik0yMy41NiwxNy45N2MyLjIsMCw0LjA2LDEuNDEsNC43NywzLjM3YzAuMTMtMC4zOSwwLjIyLTAuOCwwLjIyLTEuMjNjMC0yLjEzLTEuNzMtMy44Ni0zLjg2LTMuODYgYy0xLjYsMC0yLjk3LDAuOTctMy41NiwyLjM2QzIxLjg1LDE4LjIsMjIuNjgsMTcuOTcsMjMuNTYsMTcuOTd6Ii8+IDxwYXRoIGNsYXNzPSJzdDEwIiBkPSJNNDAuNDQsMTcuOTdjLTIuMiwwLTQuMDYsMS40MS00Ljc3LDMuMzdjLTAuMTMtMC4zOS0wLjIyLTAuOC0wLjIyLTEuMjNjMC0yLjEzLDEuNzMtMy44NiwzLjg2LTMuODYgYzEuNiwwLDIuOTcsMC45NywzLjU2LDIuMzZDNDIuMTUsMTguMiw0MS4zMiwxNy45Nyw0MC40NCwxNy45N3oiLz4gPHBhdGggY2xhc3M9InN0OSIgZD0iTTUzLjcyLDMyYy0wLjItMC4yOS04Ljc0LTExLjM4LTguOTctMTEuNjJjLTAuOS0xLjQ0LTIuNDktMi40MS00LjMxLTIuNDFjLTIuNDcsMC00LjUzLDEuNzYtNC45OSw0LjEgYy0yLjI1LTAuNjQtNC42NC0wLjY0LTYuODksMGMtMC40Ni0yLjM0LTIuNTItNC4xLTQuOTktNC4xYy0xLjgyLDAtMy40MSwwLjk3LTQuMzEsMi40MWMtMC4yMywwLjI1LTguNzgsMTEuMzMtOC45NywxMS42MiBDOC44NiwzMy42Nyw4LDM1LjgzLDgsMzguMTljMCw1LjI4LDQuMjgsOS41Niw5LjU2LDkuNTZjNS4xNCwwLDkuMzEtNC4wNSw5LjU0LTkuMTNjMC4wMy0wLjIxLDAuNTgtNS41OSwwLjk5LTkuNzYgYzIuMzEtMS43Miw1LjUtMS43MSw3LjgxLDBjMC40MSw0LjE2LDAuOTUsOS41NCwwLjk5LDkuNzVjMC4yMyw1LjA4LDQuNDEsOS4xMyw5LjU0LDkuMTNjNS4yOCwwLDkuNTYtNC4yOCw5LjU2LTkuNTYgQzU2LDM1LjgzLDU1LjE0LDMzLjY3LDUzLjcyLDMyeiBNMzIsMjYuMzdjLTAuOTMsMC0xLjY4LTAuNzUtMS42OC0xLjY4czAuNzUtMS42OCwxLjY4LTEuNjhzMS42OCwwLjc1LDEuNjgsMS42OCBTMzIuOTMsMjYuMzcsMzIsMjYuMzd6Ii8+IDxjaXJjbGUgY2xhc3M9InN0MTAiIGN4PSIxNy41NiIgY3k9IjM4LjE5IiByPSI5LjU2Ii8+IDxjaXJjbGUgY2xhc3M9InN0MyIgY3g9IjE3LjU2IiBjeT0iMzguMTkiIHI9IjcuNTYiLz4gPGNpcmNsZSBjbGFzcz0ic3QxMCIgY3g9IjQ2LjQ0IiBjeT0iMzguMTkiIHI9IjkuNTYiLz4gPGNpcmNsZSBjbGFzcz0ic3QzIiBjeD0iNDYuNDQiIGN5PSIzOC4xOSIgcj0iNy41NiIvPiA8cGF0aCBjbGFzcz0ic3QyIiBkPSJNMTcuNTYsMzIuNjNjMy44MywwLDcsMi44Nyw3LjQ5LDYuNTZjMC4wNC0wLjMzLDAuMDctMC42NiwwLjA3LTFjMC00LjE3LTMuMzktNy41Ni03LjU2LTcuNTYgUzEwLDM0LjAyLDEwLDM4LjE5YzAsMC4zNCwwLjAzLDAuNjcsMC4wNywxQzEwLjU3LDM1LjQ5LDEzLjczLDMyLjYzLDE3LjU2LDMyLjYzeiIvPiA8cGF0aCBjbGFzcz0ic3QyIiBkPSJNNDYuNDQsMzIuNjNjMy44MywwLDcsMi44Nyw3LjQ5LDYuNTZjMC4wNC0wLjMzLDAuMDctMC42NiwwLjA3LTFjMC00LjE3LTMuMzktNy41Ni03LjU2LTcuNTYgcy03LjU2LDMuMzktNy41Niw3LjU2YzAsMC4jNCwwLjAzLDAuNjcsMC4wNywxQzM5LjQ0LDM1LjQ5LDQyLjYsMzIuNjMsNDYuNDQsMzIuNjN6Ii8+IDwvZz4gPC9nPgoNPC9zdmc+); background-repeat: no-repeat; background-position: center; background-size: contain; } .gf-pill-label { font-size: 12px; font-weight: 800; letter-spacing: .2px; color: var(--gf-color-dark-blue); white-space: nowrap; } .gf-pill-badge { min-width: 18px; height: 18px; padding: 0 6px; border-radius: 999px; display: inline-flex; align-items: center; justify-content: center; font-size: 11px; font-weight: 900; color: var(--gf-color-white); background: var(--gf-color-green); line-height: 1; } .gf-pill.compact { width: 24px !important; padding: 0 !important; border-radius: 24px 0 0 24px !important; gap: 0 !important; transform: scale(0.8); opacity: 0.3; right: 0 !important; } .gf-pill.compact .gf-pill-icon { width: 100% !important; height: 100% !important; background-size: 16px 16px !important; background-position: 4px center !important; } .gf-pill.compact .gf-pill-label, .gf-pill.compact .gf-pill-badge { display: none !important; } .gf-modal { position: absolute; bottom: 44px; right: 14px; width: 480px; max-height: min(82vh, 780px); background: var(--gf-background-modal); border-radius: 20px; border: 1px solid var(--gf-border-light); box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.02), 0 20px 60px rgba(0, 0, 0, 0.12), 0 8px 20px rgba(0, 0, 0, 0.08); overflow: hidden; display: none; } .gf-modal.visible { display: flex; flex-direction: column; } .gf-modal-header { padding: 18px 24px; background: var(--gf-background-header); border-bottom: 1px solid var(--gf-border-light); position: relative; } .gf-modal-header::before { content: ''; position: absolute; top: 0; left: 0; right: 0; height: 4px; background: linear-gradient(90deg, var(--gf-color-green) 0%, #66BB6A 100%); } .gf-modal-header.sleazyfork::before { background: linear-gradient(90deg, var(--gf-color-purple) 0%, #BA68C8 100%); } .gf-modal-header-content { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; } .gf-modal-title-area { flex: 1; min-width: 0; } .gf-modal-title { font-weight: 700; font-size: 17px; line-height: 1.3; color: var(--gf-text-primary); margin: 0 0 6px 0; letter-spacing: -0.2px; } .gf-modal-subtitle { font-size: 13px; color: var(--gf-text-secondary); font-weight: 600; display: flex; align-items: center; gap: 6px; margin: 0; } .gf-modal-subtitle-count { display: inline-flex; align-items: center; justify-content: center; min-width: 22px; height: 22px; padding: 0 7px; background: var(--gf-background-tertiary); border-radius: 6px; font-size: 12px; font-weight: 700; color: var(--gf-text-primary); line-height: 1; } .gf-theme-toggle { width: 36px; height: 36px; border-radius: 10px; border: none; cursor: pointer; background: var(--gf-background-tertiary); color: var(--gf-text-secondary); font-size: 20px; display: grid; place-items: center; transition: all 0.2s ease; flex-shrink: 0; margin-left: auto; } .gf-theme-toggle:hover { background: var(--gf-background-tertiary); color: var(--gf-text-primary); transform: scale(1.05); } .gf-theme-toggle:active { transform: scale(0.95); } .gf-close-button { width: 36px; height: 36px; border-radius: 10px; border: none; cursor: pointer; background: var(--gf-background-tertiary); color: var(--gf-text-secondary); font-size: 20px; display: grid; place-items: center; transition: all 0.2s ease; flex-shrink: 0; font-weight: 300; } .gf-close-button:hover { background: var(--gf-background-tertiary); color: var(--gf-text-primary); transform: scale(1.05); } .gf-close-button:active { transform: scale(0.95); } .gf-tabs { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; padding: 12px 24px; background: var(--gf-background-secondary); border-bottom: 1px solid var(--gf-border-light); } .gf-tab { padding: 10px 16px; border: 1px solid var(--gf-border-light); border-radius: 10px; cursor: pointer; background: var(--gf-background-modal); font-size: 14px; font-weight: 600; color: var(--gf-text-secondary); transition: all 0.2s ease; position: relative; overflow: hidden; } .gf-tab::before { content: ''; position: absolute; top: 0; left: 0; right: 0; bottom: 0; background: linear-gradient(135deg, var(--gf-color-green-highlight), rgba(76, 175, 80, 0.04)); opacity: 0; transition: opacity 0.2s ease; } .gf-tab.sleazyfork::before { background: linear-gradient(135deg, rgba(156, 39, 176, 0.08), rgba(156, 39, 176, 0.04)); } .gf-tab:hover { border-color: var(--gf-border-medium); } .gf-tab:hover::before { opacity: 1; } .gf-tab.active { background: linear-gradient(135deg, var(--gf-color-green), #66BB6A); color: #ffffff; border-color: transparent; box-shadow: 0 4px 12px rgba(76, 175, 80, 0.25); } .gf-tab.sleazyfork.active { background: linear-gradient(135deg, var(--gf-color-purple), #BA68C8); box-shadow: 0 4px 12px rgba(156, 39, 176, 0.25); } .gf-tab.active::before { opacity: 0; } .gf-sort-menu { padding: 14px 24px; background: var(--gf-background-secondary); border-bottom: 1px solid var(--gf-border-light); display: flex; align-items: center; gap: 12px; } .gf-sort-label { font-size: 13px; font-weight: 600; color: var(--gf-text-primary); flex-shrink: 0; } .gf-sort-select { flex: 1; padding: 9px 14px; border-radius: 10px; border: 1px solid var(--gf-border-light); background: var(--gf-background-tertiary); color: var(--gf-text-primary); font-size: 13px; font-weight: 500; cursor: pointer; outline: none; transition: all 0.2s ease; } .gf-sort-select:hover { background: var(--gf-background-tertiary); border-color: var(--gf-border-medium); } .gf-sort-select:focus { border-color: var(--gf-color-green); background: var(--gf-background-modal); box-shadow: 0 0 0 3px rgba(76, 175, 80, 0.1); } .gf-sort-select.sleazyfork:focus { border-color: var(--gf-color-purple); box-shadow: 0 0 0 3px rgba(156, 39, 176, 0.1); } .gf-modal-content { flex: 1; overflow-y: auto; background: var(--gf-background-modal); } .gf-modal-content::-webkit-scrollbar { width: 8px; } .gf-modal-content::-webkit-scrollbar-track { background: transparent; } .gf-modal-content::-webkit-scrollbar-thumb { background: var(--gf-border-medium); border-radius: 4px; } .gf-modal-content::-webkit-scrollbar-thumb:hover { background: var(--gf-border-heavy); } .gf-loading { padding: 60px 24px; text-align: center; display: grid; gap: 16px; place-items: center; } .gf-loading-spinner { width: 40px; height: 40px; border-radius: 50%; border: 3px solid var(--gf-border-light); border-top-color: var(--gf-color-green); animation: gfSpin 0.8s linear infinite; } .gf-loading-spinner.sleazyfork { border-top-color: var(--gf-color-purple); } @keyframes gfSpin { to { transform: rotate(360deg); } } .gf-loading-text { font-size: 14px; color: var(--gf-text-secondary); font-weight: 500; } .gf-script-item { padding: 18px 24px; border-bottom: 1px solid var(--gf-border-light); cursor: pointer; transition: all 0.2s ease; position: relative; background: var(--gf-background-modal); } .gf-script-item::before { content: ''; position: absolute; left: 0; top: 0; bottom: 0; width: 3px; background: linear-gradient(135deg, var(--gf-color-green), #66BB6A); opacity: 0; transition: opacity 0.2s ease; } .gf-script-item:hover::before { opacity: 1; } .gf-script-item:hover { background: var(--gf-background-tertiary); } .gf-script-item:last-child { border-bottom: none; } .gf-script-top { display: grid; gap: 8px; margin-bottom: 10px; } .gf-script-title { display: block; text-decoration: none; color: var(--gf-text-primary); font-weight: 700; font-size: 15px; line-height: 1.4; transition: color 0.2s ease; } .gf-script-title:hover { color: var(--gf-color-green); } .gf-script-sub { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; font-size: 12px; color: var(--gf-text-secondary); font-weight: 600; } .gf-script-sub i { font-size: 13px; margin-right: 2px; } .gf-dot { opacity: 0.4; font-size: 10px; } .gf-script-description { color: var(--gf-text-primary); font-size: 13px; line-height: 1.6; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; margin-bottom: 12px; } .gf-script-meta { display: flex; flex-wrap: wrap; gap: 6px; } .gf-badge { display: inline-flex; align-items: center; gap: 5px; padding: 6px 10px; border-radius: 8px; font-size: 11px; font-weight: 700; background: var(--gf-background-tertiary); color: var(--gf-text-primary); line-height: 1; border: 1px solid var(--gf-border-light); transition: all 0.2s ease; } .gf-badge i { font-size: 14px; line-height: 1; } .gf-badge:hover { box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); } .gf-badge-score-high { background: linear-gradient(135deg, #dcfce7, #bbf7d0); color: #166534; border-color: #86efac; } .gf-badge-score-mid { background: linear-gradient(135deg, #fef3c7, #fde68a); color: #92400e; border-color: #fcd34d; } .gf-badge-score-low { background: linear-gradient(135deg, #fee2e2, #fecaca); color: #991b1b; border-color: #fca5a5; } .gf-empty-state, .gf-error { padding: 60px 32px; text-align: center; background: var(--gf-background-modal); } .gf-empty-state-title, .gf-error-title { font-weight: 700; font-size: 16px; color: var(--gf-text-primary); margin-bottom: 8px; } .gf-empty-state-text, .gf-error-text { color: var(--gf-text-secondary); font-size: 14px; line-height: 1.6; margin-bottom: 20px; } .gf-error-title { color: var(--gf-color-error-red); } .gf-empty-state a, .gf-error button { display: inline-flex; align-items: center; justify-content: center; padding: 11px 20px; border-radius: 10px; background: linear-gradient(135deg, var(--gf-color-green), #66BB6A); color: var(--gf-color-white); text-decoration: none; font-weight: 700; font-size: 13px; border: none; cursor: pointer; transition: all 0.2s ease; box-shadow: 0 4px 12px rgba(76, 175, 80, 0.2); } .gf-empty-state a:hover, .gf-error button:hover { box-shadow: 0 6px 16px rgba(76, 175, 80, 0.3); } .gf-empty-state a.sleazyfork, .gf-error button.sleazyfork { background: linear-gradient(135deg, var(--gf-color-purple), #BA68C8); box-shadow: 0 4px 12px rgba(156, 39, 176, 0.2); } .gf-empty-state a.sleazyfork:hover, .gf-error button.sleazyfork:hover { box-shadow: 0 6px 16px rgba(156, 39, 176, 0.3); } .gf-footer { padding: 16px 24px; border-top: 1px solid var(--gf-border-light); background: var(--gf-background-footer); font-size: 12px; display: flex; align-items: center; justify-content: space-between; gap: 16px; } .gf-footer-text { color: var(--gf-text-secondary); font-weight: 600; } .gf-footer a { color: var(--gf-color-green); text-decoration: none; font-weight: 700; transition: color 0.2s ease; } .gf-footer a:hover { color: #388e3c; text-decoration: underline; } .gf-footer a.sleazyfork { color: var(--gf-color-purple); } .gf-footer a.sleazyfork:hover { color: var(--gf-color-dark-purple); } .gf-hide-button { padding: 8px 14px; border-radius: 8px; border: 1px solid var(--gf-border-light); background: var(--gf-background-modal); color: var(--gf-text-secondary); font-size: 12px; font-weight: 600; cursor: pointer; transition: all 0.2s ease; } .gf-hide-button:hover { background: var(--gf-background-tertiary); border-color: var(--gf-border-medium); } @media (max-width: 480px) { .gf-toast { top: 16px; padding: 12px 20px; max-width: 90%; font-size: 13px; } .gf-modal { width: calc(100vw - 28px); right: 14px; max-height: min(85vh, 680px); } .gf-script-finder { right: 0 !important; bottom: 10px !important; } .gf-modal-header { padding: 16px 20px 14px; } .gf-modal-title { font-size: 16px; } .gf-tabs { padding: 10px 20px; } .gf-sort-menu { padding: 12px 20px; } .gf-script-item { padding: 16px 20px; } .gf-footer { padding: 14px 20px; flex-direction: column; gap: 12px; text-align: center; } } `; } getIcon(name) { return `<i class="ph ph-${name}"></i>`; } getLicenseIcon(license) { const licenseMap = { 'MIT': 'scroll', 'GPL': 'scales', 'Apache': 'apache-logo', 'BSD': 'file-text', 'ISC': 'certificate', 'MPL': 'mozilla-logo', 'CC': 'creative-commons-logo', }; for (const [key, icon] of Object.entries(licenseMap)) { if (license && license.toUpperCase().includes(key)) { return this.getIcon(icon); } } return this.getIcon('certificate'); } injectStyles() { if (typeof GM_addStyle === "function") { GM_addStyle(this.styles); return; } const style = document.createElement("style"); style.textContent = this.styles; document.head.appendChild(style); } createPill() { const pill = document.createElement("div"); pill.className = "gf-pill"; pill.title = "Find scripts for this site"; pill.innerHTML = ` <div class="gf-pill-icon"></div> <div class="gf-pill-label">Scripts</div> <div class="gf-pill-badge" style="display:none;">0</div> `; return pill; } createModal() { const modal = document.createElement("div"); modal.className = "gf-modal"; modal.innerHTML = ` <div class="gf-modal-header"> <div class="gf-modal-header-content"> <div class="gf-modal-title-area"> <h2 class="gf-modal-title">Scripts for this site</h2> <p class="gf-modal-subtitle"> <span class="gf-modal-subtitle-count">0</span> <span>scripts available</span> </p> </div> <button class="gf-theme-toggle" title="Toggle dark/light mode"> <i class="ph ph-${this.themeService.getThemeIcon()}"></i> </button> <button class="gf-close-button" title="Close">×</button> </div> </div> <div class="gf-tabs"> <button class="gf-tab greasyfork active" data-service="greasyfork">GreasyFork</button> <button class="gf-tab sleazyfork" data-service="sleazyfork">SleazyFork</button> </div> <div class="gf-sort-menu"> <span class="gf-sort-label">Sort by:</span> <select class="gf-sort-select" aria-label="Sort scripts"> <option value="daily">Daily installs</option> <option value="total">Total installs</option> <option value="good">Good ratings</option> <option value="fanscore">Fanscore</option> <option value="updatedate">Update date</option> <option value="createdate">Create date</option> </select> </div> <div class="gf-modal-content"> <div class="gf-loading"> <div class="gf-loading-spinner"></div> <div class="gf-loading-text">Searching scripts...</div> </div> </div> <div class="gf-footer"> <div class="gf-footer-text">Data from <a href="https://greasyfork.org" target="_blank" class="greasyfork">GreasyFork</a></div> <button class="gf-hide-button">Hide for this site</button> </div> `; return modal; } updateModalTitle(title, serviceName) { const titleElement = this.modal.querySelector(".gf-modal-title"); if (titleElement) titleElement.textContent = title; const headerElement = this.modal.querySelector(".gf-modal-header"); if (headerElement) { headerElement.classList.toggle("sleazyfork", serviceName === "sleazyfork"); } const footerLink = this.modal.querySelector(".gf-footer a"); if (footerLink) { if (serviceName === "greasyfork") { footerLink.href = "https://greasyfork.org"; footerLink.textContent = "GreasyFork"; footerLink.className = "greasyfork"; } else { footerLink.href = "https://sleazyfork.org"; footerLink.textContent = "SleazyFork"; footerLink.className = "sleazyfork"; } } } updateActiveTab(serviceName) { this.modal.querySelectorAll(".gf-tab").forEach((tab) => { tab.classList.toggle("active", tab.dataset.service === serviceName); }); } updateActiveSortDropdown(sortType, serviceName) { const select = this.modal.querySelector(".gf-sort-select"); if (!select) return; select.value = sortType; select.classList.toggle("sleazyfork", serviceName === "sleazyfork"); } showLoading(serviceName) { const content = this.modal.querySelector(".gf-modal-content"); if (!content) return; const spinnerClass = serviceName === "greasyfork" ? "" : "sleazyfork"; const label = serviceName === "greasyfork" ? "GreasyFork" : "SleazyFork"; content.innerHTML = ` <div class="gf-loading"> <div class="gf-loading-spinner ${spinnerClass}"></div> <div class="gf-loading-text">Searching scripts on ${label}...</div> </div> `; } showError(message, retryCallback, serviceName) { const content = this.modal.querySelector(".gf-modal-content"); if (!content) return; const buttonClass = serviceName === "greasyfork" ? "" : "sleazyfork"; content.innerHTML = ` <div class="gf-error"> <div class="gf-error-title">Something went wrong</div> <div class="gf-error-text">${this._escapeHtml(message)}</div> ${ retryCallback ? `<button class="gf-retry ${buttonClass}">Try again</button>` : "" } </div> `; if (retryCallback) { content.querySelector(".gf-retry").addEventListener("click", retryCallback); } } showEmptyState(domain, directUrl, serviceName) { const content = this.modal.querySelector(".gf-modal-content"); if (!content) return; const serviceDisplay = serviceName === "greasyfork" ? "GreasyFork" : "SleazyFork"; const linkClass = serviceName === "greasyfork" ? "" : "sleazyfork"; content.innerHTML = ` <div class="gf-empty-state"> <div class="gf-empty-state-title">No scripts found</div> <div class="gf-empty-state-text">Nothing matched <strong>${this._escapeHtml(domain)}</strong> on ${serviceDisplay}.</div> <a href="${this._escapeHtml(directUrl)}" target="_blank" class="${linkClass}">Search manually</a> </div> `; } createScriptItem(script, serviceName) { const item = document.createElement("div"); item.className = "gf-script-item"; const formatNumber = (num) => { const n = Number(num); if (!Number.isFinite(n)) return null; if (n >= 1000) return (n / 1000).toFixed(1).replace(".0", "") + "k"; return n.toString(); }; const safeDate = (iso) => { if (!iso) return null; const d = new Date(iso); return Number.isNaN(d.getTime()) ? null : d.toLocaleDateString(); }; 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 fanScoreText = Number.isFinite(fanScore) ? fanScore.toFixed(1) : null; const updatedDate = safeDate(script.code_updated_at); const createdDate = safeDate(script.created_at); const scriptUrl = script.url?.startsWith("http") ? script.url : (serviceName === "greasyfork" ? "https://greasyfork.org" : "https://sleazyfork.org") + (script.url || ""); const badge = (icon, text, title, extraClass = "") => { if (!text) return ""; return `<span class="gf-badge ${extraClass}" title="${this._escapeHtml(title)}">${this.getIcon(icon)} ${this._escapeHtml(text)}</span>`; }; const fanClass = fanScore >= 8 ? "gf-badge-score-high" : fanScore >= 6 ? "gf-badge-score-mid" : fanScore >= 0 ? "gf-badge-score-low" : ""; const author = script.users?.[0]?.name || null; const licenseIcon = script.license ? this.getLicenseIcon(script.license) : ''; item.innerHTML = ` <div class="gf-script-top"> <a href="${this._escapeHtml(scriptUrl)}" target="_blank" class="gf-script-title" title="${this._escapeHtml(script.name || "")}"> ${this._escapeHtml(script.name || "Untitled script")} </a> <div class="gf-script-sub"> ${author ? `<span title="Author">${this.getIcon('user')} ${this._escapeHtml(author)}</span>` : ""} ${author && script.version ? `<span class="gf-dot">•</span>` : ""} ${script.version ? `<span title="Version">${this.getIcon('git-branch')} v${this._escapeHtml(script.version)}</span>` : ""} ${(author || script.version) && script.license ? `<span class="gf-dot">•</span>` : ""} ${script.license ? `<span title="License">${licenseIcon} ${this._escapeHtml(script.license)}</span>` : ""} </div> </div> <div class="gf-script-description" title="${this._escapeHtml(script.description || "")}"> ${this._escapeHtml(script.description || "No description available")} </div> <div class="gf-script-meta"> ${badge("download-simple", daily ? `${daily}/day` : null, "Daily installs")} ${badge("chart-bar", total, "Total installs")} ${badge("star", good, "Positive ratings")} ${badge("flame", fanScoreText, "Fan score", fanScoreText ? fanClass : "")} ${badge("arrow-clockwise", updatedDate, "Last code update")} ${badge("calendar-plus", createdDate, "Created at")} </div> `; return item; } setPillCount(count) { const badge = this.pill.querySelector(".gf-pill-badge"); const subtitleCount = this.modal.querySelector(".gf-modal-subtitle-count"); const subtitleText = this.modal.querySelector(".gf-modal-subtitle span:last-child"); if (badge) { if (!count || count <= 0) { badge.style.display = "none"; badge.textContent = ""; } else { badge.style.display = "inline-flex"; badge.textContent = count > 99 ? "99+" : String(count); } } if (subtitleCount) { subtitleCount.textContent = count || 0; } if (subtitleText) { subtitleText.textContent = count === 1 ? "script available" : "scripts available"; } } setPillServiceColor(serviceName) { const badge = this.pill.querySelector(".gf-pill-badge"); if (!badge) return; badge.style.background = serviceName === "greasyfork" ? "var(--gf-color-green)" : "var(--gf-color-purple)"; } showPill() { this.container.style.display = ""; } hidePill() { this.container.style.display = "none"; } setCompactMode(compact) { this.pill.classList.toggle("compact", !!compact); } _escapeHtml(text) { const div = document.createElement("div"); div.textContent = text || ""; return div.innerHTML; } } class ScriptFinderController { constructor(hostService, uiService) { this.hostService = hostService; this.uiService = uiService; this.settings = new SettingsService(); this.toast = new ToastService(); this.services = { greasyfork: new ScriptService("https://greasyfork.org", "greasyfork"), sleazyfork: new ScriptService("https://sleazyfork.org", "sleazyfork"), }; this.currentService = "greasyfork"; this.currentSort = this.settings.getSetting("defaultSort"); this.isModalOpen = false; this.isLoading = false; this.currentDomain = null; this.compactTimer = null; } async initialize() { this.uiService.injectStyles(); this.setupUI(); this.setupEventListeners(); const host = this.hostService.getCurrentHost(); this.currentDomain = this.hostService.extractRootDomain(host); this.setupMenuCommands(); if (this.settings.isDomainHidden(this.currentDomain)) { this.uiService.hidePill(); return; } this.uiService.showPill(); const autoCompact = this.settings.getSetting("autoCompact"); this.uiService.setCompactMode(false); if (autoCompact) { this.startCompactTimer(); } } setupUI() { this.container = document.createElement("div"); this.container.className = "gf-script-finder"; this.pill = this.uiService.createPill(); this.modal = this.uiService.createModal(); this.uiService.container = this.container; this.uiService.pill = this.pill; this.uiService.modal = this.modal; this.modalContent = this.modal.querySelector(".gf-modal-content"); this.container.appendChild(this.pill); this.container.appendChild(this.modal); document.body.appendChild(this.container); } setupMenuCommands() { if (typeof GM_registerMenuCommand !== "function") return; if (window._menuCommandIds) { window._menuCommandIds.forEach(id => { if (typeof GM_unregisterMenuCommand === "function") { GM_unregisterMenuCommand(id); } }); window._menuCommandIds = []; } else { window._menuCommandIds = []; } const domainDisplay = this.hostService.formatHostForDisplay(this.currentDomain); const isHidden = this.settings.isDomainHidden(this.currentDomain); const createMenuCommands = () => { if (window._menuCommandIds) { window._menuCommandIds.forEach(id => { if (typeof GM_unregisterMenuCommand === "function") { GM_unregisterMenuCommand(id); } }); window._menuCommandIds = []; } const domainDisplay = this.hostService.formatHostForDisplay(this.currentDomain); const isHidden = this.settings.isDomainHidden(this.currentDomain); const toggleText = isHidden ? `Show for ${domainDisplay}` : `Hide for ${domainDisplay}`; const toggleId = GM_registerMenuCommand(toggleText, () => { if (isHidden) { this.settings.showDomain(this.currentDomain); this.uiService.showPill(); this.toast.show(`Script finder shown for ${domainDisplay}`); } else { const confirmed = confirm(`Hide script for ${domainDisplay}? You can show it again via Tampermonkey menu.`); if (confirmed) { this.settings.hideDomain(this.currentDomain); this.uiService.hidePill(); this.toast.show(`Script finder hidden for ${domainDisplay}`); } } setTimeout(() => createMenuCommands(), 100); }); window._menuCommandIds.push(toggleId); if (!isHidden) { const greasyId = GM_registerMenuCommand("Open Script Finder (GreasyFork)", () => { this.currentService = "greasyfork"; this.openModal(); }); window._menuCommandIds.push(greasyId); const sleazyId = GM_registerMenuCommand("Open Script Finder (SleazyFork)", () => { this.currentService = "sleazyfork"; this.openModal(); }); window._menuCommandIds.push(sleazyId); const compactId = GM_registerMenuCommand("Toggle Auto-Compact Mode", () => { const current = this.settings.getSetting("autoCompact"); const newValue = !current; this.settings.setSetting("autoCompact", newValue); if (newValue) { if (!this.isModalOpen) { this.startCompactTimer(); this.toast.show("Auto-compact mode ENABLED - The pill will automatically minimize."); } else { this.toast.show("Auto-compact mode ENABLED - Pill will auto-compact when you close the modal."); } } else { this.clearCompactTimer(); this.uiService.setCompactMode(false); this.toast.show("Auto-compact mode DISABLED - The pill will remain expanded."); } }); window._menuCommandIds.push(compactId); } const resetId = GM_registerMenuCommand("Reset All Settings", () => { const confirmed = confirm("Reset all settings to default?"); if (confirmed) { GM_deleteValue("user_settings"); this.toast.show("All settings reset to default"); location.reload(); } }); window._menuCommandIds.push(resetId); }; createMenuCommands(); } setupEventListeners() { this.pill.addEventListener("click", () => this.toggleModal()); this.pill.addEventListener("mouseenter", () => { this.uiService.setCompactMode(false); this.clearCompactTimer(); }); this.pill.addEventListener("mouseleave", () => { const autoCompact = this.settings.getSetting("autoCompact"); if (autoCompact && !this.isModalOpen) { this.startCompactTimer(); } }); this.modal.querySelector(".gf-close-button").addEventListener("click", () => this.closeModal()); this.modal.querySelector(".gf-theme-toggle").addEventListener("click", () => { this.uiService.themeService.toggleTheme(); const themeToggleIcon = this.modal.querySelector(".gf-theme-toggle i"); themeToggleIcon.className = `ph ph-${this.uiService.themeService.getThemeIcon()}`; }); this.modal.querySelectorAll(".gf-tab").forEach((tab) => { tab.addEventListener("click", (e) => { const serviceName = e.currentTarget.dataset.service; if (serviceName !== this.currentService) { this.currentService = serviceName; this.uiService.updateActiveTab(serviceName); this.uiService.setPillServiceColor(serviceName); this.uiService.updateActiveSortDropdown(this.currentSort, serviceName); this.loadScripts(); } }); }); const sortSelect = this.modal.querySelector(".gf-sort-select"); if (sortSelect) { sortSelect.addEventListener("change", (e) => { this.currentSort = e.currentTarget.value; this.uiService.updateActiveSortDropdown(this.currentSort, this.currentService); this.displayCachedScripts(); }); } this.modal.querySelector(".gf-hide-button").addEventListener("click", () => { const domainDisplay = this.hostService.formatHostForDisplay(this.currentDomain); const isCurrentlyHidden = this.settings.isDomainHidden(this.currentDomain); if (isCurrentlyHidden) { this.settings.showDomain(this.currentDomain); this.uiService.showPill(); this.toast.show(`Script finder shown for ${domainDisplay}`); } else { const confirmed = confirm(`Hide the pill for ${domainDisplay}? You can show it again via Tampermonkey menu.`); if (confirmed) { this.settings.hideDomain(this.currentDomain); this.uiService.hidePill(); this.toast.show(`Script finder hidden for ${domainDisplay}`); } } setTimeout(() => this.setupMenuCommands(), 100); this.closeModal(); }); document.addEventListener("keydown", (e) => { if (e.key === "Escape" && this.isModalOpen) this.closeModal(); }); document.addEventListener("click", (e) => { if (this.isModalOpen && !this.container.contains(e.target)) { this.closeModal(); } }); document.addEventListener("fullscreenchange", () => { const isFs = !!document.fullscreenElement || !!document.webkitFullscreenElement; this.container.style.display = isFs ? "none" : ""; }); } clearCompactTimer() { if (this.compactTimer) clearTimeout(this.compactTimer); this.compactTimer = null; } startCompactTimer() { this.clearCompactTimer(); const autoCompact = this.settings.getSetting("autoCompact"); if (!autoCompact) return; const delay = this.settings.getSetting("autoCompactDelay"); this.compactTimer = setTimeout(() => { if (!this.isModalOpen) this.uiService.setCompactMode(true); }, delay); } toggleModal() { this.isModalOpen ? this.closeModal() : this.openModal(); } openModal() { this.isModalOpen = true; this.modal.classList.add("visible"); this.pill.style.opacity = "1"; this.uiService.updateActiveTab(this.currentService); this.uiService.setPillServiceColor(this.currentService); this.uiService.updateActiveSortDropdown(this.currentSort, this.currentService); this.uiService.setCompactMode(false); this.clearCompactTimer(); const hideButton = this.modal.querySelector(".gf-hide-button"); if (hideButton) { const isHidden = this.settings.isDomainHidden(this.currentDomain); hideButton.textContent = "Hide for this site"; } this.loadScripts(); } closeModal() { this.isModalOpen = false; this.modal.classList.remove("visible"); this.pill.style.opacity = ""; const autoCompact = this.settings.getSetting("autoCompact"); if (autoCompact) { this.startCompactTimer(); } } sortScripts(scripts) { if (!scripts) return []; const scriptCopy = [...scripts]; switch (this.currentSort) { case "daily": return scriptCopy.sort((a, b) => (b.daily_installs || 0) - (a.daily_installs || 0)); case "total": return scriptCopy.sort((a, b) => (b.total_installs || 0) - (a.total_installs || 0)); case "good": return scriptCopy.sort((a, b) => (b.good_ratings || 0) - (a.good_ratings || 0)); case "fanscore": return scriptCopy.sort((a, b) => (b.fan_score || 0) - (a.fan_score || 0)); case "updatedate": return scriptCopy.sort((a, b) => new Date(b.code_updated_at || 0) - new Date(a.code_updated_at || 0)); case "createdate": return scriptCopy.sort((a, b) => new Date(b.created_at || 0) - new Date(a.created_at || 0)); default: return scriptCopy.sort((a, b) => (b.daily_installs || 0) - (a.daily_installs || 0)); } } displayCachedScripts() { const service = this.services[this.currentService]; const cached = service.cache.get(`${this.currentService}_${this.currentDomain}`); const scripts = cached?.data || []; const displayHost = this.hostService.formatHostForDisplay(this.currentDomain); const serviceDisplay = this.currentService === "greasyfork" ? "GreasyFork" : "SleazyFork"; this.uiService.updateModalTitle(`Scripts for ${displayHost}`, this.currentService); this.uiService.updateActiveSortDropdown(this.currentSort, this.currentService); if (!scripts || scripts.length === 0) { const directUrl = service.getDirectSearchUrl(this.currentDomain); this.uiService.showEmptyState(displayHost, directUrl, this.currentService); this.uiService.setPillCount(0); return; } const sortedScripts = this.sortScripts(scripts); this.modalContent.innerHTML = ""; this.uiService.setPillCount(sortedScripts.length); sortedScripts.forEach((script) => { const item = this.uiService.createScriptItem(script, this.currentService); item.addEventListener("click", (e) => { if (e.target.tagName === "A") return; const url = script.url?.startsWith("http") ? script.url : this.services[this.currentService].baseUrl + (script.url || ""); if (url) GM_openInTab(url, { active: true }); }); this.modalContent.appendChild(item); }); } async loadScripts() { if (this.isLoading) return; this.isLoading = true; const service = this.services[this.currentService]; this.uiService.showLoading(this.currentService); try { const host = this.hostService.getCurrentHost(); this.currentDomain = this.hostService.extractRootDomain(host); const scripts = await service.searchScriptsByHost(this.currentDomain); this.uiService.setPillCount(Array.isArray(scripts) ? scripts.length : 0); this.displayCachedScripts(); } catch (error) { this.uiService.showError( `Error searching scripts: ${error?.message || "Unknown error"}`, () => this.loadScripts(), this.currentService ); this.uiService.setPillCount(0); } finally { this.isLoading = false; } } } function initializeApp() { try { const hostService = new HostService(); const uiService = new UIService(); const controller = new ScriptFinderController(hostService, uiService); controller.initialize(); } catch (e) { console.error("[Script Finder] Init error:", e); } } if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", initializeApp); } else { setTimeout(initializeApp, 80); } })();