検索結果などから記事をフィルタリングします。
// ==UserScript==
// @name note.com Filter
// @name:ja note.com Filter
// @namespace http://tampermonkey.net/
// @version 4.2
// @description 検索結果などから記事をフィルタリングします。
// @description:ja 検索結果などから記事をフィルタリングします。
// @author Gemini
// @match https://note.com/*
// @match https://*.note.jp/*
// @match https://*.nexyzgroup.jp/*
// @grant none
// @license MIT
// ==/UserScript==
(function() {
'use strict';
let blockedUsers = JSON.parse(localStorage.getItem('note_blocked_users_v2') || '[]');
let mutedKeywords = JSON.parse(localStorage.getItem('note_muted_keywords') || '[]');
let minLikes = parseInt(localStorage.getItem('note_min_likes') || '0');
const style = document.createElement('style');
style.innerHTML = `
.block-btn {
position: absolute !important; top: -2px !important; right: -2px !important;
width: 22px !important; height: 22px !important; line-height: 18px !important;
background: #ff4d4f !important; color: white !important; border-radius: 50% !important;
text-align: center !important; font-size: 14px !important; cursor: pointer !important;
z-index: 9999 !important; border: 2px solid white !important; font-weight: bold !important;
opacity: 0; transition: opacity 0.2s; pointer-events: none;
display: flex !important; align-items: center !important; justify-content: center !important;
}
.m-avatar:hover .block-btn, .m-avatar__link:hover .block-btn,
[class*="avatar"]:hover .block-btn, [data-name="avatar"]:hover .block-btn {
opacity: 1 !important; pointer-events: auto !important;
}
#filter-mgr-btn { position: fixed; bottom: 20px; right: 20px; z-index: 10000; background: #222; color: white; padding: 8px 16px; border-radius: 4px; cursor: pointer; font-size: 12px; border: 1px solid #444; }
#filter-panel { position: fixed; bottom: 65px; right: 20px; width: 300px; max-height: 550px; background: white; border-radius: 8px; z-index: 10000; display: none; flex-direction: column; box-shadow: 0 8px 24px rgba(0,0,0,0.2); border: 1px solid #eee; font-family: sans-serif; color: #333; }
.tabs { display: flex; background: #f0f0f0; border-bottom: 1px solid #eee; border-radius: 8px 8px 0 0; flex-shrink: 0; }
.tab-btn { flex: 1; padding: 10px; text-align: center; cursor: pointer; font-size: 11px; font-weight: bold; color: #666; border-radius: 8px 8px 0 0; }
.tab-btn.active { background: white; color: #0076df; border-top: 2px solid #0076df; }
.p-content { padding: 12px; flex-grow: 1; overflow-y: auto; display: none; }
.p-content.active { display: block; }
.list-row { display: flex; justify-content: space-between; align-items: center; padding: 6px 0; border-bottom: 1px solid #eee; font-size: 12px; }
.del-x { color: #ff4d4f; cursor: pointer; font-weight: bold; padding: 0 10px; font-size: 16px; }
.input-box { width: 100%; padding: 6px; border: 1px solid #ccc; border-radius: 4px; font-size: 12px; box-sizing: border-box; }
.btn-main { background: #0076df; color: white; border: none; padding: 8px; border-radius: 4px; cursor: pointer; width: 100%; margin-top: 8px; font-size: 12px; }
`;
document.head.appendChild(style);
const ui = document.createElement('div');
ui.id = 'filter-panel';
ui.innerHTML = `
<div class="tabs">
<div class="tab-btn active" data-tab="config">設定</div>
<div class="tab-btn" data-tab="users">除外リスト</div>
<div class="tab-btn" data-tab="words">単語</div>
</div>
<div id="c-config" class="p-content active">
<label style="font-size:11px; font-weight:bold;">最小スキ数</label>
<input type="number" id="min-likes-in" value="${minLikes}" class="input-box" min="0">
<div style="margin-top:20px; border-top:1px dashed #ccc; padding-top:10px;">
<button id="exp-btn" style="font-size:10px; cursor:pointer;">エクスポート</button>
<button id="imp-btn" style="font-size:10px; cursor:pointer;">インポート</button>
</div>
</div>
<div id="c-users" class="p-content"></div>
<div id="c-words" class="p-content">
<input type="text" id="new-word" placeholder="除外単語を追加" class="input-box">
<button id="add-word-btn" class="btn-main">追加</button>
<div id="word-list" style="margin-top:10px;"></div>
</div>
<div style="text-align:right; padding:8px; border-top:1px solid #eee;"><span id="close-ui" style="cursor:pointer; font-size:11px; color:#999;">閉じる</span></div>
`;
document.body.appendChild(ui);
const toggle = document.createElement('div');
toggle.id = 'filter-mgr-btn'; toggle.innerText = 'Filter Settings'; document.body.appendChild(toggle);
function applyFilter() {
const articles = document.querySelectorAll('.o-searchNoteItem, article:not(.p-article__body), .o-noteCardV3, .m-largeNoteWrapper, .m-timelineItemWrapper__itemWrapper');
articles.forEach(art => {
if (art.closest('#filter-panel')) return;
let userId = "";
let userName = "Unknown";
const uLink = art.querySelector('a[href*="/n/"], a[class*="avatar"], a[class*="user"]');
if (uLink) {
const h = uLink.getAttribute('href');
if (h.includes('note.com/')) {
userId = h.split('note.com/')[1].split('/')[0].split('?')[0];
} else if (h.startsWith('https://note.')) {
userId = new URL(h).hostname;
} else {
const p = h.split('/').filter(x => x);
userId = p[0] === 'n' ? p[1] : p[0];
}
}
if (userId && !['recommend', 'n', 'hashtag', 'search', 'topics'].includes(userId)) {
const nameEl = art.querySelector('[class*="userName"], [class*="creatorName"], .m-avatar__name');
userName = nameEl ? nameEl.innerText.trim() : "Unknown";
if (blockedUsers.some(u => u.id === userId)) {
art.style.setProperty('display', 'none', 'important');
return;
}
}
let likes = 0;
const lEl = art.querySelector('.o-noteLikeV3__count, .m-noteAction__label, .pl-2, [class*="likeCount"]');
if (lEl) likes = parseInt(lEl.innerText.replace(/[^0-9]/g, '')) || 0;
const hasWord = mutedKeywords.some(kw => art.innerText.includes(kw));
if ((minLikes > 0 && likes < minLikes) || hasWord) {
art.style.setProperty('display', 'none', 'important');
} else {
art.style.removeProperty('display');
}
const avatarWrap = art.querySelector('.m-avatar, .m-avatar__link, [class*="avatar"]');
if (avatarWrap && !avatarWrap.querySelector('.block-btn') && userId) {
const b = document.createElement('span');
b.className = 'block-btn'; b.innerText = '×';
b.onclick = (e) => {
e.preventDefault(); e.stopPropagation();
if (!blockedUsers.some(u => u.id === userId)) {
blockedUsers.push({ id: userId, name: userName });
localStorage.setItem('note_blocked_users_v2', JSON.stringify(blockedUsers));
applyFilter();
}
};
if (getComputedStyle(avatarWrap).position === 'static') avatarWrap.style.position = 'relative';
avatarWrap.appendChild(b);
}
});
}
toggle.onclick = () => ui.style.display = ui.style.display === 'none' ? 'flex' : 'none';
document.getElementById('close-ui').onclick = () => ui.style.display = 'none';
ui.querySelectorAll('.tab-btn').forEach(b => {
b.onclick = () => {
ui.querySelectorAll('.tab-btn').forEach(btn => btn.classList.remove('active'));
ui.querySelectorAll('.p-content').forEach(content => content.classList.remove('active'));
b.classList.add('active');
const target = document.getElementById('c-' + b.dataset.tab);
target.classList.add('active');
if (b.dataset.tab === 'users') renderUsers();
if (b.dataset.tab === 'words') renderWords();
};
});
document.getElementById('min-likes-in').oninput = (e) => {
minLikes = parseInt(e.target.value) || 0;
localStorage.setItem('note_min_likes', minLikes);
applyFilter();
};
function renderUsers() {
const c = document.getElementById('c-users');
c.innerHTML = blockedUsers.length ? '' : '<div style="color:#999; font-size:11px;">リストは空です</div>';
blockedUsers.forEach(u => {
const r = document.createElement('div'); r.className = 'list-row';
r.innerHTML = `<div><b>${u.name}</b><br><small style="color:#999">@${u.id}</small></div><span class="del-x">×</span>`;
r.querySelector('.del-x').onclick = () => {
blockedUsers = blockedUsers.filter(x => x.id !== u.id);
localStorage.setItem('note_blocked_users_v2', JSON.stringify(blockedUsers));
renderUsers(); applyFilter();
};
c.appendChild(r);
});
}
function renderWords() {
const list = document.getElementById('word-list');
list.innerHTML = '';
mutedKeywords.forEach(kw => {
const r = document.createElement('div'); r.className = 'list-row';
r.innerHTML = `<span>${kw}</span><span class="del-x">×</span>`;
r.querySelector('.del-x').onclick = () => {
mutedKeywords = mutedKeywords.filter(x => x !== kw);
localStorage.setItem('note_muted_keywords', JSON.stringify(mutedKeywords));
renderWords(); applyFilter();
};
list.appendChild(r);
});
}
document.getElementById('add-word-btn').onclick = () => {
const inp = document.getElementById('new-word');
const val = inp.value.trim();
if (val && !mutedKeywords.includes(val)) {
mutedKeywords.push(val);
localStorage.setItem('note_muted_keywords', JSON.stringify(mutedKeywords));
inp.value = ''; renderWords(); applyFilter();
}
};
document.getElementById('exp-btn').onclick = () => {
const blob = new Blob([JSON.stringify({blockedUsers, mutedKeywords, minLikes})], {type: 'application/json'});
const a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = 'note_filter.json'; a.click();
};
document.getElementById('imp-btn').onclick = () => {
const i = document.createElement('input'); i.type = 'file';
i.onchange = e => {
const f = e.target.files[0];
const r = new FileReader();
r.onload = ev => {
const d = JSON.parse(ev.target.result);
blockedUsers = d.blockedUsers || []; mutedKeywords = d.mutedKeywords || []; minLikes = d.minLikes || 0;
localStorage.setItem('note_blocked_users_v2', JSON.stringify(blockedUsers));
localStorage.setItem('note_muted_keywords', JSON.stringify(mutedKeywords));
localStorage.setItem('note_min_likes', minLikes);
location.reload();
};
r.readAsText(f);
};
i.click();
};
const obs = new MutationObserver(applyFilter);
obs.observe(document.body, { childList: true, subtree: true });
applyFilter();
})();