GreasyFork/SleazyFork Script Finder

Finds GreasyFork/SleazyFork scripts for the current domain

スクリプトをインストールするには、Tampermonkey, GreasemonkeyViolentmonkey のような拡張機能のインストールが必要です。

You will need to install an extension such as Tampermonkey to install this script.

スクリプトをインストールするには、TampermonkeyViolentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、TampermonkeyUserscripts のような拡張機能のインストールが必要です。

このスクリプトをインストールするには、Tampermonkeyなどの拡張機能をインストールする必要があります。

このスクリプトをインストールするには、ユーザースクリプト管理ツールの拡張機能をインストールする必要があります。

(ユーザースクリプト管理ツールは設定済みなのでインストール!)

このスタイルをインストールするには、Stylusなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus などの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus tなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

(ユーザースタイル管理ツールは設定済みなのでインストール!)

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください
// ==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);
  }
})();