// ==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 // @downloadURL https://update.greasyfork.icu/scripts/570000/Spoiler%20Blocker%20%20%E5%89%A7%E9%80%8F%E5%B1%8F%E8%94%BD%E5%99%A8.user.js // @updateURL https://update.greasyfork.icu/scripts/570000/Spoiler%20Blocker%20%20%E5%89%A7%E9%80%8F%E5%B1%8F%E8%94%BD%E5%99%A8.meta.js // ==/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(); }); } })();