Find Scripts For This Site

Find userscripts for the current website from popular script repositories

คุณจะต้องติดตั้งส่วนขยาย เช่น Tampermonkey, Greasemonkey หรือ Violentmonkey เพื่อติดตั้งสคริปต์นี้

คุณจะต้องติดตั้งส่วนขยาย เช่น Tampermonkey หรือ Violentmonkey เพื่อติดตั้งสคริปต์นี้

คุณจะต้องติดตั้งส่วนขยาย เช่น Tampermonkey หรือ Violentmonkey เพื่อติดตั้งสคริปต์นี้

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

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

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name                 Find Scripts For This Site
// @name:zh-CN           查找适用于当前网站的脚本
// @name:zh-TW           查找適用於當前網站的腳本
// @name:ja              このサイト用のスクリプトを探す
// @name:ko              이 사이트용 스크립트 찾기
// @name:es              Buscar scripts para este sitio
// @name:fr              Trouver des scripts pour ce site
// @name:de              Skripte für diese Website finden
// @name:ru              Найти скрипты для этого сайта
// @namespace            https://github.com/utags
// @homepageURL          https://github.com/utags/userscripts#readme
// @supportURL           https://github.com/utags/userscripts/issues
// @version              0.2.1
// @description          Find userscripts for the current website from popular script repositories
// @description:zh-CN    查找适用于当前网站的用户脚本,支持多个脚本仓库
// @description:zh-TW    查找適用於當前網站的用戶腳本,支持多個腳本倉庫
// @description:ja       人気のスクリプトリポジトリから現在のウェブサイト用のユーザースクリプトを見つける
// @description:ko       인기 스크립트 저장소에서 현재 웹사이트용 사용자 스크립트 찾기
// @description:es       Encuentra userscripts para el sitio web actual desde repositorios populares
// @description:fr       Trouvez des scripts utilisateur pour le site Web actuel à partir de référentiels de scripts populaires
// @description:de       Finden Sie Benutzerskripte für die aktuelle Website aus beliebten Skript-Repositories
// @description:ru       Найдите пользовательские скрипты для текущего веб-сайта из популярных репозиториев скриптов
// @author               Pipecraft
// @license              MIT
// @match                *://*/*
// @icon                 data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20viewBox%3D%220%200%2064%2064%22%20fill%3D%22none%22%3E%3Ctext%20x%3D%2232%22%20y%3D%2232%22%20text-anchor%3D%22middle%22%20dominant-baseline%3D%22middle%22%20font-family%3D%22Menlo%2C%20Monaco%2C%20Consolas%2C%20Courier%20New%2C%20monospace%22%20font-size%3D%2242%22%20font-weight%3D%22700%22%20fill%3D%22%231f2937%22%3E%7B%7D%3C/text%3E%3C/svg%3E
// @noframes
// @grant                GM_registerMenuCommand
// @grant                GM_openInTab
// @grant                GM_getValue
// @grant                GM_setValue
// @grant                GM_addStyle
// ==/UserScript==

;(function () {
  'use strict'

  // Configuration constants
  const CONFIG = {
    REPOSITORIES: [
      {
        id: 'greasy_fork',
        name: 'Greasy Fork',
        domainSearchUrl:
          'https://greasyfork.org/scripts/by-site/{domain}?filter_locale=0',
        domainSearchEnabled: true,
        keywordSearchUrl:
          'https://greasyfork.org/scripts?filter_locale=0&q={keyword}',
        keywordSearchEnabled: true,
        icon: '🍴',
      },
      {
        id: 'openuserjs',
        name: 'OpenUserJS',
        keywordSearchUrl: 'https://openuserjs.org/?q={keyword}',
        keywordSearchEnabled: true,
        icon: '📜',
      },
      {
        id: 'scriptcat',
        name: 'ScriptCat',
        domainSearchUrl: 'https://scriptcat.org/search?domain={domain}',
        domainSearchEnabled: true,
        keywordSearchUrl: 'https://scriptcat.org/search?keyword={keyword}',
        keywordSearchEnabled: true,
        icon: '🐱',
      },
      {
        id: 'github',
        name: 'GitHub',
        keywordSearchUrl:
          'https://github.com/search?type=code&q=language%3AJavaScript+%22%3D%3DUserScript%3D%3D%22+{keyword}',
        keywordSearchEnabled: true,
        icon: '🐙',
      },
      {
        id: 'github_gist',
        name: 'GitHub Gist',
        keywordSearchUrl:
          'https://gist.github.com/search?l=JavaScript&q=%22%3D%3DUserScript%3D%3D%22+{keyword}',
        keywordSearchEnabled: true,
        icon: '📝',
      },
      {
        id: 'sleazy_fork',
        name: 'Sleazy Fork',
        domainSearchUrl:
          'https://sleazyfork.org/scripts/by-site/{domain}?filter_locale=0',
        domainSearchEnabled: false,
        keywordSearchUrl:
          'https://sleazyfork.org/scripts?filter_locale=0&q={keyword}',
        keywordSearchEnabled: false,
        icon: '🔞',
      },
    ],
    DEBUG: false, // Set to true for debug logging
    SETTINGS_KEY: 'find_scripts_settings', // Key for storing settings
  }

  // Internationalization support
  const I18N = {
    // Menu text template for domain search: "{icon} Find scripts by domain on {name}"
    menuTemplate: {
      en: '{icon} Find scripts by domain on {name}',
      'zh-CN': '{icon} 在 {name} 上按域名查找脚本',
      'zh-TW': '{icon} 在 {name} 上按域名查找腳本',
      ja: '{icon} {name} でドメインからスクリプトを探す',
      ko: '{icon} {name}에서 도메인으로 스크립트 찾기',
      es: '{icon} Buscar scripts por dominio en {name}',
      fr: '{icon} Trouver des scripts par domaine sur {name}',
      de: '{icon} Skripte nach Domain auf {name} finden',
      ru: '{icon} Найти скрипты по домену на {name}',
    },
    // Menu text template for keyword search: "{icon} Find scripts by keyword on {name}"
    keywordSearchTemplate: {
      en: '{icon} Find scripts by keyword on {name}',
      'zh-CN': '{icon} 在 {name} 上按关键字查找脚本',
      'zh-TW': '{icon} 在 {name} 上按關鍵字查找腳本',
      ja: '{icon} {name} でキーワードからスクリプトを探す',
      ko: '{icon} {name}에서 키워드로 스크립트 찾기',
      es: '{icon} Buscar scripts por palabra clave en {name}',
      fr: '{icon} Trouver des scripts par mot-clé sur {name}',
      de: '{icon} Skripte nach Stichwort auf {name} finden',
      ru: '{icon} Найти скрипты по ключевому слову на {name}',
    },
  }

  /**
   * Detect user's browser language
   * @returns {string} Language code
   */
  function detectLanguage() {
    try {
      // Get browser language
      const browserLang = (
        navigator.language ||
        navigator.userLanguage ||
        'en'
      ).toLowerCase()

      // Match with supported languages
      const supportedLangs = Object.keys(I18N.menuTemplate)

      // Try to find exact match
      if (supportedLangs.includes(browserLang)) {
        return browserLang
      }

      // Try to find language match without region (e.g., 'zh' for 'zh-CN')
      const langBase = browserLang.split('-')[0]
      const matchingLang = supportedLangs.find((lang) =>
        lang.startsWith(langBase + '-')
      )
      if (matchingLang) {
        return matchingLang
      }

      // Default to English
      return 'en'
    } catch (error) {
      debugLog('Error detecting language:', error)
      return 'en' // Fallback to English
    }
  }

  /**
   * Log debug messages if debug mode is enabled
   * @param {string} message - The message to log
   * @param {any} data - Optional data to log
   */
  function debugLog(message, data = null) {
    if (CONFIG.DEBUG) {
      console.log(`[Find Scripts] ${message}`, data || '')
    }
  }

  /**
   * Extract the top-level domain from the current URL
   * @returns {string} The top-level domain
   */
  function extractDomain() {
    try {
      const hostname = window.location.hostname
      // Remove 'www.' if present
      let domain = hostname.replace(/^www\./, '')

      // Extract the top-level domain (e.g., example.com from sub.example.com)
      const parts = domain.split('.')
      if (parts.length > 2) {
        // Handle special cases like co.uk, com.au, etc.
        const secondLevelDomains = [
          'co',
          'com',
          'org',
          'net',
          'edu',
          'gov',
          'mil',
        ]
        const thirdLevelDomain = parts[parts.length - 2]

        if (parts.length > 3 && secondLevelDomains.includes(thirdLevelDomain)) {
          // For domains like sub.example.co.uk
          domain = parts.slice(-3).join('.')
        } else {
          // For domains like sub.example.com
          domain = parts.slice(-2).join('.')
        }
      }

      debugLog('Extracted domain:', domain)
      return domain
    } catch (error) {
      debugLog('Error extracting domain:', error)
      return window.location.hostname // Fallback to full hostname
    }
  }

  /**
   * Get localized menu text based on user's language
   * @param {Object} repo - Repository information
   * @param {string} lang - Language code
   * @param {boolean} isKeywordSearch - Whether this is for keyword search
   * @returns {string} Localized menu text
   */
  function getLocalizedMenuText(repo, lang, isKeywordSearch = false) {
    // Get template for user's language or fallback to English
    const templateKey = isKeywordSearch
      ? 'keywordSearchTemplate'
      : 'menuTemplate'
    const template = I18N[templateKey][lang] || I18N[templateKey]['en']

    // Replace placeholders with actual values
    return template.replace('{icon}', repo.icon).replace('{name}', repo.name)
  }

  /**
   * Register menu commands for each repository
   * @param {string} domain - The extracted domain
   */
  function registerMenuCommands(domain) {
    // Detect user's language
    const userLang = detectLanguage()
    debugLog('Detected user language:', userLang)

    CONFIG.REPOSITORIES.forEach((repo) => {
      // Register domain search menu if domainSearchUrl is defined and enabled
      if (repo.domainSearchUrl && repo.domainSearchEnabled) {
        const url = repo.domainSearchUrl.replace('{domain}', domain)
        const menuText = getLocalizedMenuText(repo, userLang)

        GM_registerMenuCommand(menuText, () => {
          debugLog(`Opening ${repo.name} for domain:`, domain)
          GM_openInTab(url, { active: true, insert: true })
        })
      }

      // Register keyword search menu if keywordSearchUrl is defined and enabled
      if (repo.keywordSearchUrl && repo.keywordSearchEnabled) {
        const keywordUrl = repo.keywordSearchUrl.replace('{keyword}', domain)
        const keywordMenuText = getLocalizedMenuText(repo, userLang, true)

        GM_registerMenuCommand(keywordMenuText, () => {
          debugLog(`Opening ${repo.name} for keyword search:`, domain)
          GM_openInTab(keywordUrl, { active: true, insert: true })
        })
      }
    })
  }

  /**
   * Load settings from GM storage
   */
  function loadSettings() {
    try {
      const savedSettings = GM_getValue(CONFIG.SETTINGS_KEY)
      if (savedSettings) {
        // Update repository enabled states
        CONFIG.REPOSITORIES.forEach((repo) => {
          // Load domain search settings
          if (
            repo.domainSearchUrl &&
            savedSettings[`domain_${repo.id}`] !== undefined
          ) {
            repo.domainSearchEnabled = savedSettings[`domain_${repo.id}`]
          }

          // Load keyword search settings
          if (
            repo.keywordSearchUrl &&
            savedSettings[`keyword_${repo.id}`] !== undefined
          ) {
            repo.keywordSearchEnabled = savedSettings[`keyword_${repo.id}`]
          }
        })
        debugLog('Settings loaded:', savedSettings)
      }
    } catch (error) {
      debugLog('Error loading settings:', error)
    }
  }

  /**
   * Save settings to GM storage
   */
  function saveSettings() {
    try {
      const settings = {}
      CONFIG.REPOSITORIES.forEach((repo) => {
        // Save domain search settings
        if (repo.domainSearchUrl) {
          settings[`domain_${repo.id}`] = repo.domainSearchEnabled
        }

        // Save keyword search settings
        if (repo.keywordSearchUrl) {
          settings[`keyword_${repo.id}`] = repo.keywordSearchEnabled
        }
      })
      GM_setValue(CONFIG.SETTINGS_KEY, settings)
      debugLog('Settings saved:', settings)
    } catch (error) {
      debugLog('Error saving settings:', error)
    }
  }

  /**
   * Create and show settings dialog
   */
  function showSettingsDialog() {
    // Add CSS for the settings dialog
    GM_addStyle(`
      #find-scripts-settings-overlay {
        position: fixed;
        top: 0;
        left: 0;
        width: 100%;
        height: 100%;
        background: rgba(0, 0, 0, 0.5);
        z-index: 9999;
        display: flex;
        justify-content: center;
        align-items: center;
      }
      #find-scripts-settings-dialog {
        background: white;
        border-radius: 8px;
        padding: 20px;
        width: 400px;
        max-width: 90%;
        max-height: 90%;
        overflow-y: auto;
        box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2);
      }
      #find-scripts-settings-dialog h2 {
        margin-top: 0;
        margin-bottom: 15px;
        font-size: 18px;
        border-bottom: 1px solid #eee;
        padding-bottom: 10px;
      }
      .find-scripts-setting-item {
        display: flex;
        align-items: center;
        margin-bottom: 12px;
      }
      .find-scripts-setting-item label {
        margin-left: 8px;
        flex-grow: 1;
      }
      .find-scripts-buttons {
        display: flex;
        justify-content: flex-end;
        margin-top: 15px;
        gap: 10px;
      }
      .find-scripts-buttons button {
        padding: 6px 12px;
        border-radius: 4px;
        border: 1px solid #ccc;
        background: #f5f5f5;
        cursor: pointer;
      }
      .find-scripts-buttons button:hover {
        background: #e5e5e5;
      }
      .find-scripts-buttons button.primary {
        background: #4a86e8;
        color: white;
        border-color: #3a76d8;
      }
      .find-scripts-buttons button.primary:hover {
        background: #3a76d8;
      }
    `)

    // Create overlay and dialog elements
    const overlay = document.createElement('div')
    overlay.id = 'find-scripts-settings-overlay'

    const dialog = document.createElement('div')
    dialog.id = 'find-scripts-settings-dialog'

    // Detect user's language
    const userLang = detectLanguage()

    // Set dialog title based on language
    const titles = {
      en: 'Repository Settings',
      'zh-CN': '仓库设置',
      'zh-TW': '倉庫設置',
      ja: 'リポジトリ設定',
      ko: '저장소 설정',
      es: 'Configuración de repositorios',
      fr: 'Paramètres des dépôts',
      de: 'Repository-Einstellungen',
      ru: 'Настройки репозиториев',
    }

    const saveButtonText = {
      en: 'Save',
      'zh-CN': '保存',
      'zh-TW': '保存',
      ja: '保存',
      ko: '저장',
      es: 'Guardar',
      fr: 'Enregistrer',
      de: 'Speichern',
      ru: 'Сохранить',
    }

    const cancelButtonText = {
      en: 'Cancel',
      'zh-CN': '取消',
      'zh-TW': '取消',
      ja: 'キャンセル',
      ko: '취소',
      es: 'Cancelar',
      fr: 'Annuler',
      de: 'Abbrechen',
      ru: 'Отмена',
    }

    // 添加刷新提示文本
    const refreshNoteText = {
      en: 'Note: Please refresh the page after saving for changes to take effect.',
      'zh-CN': '注意:保存后请刷新页面以使更改生效。',
      'zh-TW': '注意:保存後請刷新頁面以使更改生效。',
      ja: '注意:変更を有効にするには、保存後にページを更新してください。',
      ko: '참고: 변경 사항을 적용하려면 저장 후 페이지를 새로 고침하세요.',
      es: 'Nota: Por favor, actualice la página después de guardar para que los cambios surtan efecto.',
      fr: "Remarque : Veuillez actualiser la page après l'enregistrement pour que les modifications prennent effet.",
      de: 'Hinweis: Bitte aktualisieren Sie die Seite nach dem Speichern, damit die Änderungen wirksam werden.',
      ru: 'Примечание: Пожалуйста, обновите страницу после сохранения, чтобы изменения вступили в силу.',
    }

    // Set dialog content
    dialog.innerHTML = `
      <h2>${titles[userLang] || titles.en}</h2>
      <div id="find-scripts-settings-content"></div>
      <div class="find-scripts-refresh-note" style="margin-top: 15px; color: #e74c3c; font-size: 0.9em;">
        ${refreshNoteText[userLang] || refreshNoteText.en}
      </div>
      <div class="find-scripts-buttons">
        <button id="find-scripts-cancel">${cancelButtonText[userLang] || cancelButtonText.en}</button>
        <button id="find-scripts-save" class="primary">${saveButtonText[userLang] || saveButtonText.en}</button>
      </div>
    `

    // Add repository options
    const content = dialog.querySelector('#find-scripts-settings-content')

    // Add section titles based on language
    const domainSearchTitle = {
      en: 'Domain Search',
      'zh-CN': '域名搜索',
      'zh-TW': '域名搜索',
      ja: 'ドメイン検索',
      ko: '도메인 검색',
      es: 'Búsqueda por dominio',
      fr: 'Recherche par domaine',
      de: 'Domain-Suche',
      ru: 'Поиск по домену',
    }

    const keywordSearchTitle = {
      en: 'Keyword Search',
      'zh-CN': '关键字搜索',
      'zh-TW': '關鍵字搜索',
      ja: 'キーワード検索',
      ko: '키워드 검색',
      es: 'Búsqueda por palabra clave',
      fr: 'Recherche par mot-clé',
      de: 'Stichwortsuche',
      ru: 'Поиск по ключевому слову',
    }

    // Create domain search section
    const domainSection = document.createElement('div')
    domainSection.innerHTML = `<h3>${domainSearchTitle[userLang] || domainSearchTitle.en}</h3>`
    content.appendChild(domainSection)

    // Add domain search options
    CONFIG.REPOSITORIES.forEach((repo) => {
      if (repo.domainSearchUrl) {
        const item = document.createElement('div')
        item.className = 'find-scripts-setting-item'

        const checkbox = document.createElement('input')
        checkbox.type = 'checkbox'
        checkbox.id = `find-scripts-domain-${repo.id}`
        checkbox.checked = repo.domainSearchEnabled

        const label = document.createElement('label')
        label.htmlFor = `find-scripts-domain-${repo.id}`
        label.textContent = `${repo.icon} ${repo.name}`

        item.appendChild(checkbox)
        item.appendChild(label)
        domainSection.appendChild(item)
      }
    })

    // Create keyword search section
    const keywordSection = document.createElement('div')
    keywordSection.innerHTML = `<h3>${keywordSearchTitle[userLang] || keywordSearchTitle.en}</h3>`
    content.appendChild(keywordSection)

    // Add keyword search options
    CONFIG.REPOSITORIES.forEach((repo) => {
      if (repo.keywordSearchUrl) {
        const item = document.createElement('div')
        item.className = 'find-scripts-setting-item'

        const checkbox = document.createElement('input')
        checkbox.type = 'checkbox'
        checkbox.id = `find-scripts-keyword-${repo.id}`
        checkbox.checked = repo.keywordSearchEnabled

        const label = document.createElement('label')
        label.htmlFor = `find-scripts-keyword-${repo.id}`
        label.textContent = `${repo.icon} ${repo.name}`

        item.appendChild(checkbox)
        item.appendChild(label)
        keywordSection.appendChild(item)
      }
    })

    // Add dialog to page
    overlay.appendChild(dialog)
    document.body.appendChild(overlay)

    // Handle save button click
    document
      .getElementById('find-scripts-save')
      .addEventListener('click', () => {
        // Update repository enabled states
        CONFIG.REPOSITORIES.forEach((repo) => {
          // Update domain search enabled state
          if (repo.domainSearchUrl) {
            const domainCheckbox = document.getElementById(
              `find-scripts-domain-${repo.id}`
            )
            if (domainCheckbox) {
              repo.domainSearchEnabled = domainCheckbox.checked
            }
          }

          // Update keyword search enabled state
          if (repo.keywordSearchUrl) {
            const keywordCheckbox = document.getElementById(
              `find-scripts-keyword-${repo.id}`
            )
            if (keywordCheckbox) {
              repo.keywordSearchEnabled = keywordCheckbox.checked
            }
          }
        })

        // Save settings
        saveSettings()

        // Refresh menu commands
        const domain = extractDomain()
        registerMenuCommands(domain)

        // Close dialog
        document.body.removeChild(overlay)
      })

    // Handle cancel button click
    document
      .getElementById('find-scripts-cancel')
      .addEventListener('click', () => {
        document.body.removeChild(overlay)
      })

    // Close when clicking outside the dialog
    overlay.addEventListener('click', (event) => {
      if (event.target === overlay) {
        document.body.removeChild(overlay)
      }
    })
  }

  /**
   * Register settings menu command
   */
  function registerSettingsMenu() {
    const settingsText = {
      en: '⚙️ Settings',
      'zh-CN': '⚙️ 设置',
      'zh-TW': '⚙️ 設置',
      ja: '⚙️ 設定',
      ko: '⚙️ 설정',
      es: '⚙️ Configuración',
      fr: '⚙️ Paramètres',
      de: '⚙️ Einstellungen',
      ru: '⚙️ Настройки',
    }

    const userLang = detectLanguage()
    const menuText = settingsText[userLang] || settingsText.en

    GM_registerMenuCommand(menuText, showSettingsDialog)
  }

  /**
   * Initialize the script
   */
  function initialize() {
    // Load saved settings
    loadSettings()

    // Register menu commands
    const domain = extractDomain()
    registerMenuCommands(domain)
    registerSettingsMenu()

    debugLog('Script initialized for domain:', domain)
  }

  // Initialize the script
  initialize()
})()