Greasy Fork is available in English.
Block spoiler content on social media and video platforms / 在社交媒体和视频网站上屏蔽剧透内容
// ==UserScript==
// @name Spoiler Blocker / 剧透屏蔽器
// @name:en Spoiler Blocker
// @name:zh-CN 剧透屏蔽器
// @name:zh-TW 劇透屏蔽器
// @name:ja ネタバレブロッカー
// @name:ko 스포일러 차단기
// @name:es Bloqueador de Spoilers
// @name:pt-BR Bloqueador de Spoilers
// @name:fr Bloqueur de Spoilers
// @name:de Spoiler-Blocker
// @namespace https://github.com/spoiler-blocker
// @version 1.4.1
// @description Block spoiler content on social media and video platforms / 在社交媒体和视频网站上屏蔽剧透内容
// @description:en Block spoiler content on social media and video platforms. Custom keywords, blackout or remove mode, 9 platforms supported.
// @description:zh-CN 在社交媒体和视频网站上自动屏蔽剧透内容,支持自定义关键词、涂黑/移除模式,覆盖 9 大平台。
// @description:zh-TW 在社群媒體和影片網站上自動屏蔽劇透內容,支援自訂關鍵字、塗黑/移除模式,涵蓋 9 大平台。
// @description:ja SNS・動画サイトのネタバレを自動ブロック。カスタムキーワード、黒塗り/非表示モード、9プラットフォーム対応。
// @description:ko 소셜 미디어와 동영상 사이트에서 스포일러를 자동 차단합니다. 키워드 설정, 블랙아웃/삭제 모드, 9개 플랫폼 지원.
// @description:es Bloquea automáticamente spoilers en redes sociales y plataformas de video. Palabras clave personalizadas, modo ocultar o eliminar, 9 plataformas.
// @description:pt-BR Bloqueie spoilers automaticamente em redes sociais e plataformas de vídeo. Palavras-chave personalizadas, modo escurecer ou remover, 9 plataformas.
// @description:fr Bloquez automatiquement les spoilers sur les réseaux sociaux et plateformes vidéo. Mots-clés personnalisés, mode masquer ou supprimer, 9 plateformes.
// @description:de Blockiert automatisch Spoiler in sozialen Medien und Videoplattformen. Benutzerdefinierte Schlüsselwörter, Schwärz-/Entfernmodus, 9 Plattformen.
// @author Spoiler Blocker
// @match *://x.com/*
// @match *://twitter.com/*
// @match *://weibo.com/*
// @match *://*.weibo.com/*
// @match *://*.bilibili.com/*
// @match *://www.youtube.com/*
// @match *://www.instagram.com/*
// @match *://www.facebook.com/*
// @match *://*.facebook.com/*
// @match *://www.nicovideo.jp/*
// @match *://*.nicovideo.jp/*
// @match *://nico.ms/*
// @match *://www.threads.net/*
// @match *://www.xiaohongshu.com/*
// @match *://xiaohongshu.com/*
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_addStyle
// @grant GM_registerMenuCommand
// @run-at document-idle
// @license MIT
// ==/UserScript==
(function() {
'use strict';
// ═══════════════════════════════════════════════
// I18N
// ═══════════════════════════════════════════════
const I18N = {
'zh-CN': {
extName: '剧透屏蔽器', enabled: '启用', disabled: '已禁用',
modeGlobal: '全局模式', modePlatform: '按平台选择', platforms: '平台设置',
twitter: 'X (Twitter)', weibo: '微博', bilibili: 'B站', youtube: 'YouTube',
instagram: 'Instagram', facebook: 'Facebook', niconico: 'Niconico',
threads: 'Threads', xiaohongshu: '小红书',
keywords: '屏蔽关键词', keywordPlaceholder: '输入关键词后按回车添加',
blockMode: '屏蔽方式', blockModeBlackout: '涂黑(点击可显示)', blockModeRemove: '移除',
language: '语言', noKeywords: '暂无关键词,请添加',
spoilerWarning: '⚠ 可能包含剧透内容,点击显示',
spoilerHidden: '🚫 已屏蔽剧透内容', matchedKeyword: '匹配关键词:',
settings: '设置', close: '关闭',
regexMode: '正则匹配', regexError: '无效正则表达式', regexHelp: '试试问 AI 帮你写正则,例如「帮我写一个匹配"权力的游戏"各种写法的正则」',
theme: '主题', themeLight: '浅色', themeDark: '深色', themeSystem: '跟随系统', mode: '模式',
},
'zh-TW': {
extName: '劇透屏蔽器', enabled: '啟用', disabled: '已停用',
modeGlobal: '全域模式', modePlatform: '依平台選擇', platforms: '平台設定',
twitter: 'X (Twitter)', weibo: '微博', bilibili: 'B站', youtube: 'YouTube',
instagram: 'Instagram', facebook: 'Facebook', niconico: 'Niconico',
threads: 'Threads', xiaohongshu: '小紅書',
keywords: '屏蔽關鍵字', keywordPlaceholder: '輸入關鍵字後按 Enter 新增',
blockMode: '屏蔽方式', blockModeBlackout: '塗黑(點擊可顯示)', blockModeRemove: '移除',
language: '語言', noKeywords: '尚無關鍵字,請新增',
spoilerWarning: '⚠ 可能包含劇透內容,點擊顯示',
spoilerHidden: '🚫 已屏蔽劇透內容', matchedKeyword: '匹配關鍵字:',
settings: '設定', close: '關閉',
regexMode: '正規表達式', regexError: '無效正規表達式', regexHelp: '試試問 AI 幫你寫正則,例如「幫我寫一個匹配"權力的遊戲"各種寫法的正則」',
theme: '主題', themeLight: '淺色', themeDark: '深色', themeSystem: '跟隨系統', mode: '模式',
},
en: {
extName: 'Spoiler Blocker', enabled: 'Enabled', disabled: 'Disabled',
modeGlobal: 'Global Mode', modePlatform: 'Per Platform', platforms: 'Platform Settings',
twitter: 'X (Twitter)', weibo: 'Weibo', bilibili: 'Bilibili', youtube: 'YouTube',
instagram: 'Instagram', facebook: 'Facebook', niconico: 'Niconico',
threads: 'Threads', xiaohongshu: 'Xiaohongshu (RED)',
keywords: 'Block Keywords', keywordPlaceholder: 'Type keyword and press Enter',
blockMode: 'Block Mode', blockModeBlackout: 'Blackout (click to reveal)', blockModeRemove: 'Remove',
language: 'Language', noKeywords: 'No keywords yet. Add some above.',
spoilerWarning: '⚠ May contain spoilers. Click to reveal.',
spoilerHidden: '🚫 Spoiler content hidden', matchedKeyword: 'Matched:',
settings: 'Settings', close: 'Close',
regexMode: 'Regex', regexError: 'Invalid regex pattern', regexHelp: 'Ask an AI to help write regex, e.g. "Write a regex to match Game of Thrones variations"',
theme: 'Theme', themeLight: 'Light', themeDark: 'Dark', themeSystem: 'System', mode: 'Mode',
},
ja: {
extName: 'ネタバレブロッカー', enabled: '有効', disabled: '無効',
modeGlobal: 'グローバルモード', modePlatform: 'プラットフォーム別', platforms: 'プラットフォーム設定',
twitter: 'X (Twitter)', weibo: 'Weibo', bilibili: 'Bilibili', youtube: 'YouTube',
instagram: 'Instagram', facebook: 'Facebook', niconico: 'Niconico',
threads: 'Threads', xiaohongshu: '小紅書 (RED)',
keywords: 'ブロックキーワード', keywordPlaceholder: 'キーワードを入力してEnter',
blockMode: 'ブロック方式', blockModeBlackout: '塗りつぶし(クリックで表示)', blockModeRemove: '削除',
language: '言語', noKeywords: 'キーワードがありません。上から追加してください。',
spoilerWarning: '⚠ ネタバレを含む可能性があります。クリックで表示。',
spoilerHidden: '🚫 ネタバレコンテンツを非表示', matchedKeyword: '一致キーワード:',
settings: '設定', close: '閉じる',
regexMode: '正規表現', regexError: '無効な正規表現', regexHelp: 'AIに正規表現の作成を依頼してみましょう。例:「ゲーム・オブ・スローンズの表記ゆれに対応する正規表現を書いて」',
theme: 'テーマ', themeLight: 'ライト', themeDark: 'ダーク', themeSystem: 'システム', mode: 'モード',
},
ko: {
extName: '스포일러 차단기', enabled: '활성화', disabled: '비활성화',
modeGlobal: '전체 모드', modePlatform: '플랫폼별', platforms: '플랫폼 설정',
twitter: 'X (Twitter)', weibo: 'Weibo', bilibili: 'Bilibili', youtube: 'YouTube',
instagram: 'Instagram', facebook: 'Facebook', niconico: 'Niconico',
threads: 'Threads', xiaohongshu: '샤오홍슈 (RED)',
keywords: '차단 키워드', keywordPlaceholder: '키워드 입력 후 Enter',
blockMode: '차단 방식', blockModeBlackout: '가리기 (클릭하면 표시)', blockModeRemove: '삭제',
language: '언어', noKeywords: '키워드가 없습니다. 위에서 추가하세요.',
spoilerWarning: '⚠ 스포일러가 포함되어 있을 수 있습니다. 클릭하여 표시.',
spoilerHidden: '🚫 스포일러 콘텐츠 숨김', matchedKeyword: '일치 키워드:',
settings: '설정', close: '닫기',
regexMode: '정규식', regexError: '잘못된 정규식', regexHelp: 'AI에게 정규식 작성을 요청해 보세요, 예: "왕좌의 게임 변형을 매칭하는 정규식 작성"',
theme: '테마', themeLight: '라이트', themeDark: '다크', themeSystem: '시스템', mode: '모드',
},
es: {
extName: 'Bloqueador de Spoilers', enabled: 'Activado', disabled: 'Desactivado',
modeGlobal: 'Modo global', modePlatform: 'Por plataforma', platforms: 'Plataformas',
twitter: 'X (Twitter)', weibo: 'Weibo', bilibili: 'Bilibili', youtube: 'YouTube',
instagram: 'Instagram', facebook: 'Facebook', niconico: 'Niconico',
threads: 'Threads', xiaohongshu: 'Xiaohongshu (RED)',
keywords: 'Palabras clave', keywordPlaceholder: 'Escribe y pulsa Enter',
blockMode: 'Modo de bloqueo', blockModeBlackout: 'Ocultar (clic para mostrar)', blockModeRemove: 'Eliminar',
language: 'Idioma', noKeywords: 'Sin palabras clave. Agrega alguna.',
spoilerWarning: '⚠ Puede contener spoilers. Clic para mostrar.',
spoilerHidden: '🚫 Contenido de spoiler oculto', matchedKeyword: 'Coincidencia:',
settings: 'Ajustes', close: 'Cerrar',
regexMode: 'Regex', regexError: 'Patrón regex no válido', regexHelp: 'Pide a una IA que te ayude a escribir regex, ej. "Escribe una regex para Juego de Tronos"',
theme: 'Tema', themeLight: 'Claro', themeDark: 'Oscuro', themeSystem: 'Sistema', mode: 'Modo',
},
'pt-BR': {
extName: 'Bloqueador de Spoilers', enabled: 'Ativado', disabled: 'Desativado',
modeGlobal: 'Modo global', modePlatform: 'Por plataforma', platforms: 'Plataformas',
twitter: 'X (Twitter)', weibo: 'Weibo', bilibili: 'Bilibili', youtube: 'YouTube',
instagram: 'Instagram', facebook: 'Facebook', niconico: 'Niconico',
threads: 'Threads', xiaohongshu: 'Xiaohongshu (RED)',
keywords: 'Palavras-chave', keywordPlaceholder: 'Digite e pressione Enter',
blockMode: 'Modo de bloqueio', blockModeBlackout: 'Escurecer (clique para revelar)', blockModeRemove: 'Remover',
language: 'Idioma', noKeywords: 'Nenhuma palavra-chave. Adicione acima.',
spoilerWarning: '⚠ Pode conter spoilers. Clique para revelar.',
spoilerHidden: '🚫 Conteúdo de spoiler ocultado', matchedKeyword: 'Correspondência:',
settings: 'Configurações', close: 'Fechar',
regexMode: 'Regex', regexError: 'Regex inválido', regexHelp: 'Peça a uma IA para ajudar a escrever regex, ex. "Escreva uma regex para Game of Thrones"',
theme: 'Tema', themeLight: 'Claro', themeDark: 'Escuro', themeSystem: 'Sistema', mode: 'Modo',
},
fr: {
extName: 'Bloqueur de Spoilers', enabled: 'Activé', disabled: 'Désactivé',
modeGlobal: 'Mode global', modePlatform: 'Par plateforme', platforms: 'Plateformes',
twitter: 'X (Twitter)', weibo: 'Weibo', bilibili: 'Bilibili', youtube: 'YouTube',
instagram: 'Instagram', facebook: 'Facebook', niconico: 'Niconico',
threads: 'Threads', xiaohongshu: 'Xiaohongshu (RED)',
keywords: 'Mots-clés', keywordPlaceholder: 'Tapez et appuyez sur Entrée',
blockMode: 'Mode de blocage', blockModeBlackout: 'Masquer (cliquez pour révéler)', blockModeRemove: 'Supprimer',
language: 'Langue', noKeywords: 'Aucun mot-clé. Ajoutez-en ci-dessus.',
spoilerWarning: '⚠ Peut contenir des spoilers. Cliquez pour révéler.',
spoilerHidden: '🚫 Contenu spoiler masqué', matchedKeyword: 'Correspondance :',
settings: 'Paramètres', close: 'Fermer',
regexMode: 'Regex', regexError: 'Regex invalide', regexHelp: 'Demandez à une IA de vous aider, ex. « Écris une regex pour Game of Thrones »',
theme: 'Thème', themeLight: 'Clair', themeDark: 'Sombre', themeSystem: 'Système', mode: 'Mode',
},
de: {
extName: 'Spoiler-Blocker', enabled: 'Aktiviert', disabled: 'Deaktiviert',
modeGlobal: 'Globaler Modus', modePlatform: 'Pro Plattform', platforms: 'Plattformen',
twitter: 'X (Twitter)', weibo: 'Weibo', bilibili: 'Bilibili', youtube: 'YouTube',
instagram: 'Instagram', facebook: 'Facebook', niconico: 'Niconico',
threads: 'Threads', xiaohongshu: 'Xiaohongshu (RED)',
keywords: 'Schlüsselwörter', keywordPlaceholder: 'Eingeben und Enter drücken',
blockMode: 'Blockiermodus', blockModeBlackout: 'Schwärzen (Klick zum Anzeigen)', blockModeRemove: 'Entfernen',
language: 'Sprache', noKeywords: 'Keine Schlüsselwörter. Oben hinzufügen.',
spoilerWarning: '⚠ Enthält möglicherweise Spoiler. Klicken zum Anzeigen.',
spoilerHidden: '🚫 Spoiler-Inhalt ausgeblendet', matchedKeyword: 'Treffer:',
settings: 'Einstellungen', close: 'Schließen',
regexMode: 'Regex', regexError: 'Ungültiger Regex', regexHelp: 'Frag eine KI, z.B. „Schreib eine Regex für Game of Thrones Varianten"',
theme: 'Design', themeLight: 'Hell', themeDark: 'Dunkel', themeSystem: 'System', mode: 'Modus',
},
};
function t(key, lang) {
if (lang === 'zh') lang = 'zh-CN';
return (I18N[lang] && I18N[lang][key]) || I18N.en[key] || key;
}
// ═══════════════════════════════════════════════
// CSS — Blocker overlay styles
// ═══════════════════════════════════════════════
GM_addStyle(`
/* Blackout mode */
.spoiler-blocked-blackout { position: relative !important; overflow: hidden !important; }
.spoiler-blocked-blackout > * { filter: blur(15px) !important; user-select: none !important; pointer-events: none !important; transition: filter 0.3s ease !important; }
.spoiler-blocked-blackout > .spoiler-overlay { filter: none !important; pointer-events: auto !important; user-select: auto !important; }
.spoiler-overlay { position: absolute !important; top: 0 !important; left: 0 !important; width: 100% !important; height: 100% !important; display: flex !important; align-items: center !important; justify-content: center !important; background: rgba(0,0,0,0.6) !important; color: #fff !important; font-size: 14px !important; font-weight: 500 !important; cursor: pointer !important; z-index: 9999 !important; border-radius: 8px !important; backdrop-filter: blur(5px) !important; letter-spacing: 0.5px !important; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif !important; }
.spoiler-overlay:hover { background: rgba(0,0,0,0.75) !important; }
.spoiler-overlay .spoiler-label { display: block !important; padding: 12px 20px !important; background: rgba(255,255,255,0.15) !important; border-radius: 6px !important; border: 1px solid rgba(255,255,255,0.2) !important; text-align: center !important; line-height: 1.6 !important; max-width: 80% !important; color: #fff !important; }
.spoiler-overlay .spoiler-warning-text { display: block !important; font-size: 14px !important; font-weight: 500 !important; color: #fff !important; margin: 0 !important; padding: 0 !important; line-height: 1.6 !important; }
.spoiler-overlay .spoiler-keyword-hint { display: block !important; font-size: 12px !important; opacity: 0.85 !important; font-weight: 400 !important; color: #fff !important; margin: 6px 0 0 0 !important; padding: 0 !important; line-height: 1.4 !important; word-break: break-word !important; }
/* Revealed */
.spoiler-blocked-blackout.spoiler-revealed > * { filter: none !important; user-select: auto !important; pointer-events: auto !important; }
.spoiler-blocked-blackout.spoiler-revealed > .spoiler-overlay { display: none !important; }
/* Remove mode */
.spoiler-blocked-remove { display: none !important; }
@media (prefers-color-scheme: dark) { .spoiler-overlay { background: rgba(20,20,35,0.75) !important; border: 1px solid rgba(255,255,255,0.1) !important; } .spoiler-overlay:hover { background: rgba(20,20,35,0.85) !important; } .spoiler-overlay .spoiler-label { background: rgba(255,255,255,0.1) !important; border: 1px solid rgba(255,255,255,0.15) !important; } }
`);
// ═══════════════════════════════════════════════
// Platform Handlers
// ═══════════════════════════════════════════════
const TwitterPlatform = {
name: 'twitter',
hostPatterns: ['x.com', 'twitter.com'],
isMatch(h) { return this.hostPatterns.some(p => h.includes(p)); },
getPostSelector() { return 'article[data-testid="tweet"]'; },
getTextContent(el) {
const parts = [], seen = new Set();
const add = (txt) => { const s = (txt||'').trim(); if (s && s.length > 1 && !seen.has(s)) { seen.add(s); parts.push(s); } };
const un = el.querySelector('[data-testid="User-Name"]'); if (un) add(un.textContent);
const tt = el.querySelector('[data-testid="tweetText"]'); if (tt) add(tt.textContent);
const card = el.querySelector('[data-testid="card.wrapper"]'); if (card) add(card.textContent);
const inner = el.querySelectorAll('[data-testid="tweetText"]');
for (let i = 1; i < inner.length; i++) add(inner[i].textContent);
el.querySelectorAll('img[alt]').forEach(img => { if (img.alt && img.alt.length > 3) add(img.alt); });
el.querySelectorAll('a[aria-label], div[aria-label]').forEach(e => {
const l = e.getAttribute('aria-label');
if (l && l.length > 10 && !/replies|likes|reposts|bookmarks|Embedded video|views|Verified|Reply|Repost|Like|Bookmark|Share|More|Grok/.test(l)) add(l);
});
return parts.join(' ');
}
};
const WeiboPlatform = {
name: 'weibo',
hostPatterns: ['weibo.com'],
isMatch(h) { return this.hostPatterns.some(p => h.includes(p)); },
getPostSelector() { return '.card-wrap, .Feed_body_3R0rO, [class*="Feed_body"], .wbpro-feed-content, article[class*="detail"]'; },
getTextContent(el) {
const parts = [], seen = new Set();
const add = (txt) => { const s = (txt||'').trim(); if (s && s.length > 1 && !seen.has(s)) { seen.add(s); parts.push(s); } };
['.txt','[class*="detail_wbtext"]','.wbpro-feed-content .txt','[class*="Feed_body"] [class*="text"]','.weibo-text'].forEach(sel => { const e = el.querySelector(sel); if (e) add(e.textContent); });
el.querySelectorAll('a[href*="topic"], .topic-link, [class*="topic"]').forEach(e => add(e.textContent));
el.querySelectorAll('.card-comment .txt, [class*="repost"] .txt').forEach(e => add(e.textContent));
el.querySelectorAll('img[alt]').forEach(img => { if (img.alt && img.alt.length > 3) add(img.alt); });
el.querySelectorAll('.video-title, [class*="video"] .title, .wbpro-feed-content .title').forEach(e => add(e.textContent));
if (parts.length === 0) add(el.textContent);
return parts.join(' ');
}
};
const BilibiliPlatform = {
name: 'bilibili',
hostPatterns: ['bilibili.com'],
isMatch(h) { return this.hostPatterns.some(p => h.includes(p)); },
getPostSelector() { return '.bili-dyn-list__item, .bili-video-card, .floor-single-card, .top-video, .items__item'; },
getTextContent(el) {
const parts = [], seen = new Set();
const add = (txt) => { const s = (txt||'').trim(); if (s && s.length > 1 && !seen.has(s)) { seen.add(s); parts.push(s); } };
['.bili-video-card__info--tit','.bili-video-card__info--tit a','.bili-video-card__title','.bili-video-card__details .bili-video-card__title','a[title]'].forEach(sel => {
el.querySelectorAll(sel).forEach(e => add(e.getAttribute('title') || e.textContent));
});
['.bili-dyn-content__orig__desc','.bili-rich-text','.bili-dyn-content__forward__desc','.dyn-card-opus__title','.dyn-card-opus__summary','.opus-module-title','.opus-module-content'].forEach(sel => {
el.querySelectorAll(sel).forEach(e => add(e.textContent));
});
el.querySelectorAll('.bili-dyn-content__orig__topic, .topic-link, [class*="tag"], .bili-video-card__info--tag').forEach(e => add(e.textContent));
el.querySelectorAll('.bili-dyn-card-video__title, .bili-dyn-card-video__desc').forEach(e => add(e.textContent));
el.querySelectorAll('.top-video__title, .top-video__desc').forEach(e => add(e.textContent));
el.querySelectorAll('.bili-video-card__info--owner, .bili-dyn-title__text, .bili-dyn-item__header .name').forEach(e => add(e.textContent));
el.querySelectorAll('img[alt]').forEach(img => { if (img.alt && img.alt.length > 3) add(img.alt); });
if (parts.length === 0) add(el.textContent);
return parts.join(' ');
}
};
const YoutubePlatform = {
name: 'youtube',
hostPatterns: ['youtube.com'],
isMatch(h) { return this.hostPatterns.some(p => h.includes(p)); },
getPostSelector() { return 'ytd-rich-item-renderer, ytd-video-renderer, ytd-compact-video-renderer, ytd-grid-video-renderer, ytd-playlist-panel-video-renderer'; },
getTextContent(el) {
const parts = [], seen = new Set();
const add = (txt) => { const s = (txt||'').trim(); if (s && s.length > 1 && !seen.has(s)) { seen.add(s); parts.push(s); } };
const ti = el.querySelector('#video-title'); if (ti) add(ti.getAttribute('title') || ti.textContent);
const ch = el.querySelector('#channel-name #text, .ytd-channel-name a, #text.ytd-channel-name'); if (ch) add(ch.textContent);
const de = el.querySelector('#description-text, .metadata-snippet-text, #dismissible .metadata-snippet-container span'); if (de) add(de.textContent);
const me = el.querySelector('#metadata-line'); if (me) add(me.textContent);
el.querySelectorAll('a.yt-simple-endpoint[href*="/hashtag/"], span.ytd-badge-supported-renderer').forEach(e => add(e.textContent));
const vl = el.querySelector('a#video-title-link[aria-label], a#video-title[aria-label]'); if (vl) add(vl.getAttribute('aria-label'));
el.querySelectorAll('.ytd-thumbnail-overlay-time-status-renderer, .ytd-thumbnail-overlay-bottom-panel-renderer').forEach(e => add(e.textContent));
return parts.join(' ');
}
};
const InstagramPlatform = {
name: 'instagram',
hostPatterns: ['instagram.com'],
isMatch(h) { return this.hostPatterns.some(p => h.includes(p)); },
getPostSelector() { return 'article, div[role="presentation"], ._aagw, ._ab6k'; },
getTextContent(el) {
const parts = [], seen = new Set();
const add = (txt) => { const s = (txt||'').trim(); if (s && s.length > 1 && !seen.has(s)) { seen.add(s); parts.push(s); } };
el.querySelectorAll('span._ap3a, div._a9zs span, span[class*="x1lliihq"]').forEach(e => add(e.textContent));
el.querySelectorAll('span._ap3a._aaco, a[role="link"] span, header a span').forEach(e => add(e.textContent));
el.querySelectorAll('a[href*="/tags/"], a[href*="/explore/tags/"]').forEach(e => add(e.textContent));
el.querySelectorAll('img[alt]').forEach(img => { if (img.alt && img.alt.length > 3) add(img.alt); });
el.querySelectorAll('video[aria-label], [role="button"][aria-label]').forEach(e => { const l = e.getAttribute('aria-label'); if (l && l.length > 5) add(l); });
el.querySelectorAll('[role="img"][aria-label]').forEach(e => add(e.getAttribute('aria-label')));
if (parts.length === 0) add(el.textContent);
return parts.join(' ');
}
};
const FacebookPlatform = {
name: 'facebook',
hostPatterns: ['facebook.com'],
isMatch(h) { return this.hostPatterns.some(p => h.includes(p)); },
getPostSelector() { return 'div[role="article"], div[data-pagelet^="FeedUnit"], div[class*="x1yztbdb"]'; },
getTextContent(el) {
const parts = [], seen = new Set();
const add = (txt) => { const s = (txt||'').trim(); if (s && s.length > 2 && !seen.has(s)) { seen.add(s); parts.push(s); } };
el.querySelectorAll('div[data-ad-preview="message"], div[dir="auto"], span[dir="auto"]').forEach(e => add(e.textContent));
el.querySelectorAll('span.x193iq5w').forEach(e => add(e.textContent));
el.querySelectorAll('span[class*="x1lliihq"]').forEach(e => add(e.textContent));
el.querySelectorAll('img[alt]').forEach(img => { const a = img.alt; if (a && a.length > 5 && !a.includes('profile') && !a.includes('avatar')) add(a); });
el.querySelectorAll('a[aria-label], [role="img"][aria-label]').forEach(e => { const l = e.getAttribute('aria-label'); if (l && l.length > 5) add(l); });
el.querySelectorAll('a[role="link"] span[dir="auto"]').forEach(e => add(e.textContent));
if (parts.length === 0) add(el.textContent);
return parts.join(' ');
}
};
const NiconicoPlatform = {
name: 'niconico',
hostPatterns: ['nicovideo.jp', 'nico.ms'],
isMatch(h) { return this.hostPatterns.some(p => h.includes(p)); },
getPostSelector() { return '.NC-VideoMediaObject, .NC-MediaObject, .RankingMainVideo, .VideoItem, .item, .MediaObject, [data-video-id], .RankingMatrixCell, .SpecifiedRanking-listItem, .RecommendItem, .NC-NicorepoItem'; },
getTextContent(el) {
const parts = [], seen = new Set();
const add = (txt) => { const s = (txt||'').trim(); if (s && s.length > 1 && !seen.has(s)) { seen.add(s); parts.push(s); } };
['.NC-VideoMediaObject-title','.NC-MediaObject-title','.itemTitle','.VideoTitle','a[title]','.title','h2','h3','.NC-NicorepoItem-contentTitle'].forEach(sel => {
el.querySelectorAll(sel).forEach(e => add(e.getAttribute('title') || e.textContent));
});
el.querySelectorAll('.TagItem, .NC-VideoTag, .tag, a[href*="/tag/"]').forEach(e => add(e.textContent));
el.querySelectorAll('.NC-VideoMediaObject-description, .description, .NC-MediaObject-description').forEach(e => add(e.textContent));
el.querySelectorAll('.NC-VideoMediaObject-owner, .owner, .NC-MediaObject-owner').forEach(e => add(e.textContent));
el.querySelectorAll('img[alt]').forEach(img => { if (img.alt && img.alt.length > 3) add(img.alt); });
if (parts.length === 0) add(el.textContent);
return parts.join(' ');
}
};
const ThreadsPlatform = {
name: 'threads',
hostPatterns: ['threads.net'],
isMatch(h) { return this.hostPatterns.some(p => h.includes(p)); },
getPostSelector() { return 'div[data-pressable-container="true"], article, div[role="article"]'; },
getTextContent(el) {
const parts = [], seen = new Set();
const add = (txt) => { const s = (txt||'').trim(); if (s && s.length > 2 && !seen.has(s)) { seen.add(s); parts.push(s); } };
el.querySelectorAll('span[dir="auto"], div[dir="auto"], span[class*="x1lliihq"]').forEach(e => add(e.textContent));
el.querySelectorAll('a[href*="/tag/"], a[href*="/tags/"]').forEach(e => add(e.textContent));
el.querySelectorAll('img[alt]').forEach(img => { if (img.alt && img.alt.length > 3) add(img.alt); });
el.querySelectorAll('[role="img"][aria-label], video[aria-label]').forEach(e => { const l = e.getAttribute('aria-label'); if (l && l.length > 5) add(l); });
el.querySelectorAll('a[role="link"] span').forEach(e => add(e.textContent));
if (parts.length === 0) add(el.textContent);
return parts.join(' ');
}
};
const XiaohongshuPlatform = {
name: 'xiaohongshu',
hostPatterns: ['xiaohongshu.com'],
isMatch(h) { return this.hostPatterns.some(p => h.includes(p)); },
getPostSelector() { return 'section.note-item, div.note-detail-mask'; },
getTextContent(el) {
const parts = [], seen = new Set();
const add = (txt) => { const s = (txt||'').trim(); if (s && s.length > 1 && !seen.has(s)) { seen.add(s); parts.push(s); } };
el.querySelectorAll('a.title span, a.title').forEach(e => add(e.textContent));
el.querySelectorAll('span.name').forEach(e => add(e.textContent));
const dt = el.querySelector('.note-content div.title'); if (dt) add(dt.textContent);
el.querySelectorAll('.note-content div.desc span.note-text, .note-content div.desc').forEach(e => add(e.textContent));
el.querySelectorAll('.note-text a.tag, a[href*="search_result"]').forEach(e => add(e.textContent));
el.querySelectorAll('a.note-content-user').forEach(e => add(e.textContent));
const da = el.querySelector('span.username'); if (da) add(da.textContent);
el.querySelectorAll('img[alt]').forEach(img => { if (img.alt && img.alt.length > 3) add(img.alt); });
if (parts.length === 0) add(el.textContent);
return parts.join(' ');
}
};
// ═══════════════════════════════════════════════
// Storage helpers (GM_getValue / GM_setValue)
// ═══════════════════════════════════════════════
function getDefaultLanguage() {
const lang = navigator.language;
const langMap = {
'zh-CN': 'zh-CN', 'zh-TW': 'zh-TW', 'zh': 'zh-CN',
'en': 'en', 'ja': 'ja', 'ko': 'ko',
'es': 'es', 'pt-BR': 'pt-BR', 'pt': 'pt-BR',
'fr': 'fr', 'de': 'de'
};
return langMap[lang] || langMap[lang.split('-')[0]] || 'en';
}
const DEFAULTS = {
enabled: true,
mode: 'global',
platforms: { twitter: true, weibo: true, bilibili: true, youtube: true, instagram: true, facebook: true, niconico: true, threads: true, xiaohongshu: true },
keywords: [],
blockMode: 'blackout',
useRegex: false,
theme: 'system',
language: getDefaultLanguage(),
};
function loadSettings() {
const s = {};
for (const [k, v] of Object.entries(DEFAULTS)) {
const stored = GM_getValue(k, undefined);
if (stored === undefined) {
s[k] = JSON.parse(JSON.stringify(v));
} else {
s[k] = stored;
}
}
return s;
}
function saveSetting(key, value) {
GM_setValue(key, value);
}
// ═══════════════════════════════════════════════
// Blocker Core
// ═══════════════════════════════════════════════
const SpoilerBlocker = {
settings: null,
currentPlatform: null,
observer: null,
revealedPosts: new Set(),
scanTimer: null,
init(platform) {
this.currentPlatform = platform;
this.settings = loadSettings();
this.scan();
this.startObserver();
},
isPlatformEnabled() {
if (!this.settings.enabled) return false;
if (this.settings.mode === 'global') return true;
return this.settings.platforms[this.currentPlatform.name] === true;
},
getMatchedKeywords(text) {
if (!text || this.settings.keywords.length === 0) return [];
if (this.settings.useRegex) {
return this.settings.keywords.filter(kw => {
try { return new RegExp(kw, 'i').test(text); }
catch { return text.toLowerCase().includes(kw.toLowerCase()); }
});
}
const lower = text.toLowerCase();
return this.settings.keywords.filter(kw => lower.includes(kw.toLowerCase()));
},
getPostFingerprint(element) {
const text = this.currentPlatform.getTextContent(element);
return text ? text.substring(0, 200).trim() : null;
},
blockElement(element, matchedKeywords) {
if (this.settings.blockMode === 'remove') {
element.classList.remove('spoiler-blocked-blackout', 'spoiler-revealed');
element.classList.add('spoiler-blocked-remove');
const ex = element.querySelector('.spoiler-overlay'); if (ex) ex.remove();
} else {
element.classList.remove('spoiler-blocked-remove');
element.classList.add('spoiler-blocked-blackout');
const ex = element.querySelector('.spoiler-overlay'); if (ex) ex.remove();
const overlay = document.createElement('div');
overlay.className = 'spoiler-overlay';
const label = document.createElement('div');
label.className = 'spoiler-label';
const wLine = document.createElement('div');
wLine.className = 'spoiler-warning-text';
wLine.textContent = t('spoilerWarning', this.settings.language);
label.appendChild(wLine);
const kLine = document.createElement('div');
kLine.className = 'spoiler-keyword-hint';
kLine.textContent = t('matchedKeyword', this.settings.language) + ' ' + matchedKeywords.join(', ');
label.appendChild(kLine);
overlay.appendChild(label);
overlay.addEventListener('click', (e) => {
e.stopPropagation(); e.preventDefault();
const fp = this.getPostFingerprint(element);
if (fp) this.revealedPosts.add(fp);
element.classList.add('spoiler-revealed');
});
element.appendChild(overlay);
}
},
ensureRevealed(element) {
element.classList.remove('spoiler-blocked-blackout', 'spoiler-blocked-remove');
const o = element.querySelector('.spoiler-overlay'); if (o) o.remove();
},
unblockElement(element) {
element.classList.remove('spoiler-blocked-blackout', 'spoiler-blocked-remove', 'spoiler-revealed');
const o = element.querySelector('.spoiler-overlay'); if (o) o.remove();
},
scan() {
if (!this.isPlatformEnabled()) return;
const sel = this.currentPlatform.getPostSelector();
document.querySelectorAll(sel).forEach(post => this.processPost(post));
},
processPost(post) {
if (post.parentElement && post.parentElement.closest('.spoiler-blocked-blackout, .spoiler-blocked-remove')) return;
const text = this.currentPlatform.getTextContent(post);
const fp = text ? text.substring(0, 200).trim() : null;
if (fp && this.revealedPosts.has(fp)) { this.ensureRevealed(post); return; }
const matched = this.getMatchedKeywords(text);
const shouldBlock = matched.length > 0;
const isBlocked = post.classList.contains('spoiler-blocked-blackout') || post.classList.contains('spoiler-blocked-remove');
if (shouldBlock && !isBlocked) {
this.blockElement(post, matched);
} else if (!shouldBlock && isBlocked) {
this.unblockElement(post);
} else if (shouldBlock && isBlocked) {
const isBo = post.classList.contains('spoiler-blocked-blackout');
const isRm = post.classList.contains('spoiler-blocked-remove');
if (this.settings.blockMode === 'blackout' && !isBo) { this.unblockElement(post); this.blockElement(post, matched); }
else if (this.settings.blockMode === 'remove' && !isRm) { this.unblockElement(post); this.blockElement(post, matched); }
}
},
rescanAll() {
this.revealedPosts.clear();
document.querySelectorAll('.spoiler-blocked-blackout, .spoiler-blocked-remove, .spoiler-revealed').forEach(el => this.unblockElement(el));
this.scan();
},
startObserver() {
if (this.observer) this.observer.disconnect();
this.observer = new MutationObserver((mutations) => {
if (!this.isPlatformEnabled()) return;
let shouldScan = false;
for (const m of mutations) { if (m.addedNodes.length > 0) { shouldScan = true; break; } }
if (shouldScan) {
if (this.scanTimer) cancelAnimationFrame(this.scanTimer);
this.scanTimer = requestAnimationFrame(() => { this.scanTimer = null; this.scan(); });
}
});
this.observer.observe(document.body, { childList: true, subtree: true });
}
};
// ═══════════════════════════════════════════════
// Settings Panel (replaces Chrome popup)
// ═══════════════════════════════════════════════
const PANEL_CSS = `
:host { all: initial; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 13px; color: var(--sb-text-primary); --sb-bg-primary: #f8f9fa; --sb-bg-secondary: #fff; --sb-text-primary: #1a1a1a; --sb-text-heading: #111; --sb-text-muted: #6b7280; --sb-text-placeholder: #9ca3af; --sb-border-primary: #e5e7eb; --sb-border-secondary: #d1d5db; --sb-accent: #3b82f6; --sb-accent-hover: #2563eb; --sb-tag-bg: #eef2ff; --sb-tag-text: #3b82f6; --sb-tag-remove: #93a3c0; --sb-error: #ef4444; --sb-slider-bg: #d1d5db; --sb-slider-knob: #fff; --sb-tooltip-bg: #1f2937; --sb-tooltip-text: #f3f4f6; --sb-help-icon-bg: #e5e7eb; --sb-help-icon-text: #6b7280; --sb-close-bg: #e5e7eb; --sb-close-text: #666; --sb-close-hover-bg: #d1d5db; --sb-close-hover-text: #333; }
:host([data-theme="dark"]) { --sb-bg-primary: #1a1a2e; --sb-bg-secondary: #25253e; --sb-text-primary: #e4e4e7; --sb-text-heading: #f4f4f5; --sb-text-muted: #a1a1aa; --sb-text-placeholder: #71717a; --sb-border-primary: #3f3f5c; --sb-border-secondary: #4a4a6a; --sb-accent: #60a5fa; --sb-accent-hover: #3b82f6; --sb-tag-bg: #1e3a5f; --sb-tag-text: #60a5fa; --sb-tag-remove: #6b7fa0; --sb-error: #f87171; --sb-slider-bg: #4a4a6a; --sb-slider-knob: #e4e4e7; --sb-tooltip-bg: #3f3f5c; --sb-tooltip-text: #e4e4e7; --sb-help-icon-bg: #3f3f5c; --sb-help-icon-text: #a1a1aa; --sb-close-bg: #3f3f5c; --sb-close-text: #a1a1aa; --sb-close-hover-bg: #4a4a6a; --sb-close-hover-text: #e4e4e7; }
.sb-panel { position: fixed; top: 60px; right: 20px; width: 340px; max-height: 80vh; overflow-y: auto; background: var(--sb-bg-primary); border-radius: 12px; box-shadow: 0 8px 32px rgba(0,0,0,0.18); z-index: 2147483647; padding: 16px; display: none; }
.sb-panel.sb-visible { display: block; }
.sb-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 16px; padding-bottom: 12px; border-bottom: 1px solid var(--sb-border-primary); }
.sb-header-left { display: flex; align-items: center; gap: 8px; }
.sb-header h2 { font-size: 16px; font-weight: 600; color: var(--sb-text-heading); margin: 0; }
.sb-header-right { display: flex; align-items: center; gap: 8px; }
.sb-close-btn { width: 28px; height: 28px; border: none; background: var(--sb-close-bg); color: var(--sb-close-text); font-size: 16px; border-radius: 50%; cursor: pointer; display: flex; align-items: center; justify-content: center; transition: background 0.15s; }
.sb-close-btn:hover { background: var(--sb-close-hover-bg); color: var(--sb-close-hover-text); }
.sb-lang-switch { padding: 4px 8px; border: 1px solid var(--sb-border-secondary); border-radius: 6px; font-size: 12px; background: var(--sb-bg-secondary); color: var(--sb-text-primary); cursor: pointer; outline: none; }
.sb-section { margin-bottom: 14px; }
.sb-section-title { font-size: 12px; font-weight: 600; color: var(--sb-text-muted); text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 8px; }
.sb-toggle { display: flex; align-items: center; gap: 10px; cursor: pointer; }
.sb-toggle input { display: none; }
.sb-slider { width: 40px; height: 22px; background: var(--sb-slider-bg); border-radius: 11px; position: relative; transition: background 0.2s; }
.sb-slider::after { content: ''; position: absolute; top: 2px; left: 2px; width: 18px; height: 18px; background: var(--sb-slider-knob); border-radius: 50%; transition: transform 0.2s; box-shadow: 0 1px 3px rgba(0,0,0,0.2); }
.sb-toggle input:checked + .sb-slider { background: var(--sb-accent); }
.sb-toggle input:checked + .sb-slider::after { transform: translateX(18px); }
.sb-toggle-label { font-size: 14px; font-weight: 500; }
.sb-toggle-compact { gap: 6px; }
.sb-slider-small { width: 32px; height: 18px; border-radius: 9px; }
.sb-slider-small::after { width: 14px; height: 14px; }
.sb-toggle input:checked + .sb-slider-small::after { transform: translateX(14px); }
.sb-toggle-label-small { font-size: 12px; font-weight: 500; color: var(--sb-text-muted); }
.sb-section-title-row { display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px; }
.sb-regex-error { color: var(--sb-error); font-size: 11px; margin-top: -4px; margin-bottom: 4px; }
.sb-regex-controls { display: flex; align-items: center; gap: 6px; }
.sb-regex-help-wrap { position: relative; display: inline-flex; }
.sb-regex-help-icon { width: 16px; height: 16px; border-radius: 50%; background: var(--sb-help-icon-bg); color: var(--sb-help-icon-text); font-size: 11px; font-weight: 600; display: flex; align-items: center; justify-content: center; cursor: help; transition: background 0.15s, color 0.15s; }
.sb-regex-help-icon:hover { background: var(--sb-accent); color: #fff; }
.sb-regex-help-tooltip { display: none; position: absolute; bottom: calc(100% + 6px); right: -8px; width: max-content; max-width: 200px; padding: 6px 10px; background: var(--sb-tooltip-bg); color: var(--sb-tooltip-text); font-size: 11px; line-height: 1.5; border-radius: 6px; box-shadow: 0 4px 12px rgba(0,0,0,0.15); z-index: 10; pointer-events: none; }
.sb-regex-help-tooltip::after { content: ''; position: absolute; top: 100%; right: 12px; border: 5px solid transparent; border-top-color: var(--sb-tooltip-bg); }
.sb-regex-help-wrap:hover .sb-regex-help-tooltip { display: block; }
.sb-radio-group, .sb-checkbox-group { display: flex; flex-direction: column; gap: 6px; }
.sb-radio-item, .sb-checkbox-item { display: flex; align-items: center; gap: 8px; padding: 6px 10px; background: var(--sb-bg-secondary); border: 1px solid var(--sb-border-primary); border-radius: 8px; cursor: pointer; transition: border-color 0.15s; }
.sb-radio-item:hover, .sb-checkbox-item:hover { border-color: var(--sb-accent); }
.sb-radio-item input, .sb-checkbox-item input { accent-color: var(--sb-accent); }
.sb-kw-wrap { display: flex; gap: 6px; margin-bottom: 8px; }
.sb-kw-wrap input { flex: 1; padding: 8px 12px; border: 1px solid var(--sb-border-secondary); border-radius: 8px; font-size: 13px; outline: none; transition: border-color 0.15s; background: var(--sb-bg-secondary); color: var(--sb-text-primary); }
.sb-kw-wrap input:focus { border-color: var(--sb-accent); }
.sb-kw-wrap button { width: 36px; height: 36px; border: none; background: var(--sb-accent); color: #fff; font-size: 18px; border-radius: 8px; cursor: pointer; display: flex; align-items: center; justify-content: center; }
.sb-kw-wrap button:hover { background: var(--sb-accent-hover); }
.sb-kw-list { display: flex; flex-wrap: wrap; gap: 6px; max-height: 150px; overflow-y: auto; }
.sb-kw-tag { display: flex; align-items: center; gap: 4px; padding: 4px 10px; background: var(--sb-tag-bg); color: var(--sb-tag-text); border-radius: 16px; font-size: 12px; font-weight: 500; }
.sb-kw-tag button { cursor: pointer; font-size: 14px; line-height: 1; color: var(--sb-tag-remove); background: none; border: none; padding: 0 2px; }
.sb-kw-tag button:hover { color: var(--sb-error); }
.sb-no-kw { color: var(--sb-text-placeholder); font-size: 12px; padding: 8px; text-align: center; width: 100%; }
.sb-platform-section { animation: sb-slide 0.2s ease; }
@keyframes sb-slide { from { opacity: 0; transform: translateY(-8px); } to { opacity: 1; transform: translateY(0); } }
.sb-fab { position: fixed; bottom: 20px; right: 20px; width: 44px; height: 44px; border-radius: 50%; background: var(--sb-accent); color: #fff; border: none; font-size: 22px; cursor: pointer; box-shadow: 0 4px 12px rgba(59,130,246,0.4); z-index: 2147483646; display: flex; align-items: center; justify-content: center; transition: transform 0.15s, box-shadow 0.15s; }
.sb-fab:hover { transform: scale(1.1); box-shadow: 0 6px 16px rgba(59,130,246,0.5); }
.sb-theme-switcher { display: flex; border: 1px solid var(--sb-border-primary); border-radius: 8px; overflow: hidden; }
.sb-theme-switcher input { display: none; }
.sb-theme-option { flex: 1; padding: 6px 0; text-align: center; font-size: 12px; font-weight: 500; color: var(--sb-text-muted); background: var(--sb-bg-secondary); cursor: pointer; transition: background 0.15s, color 0.15s; border-right: 1px solid var(--sb-border-primary); }
.sb-theme-option:last-of-type { border-right: none; }
.sb-theme-option:hover { background: var(--sb-bg-primary); }
.sb-theme-switcher input:checked + .sb-theme-option { background: var(--sb-accent); color: #fff; }
`;
const PLATFORM_KEYS = ['twitter','weibo','bilibili','youtube','instagram','facebook','niconico','threads','xiaohongshu'];
const LANG_OPTIONS = [
{ value: 'zh-CN', label: '简体中文' }, { value: 'zh-TW', label: '繁體中文' },
{ value: 'en', label: 'EN' }, { value: 'ja', label: '日本語' },
{ value: 'ko', label: '한국어' }, { value: 'es', label: 'Español' },
{ value: 'pt-BR', label: 'Português' }, { value: 'fr', label: 'Français' },
{ value: 'de', label: 'Deutsch' },
];
function escapeHtml(str) { const d = document.createElement('div'); d.textContent = str; return d.innerHTML; }
function createSettingsPanel() {
// Shadow DOM host
const host = document.createElement('div');
host.id = 'spoiler-blocker-panel-host';
const shadow = host.attachShadow({ mode: 'closed' });
const style = document.createElement('style');
style.textContent = PANEL_CSS;
shadow.appendChild(style);
// Panel container
const panel = document.createElement('div');
panel.className = 'sb-panel';
shadow.appendChild(panel);
const settings = SpoilerBlocker.settings;
const lang = () => settings.language;
function resolveTheme(themeSetting) {
if (themeSetting === 'system') {
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
}
return themeSetting;
}
function applyTheme(themeSetting) {
const resolved = resolveTheme(themeSetting);
if (resolved === 'dark') {
host.setAttribute('data-theme', 'dark');
} else {
host.removeAttribute('data-theme');
}
}
// --- Build DOM ---
// Header
const header = document.createElement('div');
header.className = 'sb-header';
const hLeft = document.createElement('div');
hLeft.className = 'sb-header-left';
const hTitle = document.createElement('h2');
hTitle.textContent = t('extName', lang());
hLeft.appendChild(hTitle);
const hRight = document.createElement('div');
hRight.className = 'sb-header-right';
const langSelect = document.createElement('select');
langSelect.className = 'sb-lang-switch';
LANG_OPTIONS.forEach(o => {
const opt = document.createElement('option');
opt.value = o.value; opt.textContent = o.label;
langSelect.appendChild(opt);
});
langSelect.value = lang();
const closeBtn = document.createElement('button');
closeBtn.className = 'sb-close-btn';
closeBtn.textContent = '\u00d7';
hRight.appendChild(langSelect);
hRight.appendChild(closeBtn);
header.appendChild(hLeft);
header.appendChild(hRight);
panel.appendChild(header);
// Enable toggle
const toggleSection = document.createElement('div');
toggleSection.className = 'sb-section';
const toggleLabel = document.createElement('label');
toggleLabel.className = 'sb-toggle';
const toggleInput = document.createElement('input');
toggleInput.type = 'checkbox';
toggleInput.checked = settings.enabled;
const slider = document.createElement('span');
slider.className = 'sb-slider';
const enableText = document.createElement('span');
enableText.className = 'sb-toggle-label';
enableText.textContent = settings.enabled ? t('enabled', lang()) : t('disabled', lang());
toggleLabel.appendChild(toggleInput);
toggleLabel.appendChild(slider);
toggleLabel.appendChild(enableText);
toggleSection.appendChild(toggleLabel);
panel.appendChild(toggleSection);
// Theme switcher
const themeSection = document.createElement('div');
themeSection.className = 'sb-section';
const themeTitle = document.createElement('div');
themeTitle.className = 'sb-section-title';
const themeSwitcher = document.createElement('div');
themeSwitcher.className = 'sb-theme-switcher';
const themeOptions = ['light', 'system', 'dark'];
const themeLabels = {};
const themeInputs = {};
themeOptions.forEach(val => {
const input = document.createElement('input');
input.type = 'radio'; input.name = 'sb-theme'; input.value = val;
input.id = 'sb-theme-' + val;
input.checked = settings.theme === val;
const label = document.createElement('label');
label.htmlFor = input.id;
label.className = 'sb-theme-option';
themeSwitcher.appendChild(input);
themeSwitcher.appendChild(label);
themeInputs[val] = input;
themeLabels[val] = label;
});
themeSection.appendChild(themeTitle);
themeSection.appendChild(themeSwitcher);
panel.appendChild(themeSection);
// Apply initial theme
applyTheme(settings.theme || 'system');
// Mode selection
const modeSection = document.createElement('div');
modeSection.className = 'sb-section';
const modeTitle = document.createElement('div');
modeTitle.className = 'sb-section-title';
const modeGroup = document.createElement('div');
modeGroup.className = 'sb-radio-group';
const modeGlobalLabel = document.createElement('label');
modeGlobalLabel.className = 'sb-radio-item';
const modeGlobalInput = document.createElement('input');
modeGlobalInput.type = 'radio'; modeGlobalInput.name = 'sb-mode'; modeGlobalInput.value = 'global';
modeGlobalInput.checked = settings.mode === 'global';
const modeGlobalSpan = document.createElement('span');
modeGlobalLabel.appendChild(modeGlobalInput);
modeGlobalLabel.appendChild(modeGlobalSpan);
const modePlatLabel = document.createElement('label');
modePlatLabel.className = 'sb-radio-item';
const modePlatInput = document.createElement('input');
modePlatInput.type = 'radio'; modePlatInput.name = 'sb-mode'; modePlatInput.value = 'platform';
modePlatInput.checked = settings.mode === 'platform';
const modePlatSpan = document.createElement('span');
modePlatLabel.appendChild(modePlatInput);
modePlatLabel.appendChild(modePlatSpan);
modeGroup.appendChild(modeGlobalLabel);
modeGroup.appendChild(modePlatLabel);
modeSection.appendChild(modeTitle);
modeSection.appendChild(modeGroup);
panel.appendChild(modeSection);
// Platform selection
const platSection = document.createElement('div');
platSection.className = 'sb-section sb-platform-section';
platSection.style.display = settings.mode === 'platform' ? 'block' : 'none';
const platTitle = document.createElement('div');
platTitle.className = 'sb-section-title';
const platGroup = document.createElement('div');
platGroup.className = 'sb-checkbox-group';
const platCheckboxes = {};
const platLabels = {};
PLATFORM_KEYS.forEach(pk => {
const lbl = document.createElement('label');
lbl.className = 'sb-checkbox-item';
const cb = document.createElement('input');
cb.type = 'checkbox'; cb.checked = settings.platforms[pk] !== false;
const sp = document.createElement('span');
lbl.appendChild(cb); lbl.appendChild(sp);
platGroup.appendChild(lbl);
platCheckboxes[pk] = cb;
platLabels[pk] = sp;
});
platSection.appendChild(platTitle);
platSection.appendChild(platGroup);
panel.appendChild(platSection);
// Block mode
const bmSection = document.createElement('div');
bmSection.className = 'sb-section';
const bmTitle = document.createElement('div');
bmTitle.className = 'sb-section-title';
const bmGroup = document.createElement('div');
bmGroup.className = 'sb-radio-group';
const bmBlackoutLabel = document.createElement('label');
bmBlackoutLabel.className = 'sb-radio-item';
const bmBlackoutInput = document.createElement('input');
bmBlackoutInput.type = 'radio'; bmBlackoutInput.name = 'sb-blockmode'; bmBlackoutInput.value = 'blackout';
bmBlackoutInput.checked = settings.blockMode === 'blackout';
const bmBlackoutSpan = document.createElement('span');
bmBlackoutLabel.appendChild(bmBlackoutInput);
bmBlackoutLabel.appendChild(bmBlackoutSpan);
const bmRemoveLabel = document.createElement('label');
bmRemoveLabel.className = 'sb-radio-item';
const bmRemoveInput = document.createElement('input');
bmRemoveInput.type = 'radio'; bmRemoveInput.name = 'sb-blockmode'; bmRemoveInput.value = 'remove';
bmRemoveInput.checked = settings.blockMode === 'remove';
const bmRemoveSpan = document.createElement('span');
bmRemoveLabel.appendChild(bmRemoveInput);
bmRemoveLabel.appendChild(bmRemoveSpan);
bmGroup.appendChild(bmBlackoutLabel);
bmGroup.appendChild(bmRemoveLabel);
bmSection.appendChild(bmTitle);
bmSection.appendChild(bmGroup);
panel.appendChild(bmSection);
// Keywords
const kwSection = document.createElement('div');
kwSection.className = 'sb-section';
const kwTitleRow = document.createElement('div');
kwTitleRow.className = 'sb-section-title-row';
const kwTitle = document.createElement('div');
kwTitle.className = 'sb-section-title';
kwTitle.style.marginBottom = '0';
const regexLabel = document.createElement('label');
regexLabel.className = 'sb-toggle sb-toggle-compact';
const regexInput = document.createElement('input');
regexInput.type = 'checkbox';
regexInput.checked = settings.useRegex || false;
const regexSlider = document.createElement('span');
regexSlider.className = 'sb-slider sb-slider-small';
const regexText = document.createElement('span');
regexText.className = 'sb-toggle-label-small';
regexText.textContent = t('regexMode', lang());
regexLabel.appendChild(regexInput);
regexLabel.appendChild(regexSlider);
regexLabel.appendChild(regexText);
const regexControls = document.createElement('div');
regexControls.className = 'sb-regex-controls';
regexControls.appendChild(regexLabel);
const helpWrap = document.createElement('span');
helpWrap.className = 'sb-regex-help-wrap';
const helpIcon = document.createElement('span');
helpIcon.className = 'sb-regex-help-icon';
helpIcon.textContent = '?';
const helpTooltip = document.createElement('span');
helpTooltip.className = 'sb-regex-help-tooltip';
helpTooltip.textContent = t('regexHelp', lang());
helpWrap.appendChild(helpIcon);
helpWrap.appendChild(helpTooltip);
regexControls.appendChild(helpWrap);
kwTitleRow.appendChild(kwTitle);
kwTitleRow.appendChild(regexControls);
const kwWrap = document.createElement('div');
kwWrap.className = 'sb-kw-wrap';
const kwInput = document.createElement('input');
kwInput.type = 'text';
const kwAddBtn = document.createElement('button');
kwAddBtn.textContent = '+';
kwWrap.appendChild(kwInput);
kwWrap.appendChild(kwAddBtn);
const kwList = document.createElement('div');
kwList.className = 'sb-kw-list';
kwSection.appendChild(kwTitleRow);
kwSection.appendChild(kwWrap);
kwSection.appendChild(kwList);
panel.appendChild(kwSection);
// --- Language update ---
function updateLang() {
const l = lang();
hTitle.textContent = t('extName', l);
enableText.textContent = settings.enabled ? t('enabled', l) : t('disabled', l);
modeTitle.textContent = t('mode', l);
modeGlobalSpan.textContent = t('modeGlobal', l);
modePlatSpan.textContent = t('modePlatform', l);
platTitle.textContent = t('platforms', l);
PLATFORM_KEYS.forEach(pk => { platLabels[pk].textContent = t(pk, l); });
bmTitle.textContent = t('blockMode', l);
bmBlackoutSpan.textContent = t('blockModeBlackout', l);
bmRemoveSpan.textContent = t('blockModeRemove', l);
kwTitle.textContent = t('keywords', l);
regexText.textContent = t('regexMode', l);
helpTooltip.textContent = t('regexHelp', l);
kwInput.placeholder = t('keywordPlaceholder', l);
themeTitle.textContent = t('theme', l);
themeLabels.light.textContent = t('themeLight', l);
themeLabels.dark.textContent = t('themeDark', l);
themeLabels.system.textContent = t('themeSystem', l);
renderKeywords();
}
function renderKeywords() {
kwList.innerHTML = '';
if (settings.keywords.length === 0) {
const empty = document.createElement('div');
empty.className = 'sb-no-kw';
empty.textContent = t('noKeywords', lang());
kwList.appendChild(empty);
return;
}
settings.keywords.forEach((kw, idx) => {
const tag = document.createElement('div');
tag.className = 'sb-kw-tag';
const span = document.createElement('span');
span.textContent = kw;
const rm = document.createElement('button');
rm.textContent = '\u00d7';
rm.addEventListener('click', () => {
settings.keywords.splice(idx, 1);
saveSetting('keywords', settings.keywords);
renderKeywords();
SpoilerBlocker.rescanAll();
});
tag.appendChild(span);
tag.appendChild(rm);
kwList.appendChild(tag);
});
}
function clearRegexError() {
kwInput.style.borderColor = '';
const errMsg = kwSection.querySelector('.sb-regex-error');
if (errMsg) errMsg.remove();
}
function showRegexError() {
kwInput.style.borderColor = '#ef4444';
let errMsg = kwSection.querySelector('.sb-regex-error');
if (!errMsg) {
errMsg = document.createElement('div');
errMsg.className = 'sb-regex-error';
kwWrap.after(errMsg);
}
errMsg.textContent = t('regexError', lang());
}
function addKeyword() {
const kw = kwInput.value.trim();
if (!kw) return;
if (settings.useRegex) {
try { new RegExp(kw); } catch (e) { showRegexError(); return; }
}
clearRegexError();
if (settings.keywords.includes(kw)) { kwInput.value = ''; return; }
settings.keywords.push(kw);
saveSetting('keywords', settings.keywords);
renderKeywords();
kwInput.value = '';
kwInput.focus();
SpoilerBlocker.rescanAll();
}
// --- Events ---
closeBtn.addEventListener('click', () => panel.classList.remove('sb-visible'));
toggleInput.addEventListener('change', () => {
settings.enabled = toggleInput.checked;
saveSetting('enabled', settings.enabled);
enableText.textContent = settings.enabled ? t('enabled', lang()) : t('disabled', lang());
SpoilerBlocker.rescanAll();
});
[modeGlobalInput, modePlatInput].forEach(radio => {
radio.addEventListener('change', () => {
settings.mode = radio.value;
saveSetting('mode', settings.mode);
platSection.style.display = radio.value === 'platform' ? 'block' : 'none';
SpoilerBlocker.rescanAll();
});
});
PLATFORM_KEYS.forEach(pk => {
platCheckboxes[pk].addEventListener('change', () => {
settings.platforms[pk] = platCheckboxes[pk].checked;
saveSetting('platforms', settings.platforms);
SpoilerBlocker.rescanAll();
});
});
[bmBlackoutInput, bmRemoveInput].forEach(radio => {
radio.addEventListener('change', () => {
settings.blockMode = radio.value;
saveSetting('blockMode', settings.blockMode);
SpoilerBlocker.rescanAll();
});
});
regexInput.addEventListener('change', () => {
settings.useRegex = regexInput.checked;
saveSetting('useRegex', settings.useRegex);
clearRegexError();
SpoilerBlocker.rescanAll();
});
themeOptions.forEach(val => {
themeInputs[val].addEventListener('change', () => {
settings.theme = val;
saveSetting('theme', settings.theme);
applyTheme(val);
});
});
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {
if (settings.theme === 'system') {
applyTheme('system');
}
});
kwInput.addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); addKeyword(); } });
kwInput.addEventListener('input', clearRegexError);
kwAddBtn.addEventListener('click', addKeyword);
langSelect.addEventListener('change', () => {
settings.language = langSelect.value;
saveSetting('language', settings.language);
updateLang();
SpoilerBlocker.rescanAll();
});
// Initial render
updateLang();
// FAB (floating action button)
const fab = document.createElement('button');
fab.className = 'sb-fab';
fab.textContent = '\uD83D\uDEE1'; // shield emoji
fab.title = 'Spoiler Blocker';
fab.addEventListener('click', () => {
panel.classList.toggle('sb-visible');
});
shadow.appendChild(fab);
document.body.appendChild(host);
// Toggle via panel API
return {
toggle() { panel.classList.toggle('sb-visible'); },
show() { panel.classList.add('sb-visible'); },
hide() { panel.classList.remove('sb-visible'); },
};
}
// ═══════════════════════════════════════════════
// Initialize
// ═══════════════════════════════════════════════
const hostname = window.location.hostname;
const allPlatforms = [
TwitterPlatform, WeiboPlatform, BilibiliPlatform, YoutubePlatform,
InstagramPlatform, FacebookPlatform, NiconicoPlatform, ThreadsPlatform,
XiaohongshuPlatform
];
const platform = allPlatforms.find(p => p.isMatch(hostname));
if (platform) {
SpoilerBlocker.init(platform);
// Create settings panel
const panelAPI = createSettingsPanel();
// Register Tampermonkey menu command
GM_registerMenuCommand('⚙ ' + t('settings', SpoilerBlocker.settings.language), () => {
panelAPI.toggle();
});
}
})();