Greasy Fork

Greasy Fork is available in English.

Spoiler Blocker / 剧透屏蔽器

Block spoiler content on social media and video platforms / 在社交媒体和视频网站上屏蔽剧透内容

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==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();
  });
}

})();