GreasyFork 脚本过滤器

根据标题关键字过滤 GreasyFork 脚本,并提供侧边栏管理过滤器,支持导入导出关键字txt文件

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         GreasyFork 脚本过滤器
// @namespace    http://tampermonkey.net/
// @version      1.2
// @description  根据标题关键字过滤 GreasyFork 脚本,并提供侧边栏管理过滤器,支持导入导出关键字txt文件
// @author       Yourname
// @match        https://greasyfork.org/zh-CN/scripts*
// @grant        none
// ==/UserScript==

(function () {
  'use strict';

  // 等待 DOM 元素加载完成
  function waitForElement(selector, callback) {
    const el = document.querySelector(selector);
    if (el) return callback(el);
    const observer = new MutationObserver(() => {
      const el = document.querySelector(selector);
      if (el) {
        observer.disconnect();
        callback(el);
      }
    });
    observer.observe(document.body, { childList: true, subtree: true });
  }

  waitForElement('.script-list', initFilterUI);

  function initFilterUI() {
    const savedKeywords = JSON.parse(localStorage.getItem('gf_filter_keywords') || '[]');
    let keywords = savedKeywords;

    const sidebar = document.createElement('div');
    sidebar.id = 'gf-filter-sidebar';
    sidebar.innerHTML = `
  <h2>关键词过滤器</h2>
  <div id="input-container">
    <input id="new-keyword" type="text" placeholder="添加关键字">
    <label style="font-size: 12px; margin-left: 6px;">
      <input type="checkbox" id="case-sensitive"> 区分大小写
    </label>
    <button id="add-btn">添加</button>
  </div>
  <ul id="keyword-list"></ul>
  <div id="bottom-buttons" style="display: flex; justify-content: space-between; align-items: center; margin-top: 10px; gap: 10px; flex-wrap: wrap;">
    <div style="display: flex; gap: 10px;">
      <button id="delete-selected-btn" style="background: #d9534f; color: white; border-radius: 4px; padding: 6px 10px; border: none; cursor: pointer;">删除已选中</button>
      <button id="delete-all-btn" style="background: #c9302c; color: white; border-radius: 4px; padding: 6px 10px; border: none; cursor: pointer;">删除所有</button>
    </div>
    <div style="display: flex; gap: 10px;">
      <button id="export-btn" style="background: #007bff; color: white; border: none; border-radius: 4px; padding: 6px 10px; cursor: pointer;">导出关键字</button>
      <button id="import-btn" style="background: #28a745; color: white; border: none; border-radius: 4px; padding: 6px 10px; cursor: pointer;">导入关键字</button>
      <input type="file" id="import-file" accept=".txt" style="display:none">
    </div>
  </div>
`;
      // 添加“删除所有关键字”按钮事件
sidebar.querySelector('#delete-all-btn').addEventListener('click', () => {
  if (confirm('确定要删除所有关键字吗?此操作不可撤销!')) {
    keywords = [];
    saveKeywords();
    renderKeywords();
    applyFilter();
  }
});
    document.body.appendChild(sidebar);

    const toggleBtn = document.createElement('div');
    toggleBtn.id = 'gf-filter-toggle';
    toggleBtn.innerText = '☰';
    document.body.appendChild(toggleBtn);

    const style = document.createElement('style');
    style.textContent = `
      #gf-filter-sidebar {
        position: fixed;
        top: 0;
        left: 0;
        height: 100%;
        width: 220px;
        max-width: 80vw;
        background: #ffffff;
        box-shadow: 2px 0 8px rgba(0,0,0,0.2);
        padding: 16px;
        transform: translateX(-100%);
        transition: transform 0.3s ease;
        z-index: 9999;
        font-family: sans-serif;
        overflow-y: auto;
      }
      #gf-filter-sidebar.open {
        transform: translateX(0);
      }
      #gf-filter-toggle {
        position: fixed;
        top: 900px;
        left: 0;
        width: 40px;
        height: 40px;
        background: #007acc;
        color: white;
        font-size: 20px;
        display: flex;
        justify-content: center;
        align-items: center;
        border-top-right-radius: 8px;
        border-bottom-right-radius: 8px;
        box-shadow: 2px 2px 8px rgba(0,0,0,0.3);
        cursor: pointer;
        z-index: 10000;
        transition: background 0.3s;
      }
      #gf-filter-toggle:hover {
        background: #005fa3;
      }
      #input-container {
        position: sticky;
        top: 0;
        background: white;
        padding-bottom: 10px;
        z-index: 1;
        display: flex;
        flex-wrap: wrap;
        gap: 6px;
        align-items: center;
        border-bottom: 1px solid #ddd;
        margin-bottom: 10px;
      }
      #input-container input[type="text"] {
        flex: 1;
        padding: 4px 6px;
        border: 1px solid #ccc;
        border-radius: 4px;
      }
      #input-container button {
        padding: 4px 8px;
        background: #28a745;
        border: none;
        color: white;
        border-radius: 4px;
        cursor: pointer;
      }
      #keyword-list {
        list-style: none;
        padding: 0;
        margin: 0;
        max-height: calc(100vh - 260px);
        overflow-y: auto;
      }
      #keyword-list li {
        display: flex;
        align-items: center;
        gap: 6px;
        padding: 4px 0;
        border-bottom: 1px dashed #ddd;
        font-size: 14px;
      }
      #keyword-list .remove-btn {
        margin-left: auto;
        background: transparent;
        border: none;
        color: #d00;
        cursor: pointer;
        font-weight: bold;
      }
    `;
    document.head.appendChild(style);

    function renderKeywords() {
      const list = sidebar.querySelector('#keyword-list');
      list.innerHTML = '';
      keywords.forEach((keyword, index) => {
        const li = document.createElement('li');
        li.innerHTML = `
          <input type="checkbox" class="keyword-checkbox" data-index="${index}">
          <span class="keyword-text" title="${keyword.text}">${keyword.text}${keyword.caseSensitive ? ' (区分大小写)' : ''}</span>
          <button class="remove-btn" data-index="${index}">✕</button>
        `;
        list.appendChild(li);
      });

      list.querySelectorAll('.remove-btn').forEach(btn => {
        btn.addEventListener('click', () => {
          const index = Number(btn.dataset.index);
          keywords.splice(index, 1);
          saveKeywords();
          renderKeywords();
          applyFilter();
        });
      });
    }

    function saveKeywords() {
      localStorage.setItem('gf_filter_keywords', JSON.stringify(keywords));
    }

    function applyFilter() {
      const items = document.querySelectorAll('.script-list li');
      items.forEach(item => {
        const titleEl = item.querySelector('h2 a');
        if (!titleEl) return;
        const title = titleEl.textContent;
        const hidden = keywords.some(k => {
          return k.caseSensitive
            ? title.includes(k.text)
            : title.toLowerCase().includes(k.text.toLowerCase());
        });
        item.style.display = hidden ? 'none' : '';
      });
    }

    sidebar.querySelector('#add-btn').addEventListener('click', () => {
      const input = sidebar.querySelector('#new-keyword');
      const checkbox = sidebar.querySelector('#case-sensitive');
      const text = input.value.trim();
      const caseSensitive = checkbox.checked;
      if (text && !keywords.some(k => k.text === text && k.caseSensitive === caseSensitive)) {
        keywords.push({ text, caseSensitive });
        saveKeywords();
        renderKeywords();
        applyFilter();
        input.value = '';
        checkbox.checked = false;
      }
    });

    sidebar.querySelector('#new-keyword').addEventListener('keydown', e => {
      if (e.key === 'Enter') {
        sidebar.querySelector('#add-btn').click();
      }
    });

    sidebar.querySelector('#delete-selected-btn').addEventListener('click', () => {
      const checkedBoxes = sidebar.querySelectorAll('.keyword-checkbox:checked');
      if (checkedBoxes.length === 0) return;
      const toDeleteIndices = Array.from(checkedBoxes).map(box => Number(box.dataset.index));
      toDeleteIndices.sort((a, b) => b - a).forEach(i => keywords.splice(i, 1));
      saveKeywords();
      renderKeywords();
      applyFilter();
    });

    // 导出关键字
    sidebar.querySelector('#export-btn').addEventListener('click', () => {
      if (keywords.length === 0) {
        alert('没有关键字可导出!');
        return;
      }
      // 每行:关键字\t区分大小写(true/false)
      const content = keywords.map(k => `${k.text}\t${k.caseSensitive}`).join('\n');
      const blob = new Blob([content], { type: 'text/plain;charset=utf-8' });
      const url = URL.createObjectURL(blob);

      const a = document.createElement('a');
      a.href = url;
      a.download = 'gf_filter_keywords.txt';
      a.click();
      URL.revokeObjectURL(url);
    });

    // 导入关键字
    const importFileInput = sidebar.querySelector('#import-file');
    sidebar.querySelector('#import-btn').addEventListener('click', () => {
      importFileInput.value = null;
      importFileInput.click();
    });

    importFileInput.addEventListener('change', (e) => {
      const file = e.target.files[0];
      if (!file) return;
      const reader = new FileReader();
      reader.onload = (ev) => {
        const text = ev.target.result;
        // 按行解析,每行格式:关键字\t区分大小写(true/false)
        const lines = text.split(/\r?\n/);
        let addedCount = 0;
        lines.forEach(line => {
          const [word, cs] = line.split('\t');
          if (word && cs) {
            const caseSensitive = cs.trim().toLowerCase() === 'true';
            if (!keywords.some(k => k.text === word && k.caseSensitive === caseSensitive)) {
              keywords.push({ text: word, caseSensitive });
              addedCount++;
            }
          }
        });
        if (addedCount > 0) {
          saveKeywords();
          renderKeywords();
          applyFilter();
          alert(`成功导入 ${addedCount} 条关键字`);
        } else {
          alert('没有导入任何新关键字');
        }
      };
      reader.readAsText(file);
    });

    toggleBtn.addEventListener('click', e => {
      e.stopPropagation();
      sidebar.classList.toggle('open');
    });

    document.addEventListener('click', e => {
      if (!sidebar.contains(e.target) && !toggleBtn.contains(e.target)) {
        sidebar.classList.remove('open');
      }
    });

    renderKeywords();
    applyFilter();
  }
})();