根据标题关键字过滤 GreasyFork 脚本,并提供侧边栏管理过滤器,支持导入导出关键字txt文件
// ==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();
}
})();