Greasy Fork is available in English.
Finds GreasyFork/SleazyFork scripts for the current domain
// ==UserScript==
// @name GreasyFork/SleazyFork Script Finder
// @namespace http://tampermonkey.net/
// @version 3.3.4
// @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+
// @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,PCFET0NUWVBFIHN2ZyBQVUJMSUMgIi0vL1czQy8vRFREIFNWRyAxLjEvL0VOIiAiaHR0cDovL3d3dy53My5vcmcvR3JhcGhpY3MvU1ZHLzEuMS9EVEQvc3ZnMTEuZHRkIj4KPCEtLSBVcGxvYWRlZCB0bzogU1ZHIFJlcG8sIHd3dy5zdmdyZXBvLmNvbSwgVHJhbnNmb3JtZWQgYnk6IFNWRyBSZXBvIE1peGVyIFRvb2xzIC0tPgo8c3ZnIHdpZHRoPSI4MDBweCIgaGVpZ2h0PSI4MDBweCIgdmlld0JveD0iMCAwIDY0IDY0IiBpZD0iTGF5ZXJfMSIgdmVyc2lvbj0iMS4xIiB4bWw6c3BhY2U9InByZXNlcnZlIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiBmaWxsPSIjMDAwMDAwIj4KDTxnIGlkPSJTVkdSZXBvX2JnQ2FycmllciIgc3Ryb2tlLXdpZHRoPSIwIi8+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 svg {
width: 14px;
height: 14px;
}
.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) {
const iconMap = {
'moon': '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 256 256"><path d="M233.54,142.23a8,8,0,0,0-8-2,88.08,88.08,0,0,1-109.8-109.8,8,8,0,0,0-10-10,104.84,104.84,0,0,0-52.91,37A104,104,0,0,0,136,224a103.09,103.09,0,0,0,62.52-20.88,104.84,104.84,0,0,0,37-52.91A8,8,0,0,0,233.54,142.23ZM188.9,190.34A88,88,0,0,1,65.66,67.11a89,89,0,0,1,31.4-26A106,106,0,0,0,96,56A104.11,104.11,0,0,0,200,160a106,106,0,0,0,14.92-1.06A89,89,0,0,1,188.9,190.34Z"></path></svg>',
'sun': '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 256 256"><path d="M120,40V16a8,8,0,0,1,16,0V40a8,8,0,0,1-16,0Zm72,88a64,64,0,1,1-64-64A64.07,64.07,0,0,1,192,128Zm-16,0a48,48,0,1,0-48,48A48.05,48.05,0,0,0,176,128ZM58.34,69.66A8,8,0,0,0,69.66,58.34l-16-16A8,8,0,0,0,42.34,53.66Zm0,116.68-16,16a8,8,0,0,0,11.32,11.32l16-16a8,8,0,0,0-11.32-11.32ZM192,72a8,8,0,0,0,5.66-2.34l16-16a8,8,0,0,0-11.32-11.32l-16,16A8,8,0,0,0,192,72Zm5.66,114.34a8,8,0,0,0-11.32,11.32l16,16a8,8,0,0,0,11.32-11.32ZM48,128a8,8,0,0,0-8-8H16a8,8,0,0,0,0,16H40A8,8,0,0,0,48,128Zm80,80a8,8,0,0,0-8,8v24a8,8,0,0,0,16,0V216A8,8,0,0,0,128,208Zm112-88H216a8,8,0,0,0,0,16h24a8,8,0,0,0,0-16Z"></path></svg>',
'scales': '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 256 256"><path d="M239.43,133l-32-80h0a8,8,0,0,0-9.16-4.84L136,62V40a8,8,0,0,0-16,0V65.58L54.26,80.19A8,8,0,0,0,48.57,85h0v.06L16.57,165a7.92,7.92,0,0,0-.57,3c0,23.31,24.54,32,40,32s40-8.69,40-32a7.92,7.92,0,0,0-.57-3L66.92,93.77,120,82V208H104a8,8,0,0,0,0,16h48a8,8,0,0,0,0-16H136V78.42L187,67.1,160.57,133a7.92,7.92,0,0,0-.57,3c0,23.31,24.54,32,40,32s40-8.69,40-32A7.92,7.92,0,0,0,239.43,133ZM56,184c-7.53,0-22.76-3.61-23.93-14.64L56,109.54l23.93,59.82C78.76,180.39,63.53,184,56,184Zm144-32c-7.53,0-22.76-3.61-23.93-14.64L200,77.54l23.93,59.82C222.76,148.39,207.53,152,200,152Z"></path></svg>',
'file-text': '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 256 256"><path d="M213.66,82.34l-56-56A8,8,0,0,0,152,24H56A16,16,0,0,0,40,40V216a16,16,0,0,0,16,16H200a16,16,0,0,0,16-16V88A8,8,0,0,0,213.66,82.34ZM160,51.31,188.69,80H160ZM200,216H56V40h88V88a8,8,0,0,0,8,8h48V216Zm-32-80a8,8,0,0,1-8,8H96a8,8,0,0,1,0-16h64A8,8,0,0,1,168,136Zm0,32a8,8,0,0,1-8,8H96a8,8,0,0,1,0-16h64A8,8,0,0,1,168,168Z"></path></svg>',
'user': '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 256 256"><path d="M230.92,212c-15.23-26.33-38.7-45.21-66.09-54.16a72,72,0,1,0-73.66,0C63.78,166.78,40.31,185.66,25.08,212a8,8,0,1,0,13.85,8c18.84-32.56,52.14-52,89.07-52s70.23,19.44,89.07,52a8,8,0,1,0,13.85-8ZM72,96a56,56,0,1,1,56,56A56.06,56.06,0,0,1,72,96Z"></path></svg>',
'git-branch': '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 256 256"><path d="M232,64a32,32,0,1,0-40,31v17a8,8,0,0,1-8,8H96a23.84,23.84,0,0,0-8,1.38V95a32,32,0,1,0-16,0v66a32,32,0,1,0,16,0V144a8,8,0,0,1,8-8h88a24,24,0,0,0,24-24V95A32.06,32.06,0,0,0,232,64ZM64,64A16,16,0,1,1,80,80,16,16,0,0,1,64,64ZM96,192a16,16,0,1,1-16-16A16,16,0,0,1,96,192ZM200,80a16,16,0,1,1,16-16A16,16,0,0,1,200,80Z"></path></svg>',
'download-simple': '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 256 256"><path d="M224,144v64a8,8,0,0,1-8,8H40a8,8,0,0,1-8-8V144a8,8,0,0,1,16,0v56H208V144a8,8,0,0,1,16,0Zm-101.66,5.66a8,8,0,0,0,11.32,0l40-40a8,8,0,0,0-11.32-11.32L136,124.69V32a8,8,0,0,0-16,0v92.69L93.66,98.34a8,8,0,0,0-11.32,11.32Z"></path></svg>',
'chart-bar': '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 256 256"><path d="M224,200h-8V40a8,8,0,0,0-8-8H152a8,8,0,0,0-8,8V80H96a8,8,0,0,0-8,8v40H48a8,8,0,0,0-8,8v64H32a8,8,0,0,0,0,16H224a8,8,0,0,0,0-16ZM160,48h40V200H160ZM104,96h40V200H104ZM56,144H88v56H56Z"></path></svg>',
'star': '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 256 256"><path d="M239.18,97.26A16.38,16.38,0,0,0,224.92,86l-59-4.76L143.14,26.15a16.36,16.36,0,0,0-30.27,0L90.11,81.23,31.08,86a16.46,16.46,0,0,0-9.37,28.86l45,38.83L53,211.75a16.38,16.38,0,0,0,24.5,17.82L128,198.49l50.53,31.08A16.4,16.4,0,0,0,203,211.75l-13.76-58.07,45-38.83A16.43,16.43,0,0,0,239.18,97.26Zm-15.34,5.47-48.7,42a8,8,0,0,0-2.56,7.91l14.88,62.8a.37.37,0,0,1-.17.48c-.18.14-.23.11-.38,0l-54.72-33.65a8,8,0,0,0-8.38,0L69.09,215.94c-.15.09-.19.12-.38,0a.37.37,0,0,1-.17-.48l14.88-62.8a8,8,0,0,0-2.56-7.91l-48.7-42c-.12-.1-.23-.19-.13-.5s.18-.27.33-.29l63.92-5.16A8,8,0,0,0,103,91.86l24.62-59.61c.08-.17.11-.25.35-.25s.27.08.35.25L153,91.86a8,8,0,0,0,6.75,4.92l63.92,5.16c.15,0,.24,0,.33.29S224,102.63,223.84,102.73Z"></path></svg>',
'flame': '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 256 256"><path d="M173.79,51.48a221.25,221.25,0,0,0-41.67-34.34a8,8,0,0,0-8.24,0A221.25,221.25,0,0,0,82.21,51.48C54.59,80.48,40,112.47,40,144a88,88,0,0,0,176,0C216,112.47,201.41,80.48,173.79,51.48ZM96,184c0-27.67,22.53-47.28,32-54.3,9.48,7,32,26.63,32,54.3a32,32,0,0,1-64,0Zm77.27,15.93A47.8,47.8,0,0,0,176,184c0-44-42.09-69.79-43.88-70.86a8,8,0,0,0-8.24,0C122.09,114.21,80,140,80,184a47.8,47.8,0,0,0,2.73,15.93A71.88,71.88,0,0,1,56,144c0-34.41,20.4-63.15,37.52-81.19A216.21,216.21,0,0,1,128,33.54a215.77,215.77,0,0,1,34.48,29.27C193.49,95.5,200,125,200,144A71.88,71.88,0,0,1,173.27,199.93Z"></path></svg>',
'arrow-clockwise': '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 256 256"><path d="M240,56v48a8,8,0,0,1-8,8H184a8,8,0,0,1,0-16H211.4L184.81,71.64l-.25-.24a80,80,0,1,0-1.67,114.78a8,8,0,0,1,11,11.63A95.44,95.44,0,0,1,128,224h-1.32A96,96,0,1,1,195.75,60L224,85.8V56a8,8,0,1,1,16,0Z"></path></svg>',
'calendar-plus': '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 256 256"><path d="M208,32H184V24a8,8,0,0,0-16,0v8H88V24a8,8,0,0,0-16,0v8H48A16,16,0,0,0,32,48V208a16,16,0,0,0,16,16H208a16,16,0,0,0,16-16V48A16,16,0,0,0,208,32ZM72,48v8a8,8,0,0,0,16,0V48h80v8a8,8,0,0,0,16,0V48h24V80H48V48ZM208,208H48V96H208V208Zm-48-56a8,8,0,0,1-8,8H136v16a8,8,0,0,1-16,0V160H104a8,8,0,0,1,0-16h16V128a8,8,0,0,1,16,0v16h16A8,8,0,0,1,160,152Z"></path></svg>'
};
return iconMap[name] || '';
}
getLicenseIcon(license) {
// Simplificação: usar o ícone de scales para todas as licenças
return this.getIcon('scales');
}
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 type="button" class="gf-theme-toggle" title="Toggle dark/light mode">
${this.getIcon(this.themeService.getThemeIcon())}
</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 = 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";
const shadow = this.container.attachShadow({ mode: 'open' });
const styleEl = document.createElement('style');
styleEl.textContent = this.uiService.styles;
shadow.appendChild(styleEl);
this.pill = this.uiService.createPill();
this.modal = this.uiService.createModal();
shadow.appendChild(this.pill);
shadow.appendChild(this.modal);
this.uiService.container = this.container;
this.uiService.pill = this.pill;
this.uiService.modal = this.modal;
this.modalContent = this.modal.querySelector(".gf-modal-content");
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", (e) => {
e.stopPropagation();
this.uiService.themeService.toggleTheme();
const themeToggleButton = this.modal.querySelector(".gf-theme-toggle");
themeToggleButton.innerHTML = this.uiService.getIcon(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);
}
})();