守护你的眼睛,远离你不喜欢的任何用户,你有权选择你想看见的东西。
// ==UserScript==
// @name LinuxDo Sight shield
// @namespace https://github.com/Ooxygen7
// @version 1.1.0
// @description 守护你的眼睛,远离你不喜欢的任何用户,你有权选择你想看见的东西。
// @author -
// @match https://linux.do/*
// @match https://www.linux.do/*
// @run-at document-start
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_registerMenuCommand
// @grant GM_addValueChangeListener
// @license MIT
// @noframes
// ==/UserScript==
(function () {
'use strict';
const STORE_KEY = 'ldub_blocked_users';
const CFG_KEY = 'ldub_config';
const HIDDEN = 'ldub-hidden';
const STYLE_ID = 'ldub-style';
let blockedList = loadList();
let blockedSet = new Set(blockedList.map(normalize));
let cfg = Object.assign({ enabled: true, showCardButton: true }, loadConfig());
cfg.topicMode = 'op';
cfg.hideBoosts = true;
function loadList() {
try {
const value = JSON.parse(GM_getValue(STORE_KEY, '[]'));
return Array.isArray(value) ? value : [];
} catch (_) {
return [];
}
}
function loadConfig() {
try {
return JSON.parse(GM_getValue(CFG_KEY, '{}')) || {};
} catch (_) {
return {};
}
}
function saveList() {
GM_setValue(STORE_KEY, JSON.stringify(blockedList));
blockedSet = new Set(blockedList.map(normalize));
}
function saveConfig() {
GM_setValue(CFG_KEY, JSON.stringify(cfg));
}
function normalize(value) {
return String(value || '').trim().replace(/^@+/, '').toLowerCase();
}
function isBlocked(username) {
const normalized = normalize(username);
return normalized !== '' && blockedSet.has(normalized);
}
function usernameFromHref(href) {
const match = String(href || '').match(/\/u\/([^/?#]+)/i);
return match ? decodeURIComponent(match[1]) : null;
}
function usernameOf(element) {
if (!element || element.nodeType !== Node.ELEMENT_NODE) return null;
return element.getAttribute('data-user-card')
|| element.getAttribute('data-username')
|| usernameFromHref(element.getAttribute('href'));
}
function hasBlockedUser(container) {
if (!container) return false;
for (const element of container.querySelectorAll('[data-user-card], [data-username], a[href*="/u/"]')) {
if (isBlocked(usernameOf(element))) return true;
}
return false;
}
function collect(root, selector) {
if (!root || root.nodeType !== Node.ELEMENT_NODE && root.nodeType !== Node.DOCUMENT_NODE) return [];
const nodes = [];
if (root.nodeType === Node.ELEMENT_NODE && root.matches(selector)) nodes.push(root);
nodes.push(...root.querySelectorAll(selector));
return nodes;
}
function hide(element) {
if (!element || element.nodeType !== Node.ELEMENT_NODE) return;
element.classList.add(HIDDEN);
element.hidden = true;
element.style.setProperty('display', 'none', 'important');
}
function unhideAll() {
document.querySelectorAll('.' + HIDDEN).forEach(element => {
element.classList.remove(HIDDEN);
element.hidden = false;
element.style.removeProperty('display');
});
}
function postAuthor(post) {
return post.querySelector(
'.topic-avatar a.main-avatar[data-user-card], ' +
'.post-avatar a.main-avatar[data-user-card], ' +
'.topic-meta-data .names a[data-user-card], ' +
'.names a[data-user-card]'
);
}
function scanPosts(root) {
for (const post of collect(root, 'article[data-post-id]')) {
const author = postAuthor(post);
if (isBlocked(usernameOf(author))) hide(post.closest('.topic-post') || post);
}
}
function scanOpenedTopic(root) {
for (const owner of collect(root, '.topic-post.post--topic-owner')) {
const author = owner.querySelector('a.main-avatar[data-user-card]');
if (!isBlocked(usernameOf(author))) continue;
const topic = owner.closest('.regular.ember-view');
hide(topic || owner.closest('#topic') || owner);
}
}
function posterLinks(row) {
const scoped = row.querySelectorAll(
'.posters a[data-user-card], .topic-list-data.posters a[data-user-card], td.posters a[data-user-card]'
);
return scoped.length ? Array.from(scoped) : Array.from(row.querySelectorAll('a[data-user-card]'));
}
function scanTopicRows(root) {
for (const row of collect(root, '.topic-list-item, .latest-topic-list-item')) {
const posters = posterLinks(row);
if (!posters.length) continue;
if (isBlocked(usernameOf(posters[0]))) {
hide(row);
continue;
}
for (const poster of posters) {
if (isBlocked(usernameOf(poster))) hide(poster);
}
}
}
function scanSearchResults(root) {
for (const result of collect(root, '.fps-result, .search-result')) {
const author = result.querySelector('.author [data-user-card], .author a[href*="/u/"]');
if (isBlocked(usernameOf(author))) hide(result);
}
}
function scanTopicMapUsers(root) {
for (const avatar of collect(root, '.topic-map__users-list a[data-user-card]')) {
if (!isBlocked(usernameOf(avatar))) continue;
const item = avatar.parentElement;
hide(item?.parentElement?.classList.contains('topic-map__users-list') ? item : avatar);
}
}
function scanBoosts(root) {
if (!cfg.hideBoosts) return;
const selector = '.discourse-boosts__bubble, .post-boost, .discourse-boost, [class*="boost-item"], [class*="boost-bubble"]';
for (const boost of collect(root, selector)) {
if (hasBlockedUser(boost)) hide(boost);
}
}
function scanQuotes(root) {
for (const quote of collect(root, 'aside.quote[data-username], .quote[data-username]')) {
if (isBlocked(quote.getAttribute('data-username'))) hide(quote);
}
}
function scanFeeds(root) {
const selector = '.user-stream-item, .activity-stream .item, .bookmark-list-item';
for (const item of collect(root, selector)) {
const author = item.querySelector('a.main-avatar[data-user-card], .author [data-user-card], .avatar[data-user-card]');
if (isBlocked(usernameOf(author))) hide(item);
}
}
function scan(root) {
if (!cfg.enabled || blockedSet.size === 0) return;
scanPosts(root);
scanOpenedTopic(root);
scanTopicRows(root);
scanSearchResults(root);
scanTopicMapUsers(root);
scanBoosts(root);
scanQuotes(root);
scanFeeds(root);
}
function maybeInjectCardButton(root) {
if (!cfg.showCardButton) return;
for (const card of collect(root, '.user-card, #user-card')) {
if (card.querySelector('.ldub-card-btn')) continue;
const profile = card.querySelector('a[data-user-card], a[href^="/u/"]');
const username = usernameOf(profile);
if (!username) continue;
const host = card.querySelector('.usercard-controls, .user-card-controls, .card-row .controls, .names, .card-content') || card;
const button = document.createElement('button');
button.className = 'ldub-card-btn';
button.textContent = isBlocked(username) ? '已屏蔽 ✓' : '🚫 本地屏蔽';
button.title = '将 @' + username + ' 加入本地屏蔽名单';
button.addEventListener('click', event => {
event.preventDefault();
event.stopPropagation();
if (!isBlocked(username)) {
blockedList.push(username);
saveList();
scan(document);
}
button.textContent = '已屏蔽 ✓';
});
host.appendChild(button);
}
}
function injectStyle() {
if (document.getElementById(STYLE_ID)) return;
const style = document.createElement('style');
style.id = STYLE_ID;
style.textContent = [
'.' + HIDDEN + '{display:none !important;}',
'.ldub-card-btn{display:inline-block;margin:6px 4px 2px;padding:4px 10px;font-size:12px;line-height:1.4;cursor:pointer;border:1px solid var(--primary-low-mid,#aaa);border-radius:6px;background:var(--secondary,#fff);color:var(--primary,#333);}',
'.ldub-card-btn:hover{background:var(--danger-low,#fdecec);border-color:var(--danger,#c00);color:var(--danger,#c00);}'
].join('');
(document.head || document.documentElement).appendChild(style);
}
let pending = false;
function requestFullScan() {
if (pending) return;
pending = true;
queueMicrotask(() => {
pending = false;
scan(document);
maybeInjectCardButton(document);
});
}
function scanMutationNode(node) {
if (node.nodeType !== Node.ELEMENT_NODE) return;
scan(node);
maybeInjectCardButton(node);
const container = node.closest(
'article[data-post-id], .topic-post.post--topic-owner, .topic-list-item, .latest-topic-list-item, ' +
'.fps-result, .search-result, .discourse-boosts__bubble, .post-boost, .discourse-boost, ' +
'.topic-map__users-list, .user-stream-item, .activity-stream .item, .bookmark-list-item'
);
if (container && container !== node) {
scan(container);
maybeInjectCardButton(container);
}
}
function startObserver() {
new MutationObserver(mutations => {
for (const mutation of mutations) {
if (mutation.type === 'childList') {
mutation.addedNodes.forEach(scanMutationNode);
continue;
}
if (mutation.target.classList?.contains(HIDDEN)) continue;
if (mutation.attributeName === 'data-user-card' || mutation.attributeName === 'data-username' || mutation.attributeName === 'href' || mutation.attributeName === 'class') {
scanMutationNode(mutation.target);
}
}
}).observe(document.documentElement, {
childList: true,
subtree: true,
attributes: true,
attributeFilter: ['data-user-card', 'data-username', 'href', 'class']
});
}
function hookRouting() {
const schedule = () => setTimeout(requestFullScan, 0);
for (const method of ['pushState', 'replaceState']) {
const original = history[method];
history[method] = function () {
const result = original.apply(this, arguments);
schedule();
return result;
};
}
window.addEventListener('popstate', schedule);
}
function addUsers() {
const input = prompt('输入要屏蔽的用户名(@ 后的用户名;可用空格或逗号分隔):');
if (!input) return;
let added = 0;
for (const username of input.split(/[\s,,]+/).map(normalize).filter(Boolean)) {
if (!blockedSet.has(username)) {
blockedList.push(username);
blockedSet.add(username);
added++;
}
}
if (added) saveList();
scan(document);
alert('已添加 ' + added + ' 人;当前共屏蔽 ' + blockedList.length + ' 人。');
}
function manageUsers() {
if (!blockedList.length) {
alert('当前没有屏蔽任何用户。');
return;
}
const input = prompt('当前名单:\n' + blockedList.join('\n') + '\n\n输入要移除的用户名(多个用逗号分隔);输入 * 清空全部。');
if (input === null) return;
const value = input.trim();
if (value === '*') blockedList = [];
else if (value) {
const removed = new Set(value.split(/[\s,,]+/).map(normalize).filter(Boolean));
blockedList = blockedList.filter(username => !removed.has(normalize(username)));
} else return;
saveList();
unhideAll();
scan(document);
alert('名单已更新;当前共屏蔽 ' + blockedList.length + ' 人。');
}
function toggle() {
cfg.enabled = !cfg.enabled;
saveConfig();
if (cfg.enabled) scan(document);
else unhideAll();
alert('本地屏蔽已' + (cfg.enabled ? '开启' : '关闭') + '。');
}
function registerMenus() {
GM_registerMenuCommand('➕ 添加屏蔽用户', addUsers);
GM_registerMenuCommand('🧾 管理屏蔽名单 (' + blockedList.length + ')', manageUsers);
GM_registerMenuCommand('🔇 ' + (cfg.enabled ? '关闭' : '开启') + '本地屏蔽', toggle);
}
function init() {
injectStyle();
registerMenus();
if (typeof GM_addValueChangeListener === 'function') {
GM_addValueChangeListener(STORE_KEY, (_, __, value, remote) => {
if (!remote) return;
try {
blockedList = JSON.parse(value) || [];
blockedSet = new Set(blockedList.map(normalize));
unhideAll();
scan(document);
} catch (_) {}
});
}
const ready = () => {
requestFullScan();
startObserver();
hookRouting();
};
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', ready, { once: true });
else ready();
}
init();
})();