Greasy Fork is available in English.

Greasy Fork Filter

Filter scripts by installs and keywords — with import/export

スクリプトをインストールするには、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         Greasy Fork Filter
// @namespace    gf-filter
// @version      1.1
// @description  Filter scripts by installs and keywords — with import/export
// @match        https://greasyfork.org/*/scripts*
// @grant        GM_setValue
// @grant        GM_getValue
// @license      MIT
// ==/UserScript==

(function () {
  'use strict';

  // ─── State ────────────────────────────────────────────────────────────────
  const DEFAULTS = { daily: 0, total: 0, keywords: [] };

  const S = {
    daily:    GM_getValue('daily',    DEFAULTS.daily),
    total:    GM_getValue('total',    DEFAULTS.total),
    keywords: GM_getValue('keywords', DEFAULTS.keywords),
    open:     false,
  };

  const save = (k, v) => { S[k] = v; GM_setValue(k, v); };
  const resetAll = () => Object.entries(DEFAULTS).forEach(([k, v]) => save(k, v));

  // ─── Helpers ──────────────────────────────────────────────────────────────
  const debounce = (fn, ms) => {
    let t;
    return (...a) => { clearTimeout(t); t = setTimeout(() => fn(...a), ms); };
  };

  const el = (tag, attrs = {}, ...kids) => {
    const n = Object.assign(document.createElement(tag), attrs);
    kids.forEach(c => n.append(c));
    return n;
  };

  const toast = msg => {
    const t = el('div', { className: 'gf-toast', textContent: msg });
    document.body.append(t);
    setTimeout(() => t.remove(), 2000);
  };

  // ─── CSS ──────────────────────────────────────────────────────────────────
  document.head.append(el('style', { textContent: `
    #gf-fab {
      position: fixed; bottom: 28px; right: 28px; z-index: 99999;
      width: 40px; height: 40px; border-radius: 10px;
      background: #1a1a1a; border: 1px solid rgba(255,255,255,0.08);
      cursor: pointer; display: grid; place-items: center;
      box-shadow: 0 2px 12px rgba(0,0,0,0.3);
      transition: transform .18s, background .18s;
    }
    #gf-fab:hover { transform: scale(1.06); background: #2a2a2a; }
    #gf-fab.active { background: #111; }
    #gf-fab svg { color: #e0e0e0; display: block; pointer-events: none; }

    #gf-panel {
      position: fixed; bottom: 78px; right: 28px; z-index: 99999;
      width: 276px; background: #fff;
      border: 1px solid rgba(0,0,0,0.09); border-radius: 14px;
      box-shadow: 0 12px 40px rgba(0,0,0,0.13), 0 2px 8px rgba(0,0,0,0.05);
      font: 13px/1.45 -apple-system, BlinkMacSystemFont, 'SF Pro Text', system-ui, sans-serif;
      color: #1a1a1a; transform-origin: bottom right;
      transition: transform .22s cubic-bezier(.34,1.56,.64,1), opacity .16s;
    }
    #gf-panel.closed {
      transform: scale(0.86) translateY(10px);
      opacity: 0; pointer-events: none;
    }

    .gf-section { padding: 13px 15px; border-bottom: 1px solid rgba(0,0,0,0.06); }
    .gf-section:last-child { border-bottom: none; }

    .gf-label {
      font-size: 10.5px; font-weight: 600;
      letter-spacing: .06em; text-transform: uppercase;
      color: #999; margin-bottom: 10px;
    }

    .gf-row {
      display: flex; align-items: center;
      justify-content: space-between; gap: 10px; margin-bottom: 7px;
    }
    .gf-row:last-child { margin-bottom: 0; }
    .gf-row-text { font-size: 13px; color: #3a3a3a; flex: 1; }

    .gf-num {
      width: 70px; height: 28px; padding: 0 9px;
      border: 1px solid rgba(0,0,0,0.13); border-radius: 7px;
      background: #f5f5f7; color: #1a1a1a;
      font-size: 13px; text-align: right; outline: none;
      transition: border-color .15s, box-shadow .15s;
      -moz-appearance: textfield;
    }
    .gf-num::-webkit-inner-spin-button { -webkit-appearance: none; }
    .gf-num:focus { border-color: #888; box-shadow: 0 0 0 3px rgba(0,0,0,0.07); }

    .gf-kw-row { display: flex; gap: 6px; margin-bottom: 9px; }
    .gf-kw-input {
      flex: 1; height: 28px; padding: 0 9px;
      border: 1px solid rgba(0,0,0,0.13); border-radius: 7px;
      background: #f5f5f7; color: #1a1a1a;
      font-size: 13px; outline: none;
      transition: border-color .15s, box-shadow .15s;
    }
    .gf-kw-input:focus { border-color: #888; box-shadow: 0 0 0 3px rgba(0,0,0,0.07); }

    .gf-add-btn {
      height: 28px; padding: 0 11px;
      border: 1px solid rgba(0,0,0,0.15); border-radius: 7px;
      background: #1a1a1a; color: #f0f0f0;
      font-size: 12px; font-weight: 500; cursor: pointer;
      transition: background .15s; white-space: nowrap;
    }
    .gf-add-btn:hover { background: #333; }
    .gf-add-btn:disabled {
      background: #e5e5e7; color: #bbb;
      border-color: transparent; cursor: default;
    }

    .gf-tags { display: flex; flex-wrap: wrap; gap: 5px; min-height: 4px; }
    .gf-tag {
      display: inline-flex; align-items: center; gap: 4px;
      padding: 3px 6px 3px 9px; background: #ebebed; color: #2a2a2a;
      border-radius: 20px; font-size: 11.5px; font-weight: 500;
      animation: gf-pop .14s ease;
    }
    @keyframes gf-pop {
      from { transform: scale(0.72); opacity: 0; }
      to   { transform: scale(1); opacity: 1; }
    }

    .gf-tag-x {
      display: grid; place-items: center;
      width: 15px; height: 15px; border-radius: 50%;
      background: rgba(0,0,0,0.1); border: none;
      cursor: pointer; color: inherit;
      font-size: 11px; line-height: 1; padding: 0;
      transition: background .12s;
    }
    .gf-tag-x:hover { background: #e63946; color: #fff; }

    .gf-io-row { display: flex; gap: 6px; margin-top: 11px; }
    .gf-io-btn {
      flex: 1; height: 28px; border-radius: 7px;
      border: 1px solid rgba(0,0,0,0.13);
      background: #f5f5f7; color: #3a3a3a;
      font-size: 12px; font-weight: 500; cursor: pointer;
      transition: background .15s, border-color .15s;
    }
    .gf-io-btn:hover { background: #e5e5e7; border-color: rgba(0,0,0,0.2); }

    .gf-toast {
      position: fixed; bottom: 78px; right: 28px; z-index: 100000;
      padding: 8px 14px; border-radius: 8px;
      background: #1a1a1a; color: #f0f0f0;
      font: 12px/1 -apple-system, system-ui, sans-serif;
      pointer-events: none;
      animation: gf-toast-in .2s ease, gf-toast-out .25s ease 1.6s forwards;
    }
    @keyframes gf-toast-in  { from { opacity:0; transform:translateY(6px); } to { opacity:1; transform:none; } }
    @keyframes gf-toast-out { to   { opacity:0; transform:translateY(4px); } }

    .gf-footer {
      padding: 9px 15px; display: flex;
      align-items: center; justify-content: space-between;
    }
    .gf-stat { font-size: 12px; color: #999; }
    .gf-stat b { color: #1a1a1a; font-weight: 600; }

    .gf-clear {
      font-size: 12px; color: #e63946; background: none;
      border: none; cursor: pointer; padding: 0;
      opacity: .75; transition: opacity .12s;
    }
    .gf-clear:hover { opacity: 1; }

    @media (prefers-color-scheme: dark) {
      #gf-panel {
        background: #1c1c1e; border-color: rgba(255,255,255,0.09);
        color: #f0f0f2; box-shadow: 0 12px 40px rgba(0,0,0,0.5);
      }
      .gf-section   { border-bottom-color: rgba(255,255,255,0.06); }
      .gf-label      { color: #555; }
      .gf-row-text   { color: #bbb; }
      .gf-num,
      .gf-kw-input   { background: #2c2c2e; border-color: rgba(255,255,255,0.1); color: #f0f0f2; }
      .gf-num:focus,
      .gf-kw-input:focus { border-color: #666; box-shadow: 0 0 0 3px rgba(255,255,255,0.06); }
      .gf-add-btn    { background: #3a3a3c; border-color: rgba(255,255,255,0.08); color: #e0e0e0; }
      .gf-add-btn:hover    { background: #4a4a4c; }
      .gf-add-btn:disabled { background: #2c2c2e; color: #555; }
      .gf-tag        { background: #3a3a3c; color: #ddd; }
      .gf-tag-x      { background: rgba(255,255,255,0.1); }
      .gf-io-btn     { background: #2c2c2e; border-color: rgba(255,255,255,0.1); color: #bbb; }
      .gf-io-btn:hover { background: #3a3a3c; }
      .gf-stat       { color: #555; }
      .gf-stat b     { color: #ccc; }
    }
  ` }));

  // ─── Filter ───────────────────────────────────────────────────────────────
  let shown = 0, hidden = 0;

  const applyFilter = debounce(() => {
    shown = 0; hidden = 0;
    const kws = S.keywords.map(k => k.toLowerCase());

    document.querySelectorAll('#browse-script-list > li[data-script-id]').forEach(li => {
      const daily    = +li.dataset.scriptDailyInstalls || 0;
      const total    = +li.dataset.scriptTotalInstalls || 0;
      const name     = (li.dataset.scriptName || '').toLowerCase();
      const desc     = (li.querySelector('.script-description')?.textContent || '').toLowerCase();
      const haystack = name + ' ' + desc;

      const filtered = daily < S.daily
                    || total < S.total
                    || kws.some(k => haystack.includes(k));

      li.style.display = filtered ? 'none' : '';
      filtered ? hidden++ : shown++;
    });

    updateStat();
  }, 120);

  // ─── Tag‑list builder ────────────────────────────────────────────────────
  const makeTagSection = (listKey, placeholder) => {
    const tagsEl = el('div', { className: 'gf-tags' });
    const input  = el('input', { className: 'gf-kw-input', type: 'text', placeholder });
    const addBtn = el('button', { className: 'gf-add-btn', textContent: 'Block', disabled: true });

    const render = () => {
      tagsEl.innerHTML = '';
      S[listKey].forEach(val => {
        const x = el('button', { className: 'gf-tag-x', textContent: '×' });
        x.addEventListener('click', e => {
          e.stopPropagation();              // prevent panel‑close handler
          save(listKey, S[listKey].filter(v => v !== val));
          render();
          applyFilter();
        });
        tagsEl.append(el('span', { className: 'gf-tag' }, val, x));
      });
    };

    const add = () => {
      const phrase = input.value.trim().toLowerCase();
      if (!phrase || S[listKey].includes(phrase)) {
        input.value = ''; addBtn.disabled = true; return;
      }
      save(listKey, [...S[listKey], phrase]);
      input.value = ''; addBtn.disabled = true;
      render();
      applyFilter();
    };

    input.addEventListener('input',   () => { addBtn.disabled = !input.value.trim(); });
    input.addEventListener('keydown', e => e.key === 'Enter' && add());
    addBtn.addEventListener('click',  add);

    return {
      tagsEl, input, addBtn, render,
      row: el('div', { className: 'gf-kw-row' }, input, addBtn),
    };
  };

  // ─── Import / Export ──────────────────────────────────────────────────────
  const exportSettings = () => {
    const json = JSON.stringify(
      { daily: S.daily, total: S.total, keywords: S.keywords }, null, 2
    );
    const a = el('a', {
      href:     'data:application/json;charset=utf-8,' + encodeURIComponent(json),
      download: 'gf-filter-settings.json',
    });
    document.body.append(a); a.click(); a.remove();
    toast('Settings exported');
  };

  const importSettings = () => {
    const fi = el('input', { type: 'file', accept: '.json', style: 'display:none' });
    fi.addEventListener('change', () => {
      const file = fi.files[0]; fi.remove();
      if (!file) return;
      const reader = new FileReader();
      reader.onload = e => {
        try {
          const d = JSON.parse(e.target.result);
          if (typeof d !== 'object' || d === null) throw 0;
          if (typeof d.daily === 'number')  save('daily', d.daily);
          if (typeof d.total === 'number')  save('total', d.total);
          if (Array.isArray(d.keywords))    save('keywords', d.keywords.map(k => String(k).toLowerCase()));
          syncUI();
          applyFilter();
          toast('Settings imported');
        } catch { toast('Invalid JSON file'); }
      };
      reader.readAsText(file);
    });
    document.body.append(fi); fi.click();
  };

  // ─── Build UI ─────────────────────────────────────────────────────────────
  const kwSection = makeTagSection('keywords', 'e.g. "mod menu", cheat…');
  const statEl    = el('span', { className: 'gf-stat' });
  const clearBtn  = el('button', { className: 'gf-clear', textContent: 'Reset all' });

  const updateStat = () => {
    statEl.innerHTML = `<b>${shown}</b> shown &nbsp;·&nbsp; <b>${hidden}</b> hidden`;
  };

  const makeNumRow = (label, key) => {
    const input = el('input', {
      className: 'gf-num', type: 'number', min: 0, value: S[key],
    });
    input.dataset.key = key;
    input.addEventListener('input', () => { save(key, +input.value || 0); applyFilter(); });
    return el('div', { className: 'gf-row' },
      el('span', { className: 'gf-row-text', textContent: label }), input
    );
  };

  const panel = el('div', { id: 'gf-panel', className: 'closed' },
    el('div', { className: 'gf-section' },
      el('div', { className: 'gf-label', textContent: 'Install threshold' }),
      makeNumRow('Daily installs ≥', 'daily'),
      makeNumRow('Total installs ≥', 'total'),
    ),
    el('div', { className: 'gf-section' },
      el('div', { className: 'gf-label', textContent: 'Block keywords' }),
      kwSection.row,
      kwSection.tagsEl,
    ),
    el('div', { className: 'gf-section' },
      el('div', { className: 'gf-label', textContent: 'Settings' }),
      el('div', { className: 'gf-io-row' },
        Object.assign(el('button', { className: 'gf-io-btn', textContent: 'Export JSON' }),
          { onclick: exportSettings }),
        Object.assign(el('button', { className: 'gf-io-btn', textContent: 'Import JSON' }),
          { onclick: importSettings }),
      ),
    ),
    el('div', { className: 'gf-footer' }, statEl, clearBtn),
  );

  // syncUI — defined after panel so querySelectorAll works at call‑time
  const syncUI = () => {
    panel.querySelectorAll('.gf-num').forEach(i => {
      const k = i.dataset.key;
      if (k) i.value = S[k];
    });
    kwSection.input.value = '';
    kwSection.addBtn.disabled = true;
    kwSection.render();
  };

  clearBtn.addEventListener('click', () => { resetAll(); syncUI(); applyFilter(); });

  const fab = el('button', { id: 'gf-fab', title: 'Filter scripts', innerHTML:
    `<svg width="16" height="16" viewBox="0 0 16 16" fill="none"
          stroke="currentColor" stroke-width="1.6" stroke-linecap="round">
       <path d="M2 4h12"/><path d="M4.5 8h7"/><path d="M7 12h2"/>
     </svg>` });

  document.body.append(fab, panel);

  // ─── Toggle ───────────────────────────────────────────────────────────────
  const toggle = force => {
    S.open = force ?? !S.open;
    panel.classList.toggle('closed', !S.open);
    fab.classList.toggle('active', S.open);
  };

  fab.addEventListener('click', e => { e.stopPropagation(); toggle(); });
  document.addEventListener('click',   e => S.open && !panel.contains(e.target) && toggle(false));
  document.addEventListener('keydown', e => e.key === 'Escape' && toggle(false));

  // ─── Init ─────────────────────────────────────────────────────────────────
  kwSection.render();
  applyFilter();

  const list = document.getElementById('browse-script-list');
  if (list) new MutationObserver(applyFilter).observe(list, { childList: true });
})();