YouTubeのボットコメントを非表示またはぼかし(ニックネーム検出)、コメント並べ替えパネルに切り替え。多言語UI。
// ==UserScript==
// @name YouTube Bot Comments Filter
// @name:ru YouTube Bot Comments Filter — фильтр бот-комментариев
// @name:es YouTube Bot Comments Filter — filtro de bots
// @name:fr YouTube Bot Comments Filter — filtre de bots
// @name:de YouTube Bot Comments Filter — Bot-Kommentarfilter
// @name:pt-BR YouTube Bot Comments Filter — filtro de bots
// @name:zh-CN YouTube Bot Comments Filter — 机器人评论过滤
// @name:ja YouTube Bot Comments Filter — ボットコメントフィルター
// @name:ar YouTube Bot Comments Filter — فلتر تعليقات الروبوت
// @name:hi YouTube Bot Comments Filter — बॉट टिप्पणी फ़िल्टर
// @namespace https://github.com/NemoKing1210/youtube-bot-comments-filter
// @version 1.1.0
// @description Hides or blurs bot comments on YouTube (nickname pattern detection) with a toggle in the comments sort panel. Multilingual UI.
// @description:ru Скрывает или размывает бот-комментарии на YouTube (детекция по нику) с переключателем в панели сортировки. Мультиязычный UI.
// @description:es Oculta o difumina comentarios de bots en YouTube (detección por apodo) con un interruptor en el panel de ordenación. UI multilingüe.
// @description:fr Masque ou floute les commentaires de bots sur YouTube (détection par pseudo) avec un bouton dans le panneau de tri. UI multilingue.
// @description:de Blendet Bot-Kommentare auf YouTube aus oder macht sie unscharf (Erkennung per Nickname) mit Umschalter im Sortierpanel. Mehrsprachige UI.
// @description:pt-BR Oculta ou desfoca comentários de bots no YouTube (detecção por apelido) com alternância no painel de ordenação. UI multilíngue.
// @description:zh-CN 在 YouTube 上隐藏或模糊机器人评论(昵称模式检测),评论排序面板内可切换显示模式。多语言界面。
// @description:ja YouTubeのボットコメントを非表示またはぼかし(ニックネーム検出)、コメント並べ替えパネルに切り替え。多言語UI。
// @description:ar يخفي أو يموّه تعليقات الروبوت على YouTube (كشف بالاسم) مع زر تبديل في لوحة ترتيب التعليقات. واجهة متعددة اللغات.
// @description:hi YouTube पर बॉट टिप्पणियों को छिपाता या धुंधला करता है (उपनाम पैटर्न), सॉर्ट पैनल में टॉगल। बहुभाषी UI।
// @author NemoKing1210
// @tag youtube
// @tag comments
// @tag filter
// @homepageURL https://github.com/NemoKing1210/youtube-bot-comments-filter
// @supportURL https://github.com/NemoKing1210/youtube-bot-comments-filter/issues
// @license MIT
// @icon https://www.youtube.com/s/desktop/favicon_48x48.png
// @match https://www.youtube.com/*
// @grant GM_getValue
// @grant GM_setValue
// @run-at document-idle
// @noframes
// ==/UserScript==
(function () {
'use strict';
/* =========================================================================
* 1. SETTINGS / STORAGE
* ========================================================================= */
const STORAGE_KEY_MODE = 'ytbf_mode'; // 'hide' | 'blur'
const DEFAULT_MODE = 'hide';
function getMode() {
return GM_getValue(STORAGE_KEY_MODE, DEFAULT_MODE);
}
function setMode(mode) {
GM_setValue(STORAGE_KEY_MODE, mode);
}
/* =========================================================================
* 2. I18N — auto-detected from browser language, falls back to English
*
* To add a new language: add a new key (ISO 639-1 code) to LOCALES
* with the same fields as the others.
* ========================================================================= */
const LOCALES = {
en: {
hide: 'Bots: hidden',
blur: 'Bots: blurred',
tooltip: 'Toggle bot-comment display mode (hide / blur)',
hiddenNotice: 'This comment was hidden by the bot filter.',
botScore: 'Bot {n}%',
botScoreTooltip: 'Estimated probability this comment is from a bot',
},
ru: {
hide: 'Боты: скрыты',
blur: 'Боты: размыты',
tooltip: 'Переключить режим отображения бот-комментариев (скрыть / размыть)',
hiddenNotice: 'Этот комментарий скрыт фильтром ботов.',
botScore: 'Бот {n}%',
botScoreTooltip: 'Оценка вероятности, что комментарий от бота',
},
es: {
hide: 'Bots: ocultos',
blur: 'Bots: difuminados',
tooltip: 'Alternar el modo de visualización de comentarios de bots (ocultar / difuminar)',
hiddenNotice: 'Este comentario fue ocultado por el filtro de bots.',
botScore: 'Bot {n}%',
botScoreTooltip: 'Probabilidad estimada de que el comentario sea de un bot',
},
fr: {
hide: 'Bots : masqués',
blur: 'Bots : flous',
tooltip: "Basculer le mode d'affichage des commentaires de bots (masquer / flouter)",
hiddenNotice: 'Ce commentaire a été masqué par le filtre anti-bots.',
botScore: 'Bot {n}%',
botScoreTooltip: 'Probabilité estimée que le commentaire vienne d’un bot',
},
de: {
hide: 'Bots: ausgeblendet',
blur: 'Bots: unscharf',
tooltip: 'Anzeigemodus für Bot-Kommentare umschalten (ausblenden / unscharf)',
hiddenNotice: 'Dieser Kommentar wurde vom Bot-Filter ausgeblendet.',
botScore: 'Bot {n}%',
botScoreTooltip: 'Geschätzte Wahrscheinlichkeit, dass der Kommentar von einem Bot stammt',
},
pt: {
hide: 'Bots: ocultos',
blur: 'Bots: desfocados',
tooltip: 'Alternar o modo de exibição de comentários de bots (ocultar / desfocar)',
hiddenNotice: 'Este comentário foi ocultado pelo filtro de bots.',
botScore: 'Bot {n}%',
botScoreTooltip: 'Probabilidade estimada de o comentário ser de um bot',
},
zh: {
hide: '机器人:已隐藏',
blur: '机器人:已模糊',
tooltip: '切换机器人评论的显示模式(隐藏 / 模糊)',
hiddenNotice: '此评论已被机器人过滤器隐藏。',
botScore: '机器人 {n}%',
botScoreTooltip: '该评论来自机器人的估计概率',
},
ja: {
hide: 'ボット:非表示',
blur: 'ボット:ぼかし',
tooltip: 'ボットコメントの表示モードを切り替え(非表示 / ぼかし)',
hiddenNotice: 'このコメントはボットフィルターにより非表示にされました。',
botScore: 'ボット {n}%',
botScoreTooltip: 'ボットによるコメントである推定確率',
},
ar: {
hide: 'الروبوتات: مخفية',
blur: 'الروبوتات: ضبابية',
tooltip: 'تبديل طريقة عرض تعليقات الروبوتات (إخفاء / تمويه)',
hiddenNotice: 'تم إخفاء هذا التعليق بواسطة فلتر الروبوتات.',
botScore: 'روبوت {n}%',
botScoreTooltip: 'الاحتمال التقديري أن التعليق من روبوت',
},
hi: {
hide: 'बॉट: छिपे हुए',
blur: 'बॉट: धुंधले',
tooltip: 'बॉट टिप्पणियों का प्रदर्शन मोड बदलें (छिपाएं / धुंधला करें)',
hiddenNotice: 'यह टिप्पणी बॉट फ़िल्टर द्वारा छिपाई गई थी।',
botScore: 'बॉट {n}%',
botScoreTooltip: 'अनुमानित संभावना कि टिप्पणी बॉट की है',
},
};
function detectLang() {
const browserLang = (navigator.language || 'en').split('-')[0].toLowerCase();
return LOCALES[browserLang] ? browserLang : 'en';
}
const LANG = detectLang();
function t(key) {
return (LOCALES[LANG] && LOCALES[LANG][key]) || LOCALES.en[key] || key;
}
/* =========================================================================
* 3. BOT DETECTION ENGINE (easily extensible)
*
* Each rule is an object { name, weight, test(name) }.
* test() receives the commenter's display name (e.g. "@SAWS.PW__TyT...")
* and returns true/false. The total score is the sum of weights of all
* rules that matched. If total >= THRESHOLD, the comment is flagged as bot.
*
* To add a new rule — just push a new object into RULES.
* To ban an exact nickname/substring — add it to EXACT_MATCHES.
* ========================================================================= */
// Exact substrings — if the nickname contains one of these, it's almost
// certainly a bot. Extend this list as new spam accounts are discovered.
const EXACT_MATCHES = [
'SAWS.PW',
'TLES.TOP',
'FK59.TOP',
];
// Common "junk" TLDs that spam bots stuff into their nickname like a domain.
const SPAM_TLDS = [
'PW', 'TOP', 'XYZ', 'ONLINE', 'SITE', 'CLUB', 'WIN', 'CC', 'ICU',
'VIP', 'CFD', 'SBS', 'SPACE', 'FUN', 'LIVE', 'MOM', 'CASA', 'MONSTER',
'CYOU', 'BUZZ', 'WORK', 'RUN', 'STREAM', 'CAM',
];
const TLD_ALT = SPAM_TLDS.join('|');
const RULES = [
{
name: 'domain-like-prefix',
// e.g. SAWS.PW__ or FK59.TOP_ (\b fails before _ — TLD must allow _/- suffix)
weight: 3,
test: (name) => new RegExp(`^@?[A-Z0-9]{2,}\\.(${TLD_ALT})(?:[_-]|$|[^A-Za-z0-9])`, 'i').test(name),
},
{
name: 'separator-right-after-tld',
// .PW__ or .TOP-
weight: 2,
test: (name) => new RegExp(`\\.(${TLD_ALT})[_-]{1,3}`, 'i').test(name),
},
{
name: 'translit-spam-keywords',
// transliterated spam phrases like "here on the site", "go to", "watch"
weight: 3,
test: (name) => /(tyt[_-]?ha|[_-]caut[_-]|^caut|na[_-]?caut|na[_-]?sait|saite|sayte|perehod|smotri|4ki[_-]?na|shtuki|haxodui|hodui[_-]?caut)/i.test(name),
},
{
name: 'alnum-digit-before-tld',
// e.g. FK59.TOP — letters+digits stuffed before a spam TLD
weight: 2,
test: (name) => new RegExp(`^@?[A-Z0-9]*\\d[A-Z0-9]*\\.(${TLD_ALT})`, 'i').test(name),
},
{
name: 'tld-then-translit-suffix',
// e.g. .TOP_Haxodui_caut_73 — domain TLD followed by translit spam tail
weight: 3,
test: (name) => new RegExp(`\\.(${TLD_ALT})_[A-Za-z0-9]*caut`, 'i').test(name),
},
{
name: 'trailing-numeric-suffix',
// e.g. _73, _05 at end of bot handles
weight: 1,
test: (name) => /[_-]\d{2,3}$/.test(name.trim()),
},
{
name: 'many-underscores',
weight: 1,
test: (name) => (name.match(/_/g) || []).length >= 3,
},
{
name: 'leetspeak-digit-in-word',
// letter-digit-letter inside a word (Sy4ki, CauT0)
weight: 1,
test: (name) => /[a-z]\d[a-z]/i.test(name),
},
{
name: 'caps-prefix-with-dot',
weight: 1,
test: (name) => /^@?[A-Z]{3,}\.[A-Z]{2,5}/.test(name),
},
{
name: 'exact-match-list',
weight: 100, // a single match is enough to flag it for certain
test: (name) => EXACT_MATCHES.some((s) => name.toUpperCase().includes(s.toUpperCase())),
},
];
const THRESHOLD = 4;
// Soft score that maps to 100% confidence (threshold itself lands ~67%).
const PERCENT_SCORE_CAP = THRESHOLD + 2;
const SCORE_BADGE_MIN_PERCENT = 50;
const BotDetector = {
rules: RULES,
threshold: THRESHOLD,
score(name) {
let total = 0;
const matched = [];
for (const rule of this.rules) {
try {
if (rule.test(name)) {
total += rule.weight;
matched.push(rule.name);
}
} catch (e) {
console.warn('[ytbf] rule error:', rule.name, e);
}
}
return { total, matched };
},
confidencePercent(total) {
if (total <= 0) return 0;
return Math.min(100, Math.round((total / PERCENT_SCORE_CAP) * 100));
},
isBot(name) {
if (!name) return false;
return this.score(name).total >= this.threshold;
},
};
function scoreLevel(percent) {
if (percent >= 85) return 'critical';
if (percent >= 70) return 'high';
return 'mid';
}
function formatBotScore(percent) {
return t('botScore').replace('{n}', String(percent));
}
/* =========================================================================
* 4. CSS
*
* "hide" mode: original comment body is replaced by a short notice.
* "blur" mode: the comment content is hidden/blurred by default and
* smoothly revealed only while hovering over the comment.
* ========================================================================= */
const HIDDEN_NOTICE_CLASS = 'ytbf-hidden-notice';
const style = document.createElement('style');
style.textContent = `
.ytbf-hide-mode #main > :not(.${HIDDEN_NOTICE_CLASS}) {
display: none !important;
}
.ytbf-hide-mode:not(:has(#main)) > :not(.${HIDDEN_NOTICE_CLASS}):not(.ytbf-bot-score) {
display: none !important;
}
.${HIDDEN_NOTICE_CLASS} {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 8px;
padding: 10px 0 6px;
font-size: 13px;
line-height: 1.4;
color: var(--yt-spec-text-secondary, #909090);
font-style: italic;
}
.${HIDDEN_NOTICE_CLASS} .ytbf-hidden-notice-text {
font-style: italic;
}
/* Blur comment body + nickname; keep only the score badge sharp. */
.ytbf-blur #author-thumbnail,
.ytbf-blur #author-text,
.ytbf-blur #name,
.ytbf-blur #published-time-text,
.ytbf-blur #sponsor-comment-badge,
.ytbf-blur #header-author > :not(.ytbf-bot-score),
.ytbf-blur #content-text,
.ytbf-blur #comment-content,
.ytbf-blur #expander,
.ytbf-blur #toolbar,
.ytbf-blur #action-buttons,
.ytbf-blur ytd-comment-engagement-bar,
.ytbf-blur #replies {
filter: blur(7px) grayscale(1);
opacity: 0.35;
transition: filter 0.25s ease, opacity 0.25s ease;
}
.ytbf-blur:hover #author-thumbnail,
.ytbf-blur:hover #author-text,
.ytbf-blur:hover #name,
.ytbf-blur:hover #published-time-text,
.ytbf-blur:hover #sponsor-comment-badge,
.ytbf-blur:hover #header-author > :not(.ytbf-bot-score),
.ytbf-blur:hover #content-text,
.ytbf-blur:hover #comment-content,
.ytbf-blur:hover #expander,
.ytbf-blur:hover #toolbar,
.ytbf-blur:hover #action-buttons,
.ytbf-blur:hover ytd-comment-engagement-bar,
.ytbf-blur:hover #replies {
filter: blur(0) grayscale(0);
opacity: 1;
}
.ytbf-blur .ytbf-bot-score {
filter: none !important;
opacity: 1 !important;
}
.ytbf-bot-score {
display: inline-flex;
align-items: center;
gap: 5px;
flex: 0 0 auto;
margin-inline-end: 6px;
padding: 2px 7px;
border-radius: 10px;
vertical-align: middle;
font-family: "Roboto", "Arial", sans-serif;
font-size: 12px;
font-weight: 500;
line-height: 1.2;
letter-spacing: 0.01em;
user-select: none;
background: var(--yt-spec-badge-chip-background, rgba(0, 0, 0, 0.06));
color: var(--ytbf-score-color, #e6a700);
box-shadow: 0 0 0 1px color-mix(in srgb, var(--ytbf-score-color, #e6a700) 35%, transparent);
}
html[dark] .ytbf-bot-score,
html[darker-dark-theme] .ytbf-bot-score {
background: rgba(0, 0, 0, 0.55);
}
.ytbf-bot-score[data-level="mid"] {
--ytbf-score-color: #c79100;
}
.ytbf-bot-score[data-level="high"] {
--ytbf-score-color: #e07000;
}
.ytbf-bot-score[data-level="critical"] {
--ytbf-score-color: #e53935;
}
.ytbf-bot-score-dot {
width: 8px;
height: 8px;
flex: 0 0 auto;
border-radius: 50%;
background: var(--ytbf-score-color, #e6a700);
box-shadow: 0 0 0 2px color-mix(in srgb, var(--ytbf-score-color, #e6a700) 25%, transparent);
}
.ytbf-bot-score-label {
white-space: nowrap;
}
#ytbf-toggle-wrap {
display: inline-flex;
align-items: center;
margin-inline-start: 8px;
vertical-align: middle;
}
ytd-comments-header-renderer #additional-section {
display: inline-flex;
align-items: center;
flex-wrap: nowrap;
}
#ytbf-toggle-wrap button.ytbf-toggle-btn {
display: inline-flex;
align-items: center;
gap: 6px;
min-height: 36px;
padding: 0 12px;
margin: 0;
border: none;
outline: none;
cursor: pointer;
appearance: none;
-webkit-appearance: none;
font-family: "Roboto", "Arial", sans-serif;
font-size: 14px;
font-weight: 500;
letter-spacing: normal;
text-transform: none;
color: var(--yt-spec-text-primary, #0f0f0f);
background: transparent;
border-radius: 18px;
-webkit-tap-highlight-color: transparent;
}
html[dark] #ytbf-toggle-wrap button.ytbf-toggle-btn,
html[darker-dark-theme] #ytbf-toggle-wrap button.ytbf-toggle-btn {
color: var(--yt-spec-text-primary, #f1f1f1);
}
#ytbf-toggle-wrap button.ytbf-toggle-btn:hover {
background: var(--yt-spec-badge-chip-background, rgba(0, 0, 0, 0.05));
}
html[dark] #ytbf-toggle-wrap button.ytbf-toggle-btn:hover,
html[darker-dark-theme] #ytbf-toggle-wrap button.ytbf-toggle-btn:hover {
background: var(--yt-spec-badge-chip-background, rgba(255, 255, 255, 0.1));
}
#ytbf-toggle-wrap button.ytbf-toggle-btn:focus-visible {
box-shadow: 0 0 0 2px var(--yt-spec-call-to-action-inverse, #065fd4);
}
#ytbf-toggle-wrap .ytbf-toggle-icon {
display: inline-flex;
width: 24px;
height: 24px;
flex: 0 0 auto;
color: inherit;
}
#ytbf-toggle-wrap .ytbf-toggle-icon svg {
fill: currentColor;
}
#ytbf-toggle-wrap .ytbf-label {
white-space: nowrap;
line-height: 1.2;
color: inherit;
}
`;
document.documentElement.appendChild(style);
/* =========================================================================
* 5. COMMENT PROCESSING
* ========================================================================= */
const PROCESSED_ATTR = 'data-ytbf-processed';
// Selectors covering both current YouTube comment markup variants.
const COMMENT_SELECTOR = 'ytd-comment-view-model, ytd-comment-renderer';
function getAuthorName(commentEl) {
const authorLink = commentEl.querySelector('#author-text');
if (!authorLink) return '';
// text like "@SAWS.PW__TyT_Ha_CauT_05"
return authorLink.textContent.trim();
}
function getCommentHost(commentEl) {
return commentEl.querySelector('#main') || commentEl;
}
function ensureHiddenNotice(commentEl) {
const host = getCommentHost(commentEl);
let notice = host.querySelector(`.${HIDDEN_NOTICE_CLASS}`);
if (!notice) {
notice = document.createElement('div');
notice.className = HIDDEN_NOTICE_CLASS;
host.appendChild(notice);
}
let text = notice.querySelector('.ytbf-hidden-notice-text');
if (!text) {
text = document.createElement('span');
text.className = 'ytbf-hidden-notice-text';
notice.appendChild(text);
}
text.textContent = t('hiddenNotice');
return notice;
}
function removeHiddenNotice(commentEl) {
commentEl.querySelectorAll(`.${HIDDEN_NOTICE_CLASS}`).forEach((el) => el.remove());
}
function getScoreBadgeAnchor(commentEl) {
if (commentEl.classList.contains('ytbf-hide-mode')) {
const notice = ensureHiddenNotice(commentEl);
const text = notice.querySelector('.ytbf-hidden-notice-text');
return { parent: notice, before: text };
}
const authorText = commentEl.querySelector('#author-text');
if (authorText && authorText.parentElement) {
return { parent: authorText.parentElement, before: authorText };
}
const headerAuthor = commentEl.querySelector('#header-author');
if (headerAuthor) {
return { parent: headerAuthor, before: headerAuthor.firstChild };
}
return { parent: commentEl, before: commentEl.firstChild };
}
function ensureScoreBadge(commentEl, percent) {
const { parent, before } = getScoreBadgeAnchor(commentEl);
let badge = commentEl.querySelector('.ytbf-bot-score');
if (!badge) {
badge = document.createElement('span');
badge.className = 'ytbf-bot-score';
badge.setAttribute('role', 'status');
const dot = document.createElement('span');
dot.className = 'ytbf-bot-score-dot';
dot.setAttribute('aria-hidden', 'true');
const label = document.createElement('span');
label.className = 'ytbf-bot-score-label';
badge.append(dot, label);
}
if (badge.parentElement !== parent || badge.nextSibling !== before) {
parent.insertBefore(badge, before);
}
badge.dataset.level = scoreLevel(percent);
badge.title = t('botScoreTooltip');
badge.setAttribute('aria-label', formatBotScore(percent));
const label = badge.querySelector('.ytbf-bot-score-label');
if (label) label.textContent = formatBotScore(percent);
}
function removeScoreBadge(commentEl) {
commentEl.querySelectorAll('.ytbf-bot-score').forEach((el) => el.remove());
}
function applyHideMode(commentEl) {
commentEl.classList.remove('ytbf-blur');
commentEl.classList.add('ytbf-hide-mode');
ensureHiddenNotice(commentEl);
}
function applyBlurMode(commentEl) {
commentEl.classList.remove('ytbf-hide-mode');
removeHiddenNotice(commentEl);
commentEl.classList.add('ytbf-blur');
}
function applyModeClass(commentEl) {
commentEl.classList.remove('ytbf-blur', 'ytbf-hide-mode');
removeHiddenNotice(commentEl);
if (getMode() === 'hide') {
applyHideMode(commentEl);
} else {
applyBlurMode(commentEl);
}
}
function clearModeClass(commentEl) {
commentEl.classList.remove('ytbf-blur', 'ytbf-hide-mode');
removeHiddenNotice(commentEl);
}
function processComment(commentEl) {
const author = getAuthorName(commentEl);
const { total } = BotDetector.score(author);
const bot = total >= BotDetector.threshold;
const percent = BotDetector.confidencePercent(total);
commentEl.setAttribute(PROCESSED_ATTR, '1');
commentEl.dataset.ytbfBot = bot ? '1' : '0';
commentEl.dataset.ytbfPercent = String(percent);
if (bot) {
applyModeClass(commentEl);
} else {
clearModeClass(commentEl);
}
if (percent > SCORE_BADGE_MIN_PERCENT) {
ensureScoreBadge(commentEl, percent);
} else {
removeScoreBadge(commentEl);
}
}
// Process only newly-added (unprocessed) comments — used on every mutation.
function processNewComments(root = document) {
root.querySelectorAll(`${COMMENT_SELECTOR}:not([${PROCESSED_ATTR}])`).forEach(processComment);
}
// Re-apply the current display mode to already-flagged bot comments —
// used when the user toggles the mode, no re-detection needed.
function reapplyModeToFlaggedComments() {
document.querySelectorAll(`${COMMENT_SELECTOR}[data-ytbf-bot="1"]`).forEach((commentEl) => {
applyModeClass(commentEl);
const percent = Number(commentEl.dataset.ytbfPercent) || 0;
if (percent > SCORE_BADGE_MIN_PERCENT) {
ensureScoreBadge(commentEl, percent);
} else {
removeScoreBadge(commentEl);
}
});
}
/* =========================================================================
* 6. TOGGLE CONTROL INSIDE THE COMMENTS PANEL
* ========================================================================= */
function createToggleIcon() {
const icon = document.createElement('span');
icon.className = 'yt-icon-shape style-scope yt-icon ytSpecIconShapeHost ytbf-toggle-icon';
icon.setAttribute('aria-hidden', 'true');
const shell = document.createElement('div');
shell.style.width = '100%';
shell.style.height = '100%';
shell.style.display = 'block';
shell.style.fill = 'currentcolor';
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
svg.setAttribute('height', '24');
svg.setAttribute('viewBox', '0 0 24 24');
svg.setAttribute('width', '24');
svg.setAttribute('focusable', 'false');
svg.setAttribute('aria-hidden', 'true');
svg.style.pointerEvents = 'none';
svg.style.display = 'inherit';
svg.style.width = '100%';
svg.style.height = '100%';
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
path.setAttribute(
'd',
'M4.25 5.61C6.27 8.2 10 13 10 13v6c0 .55.45 1 1 1h2c.55 0 1-.45 1-1v-6s3.72-4.8 5.74-7.39C20.25 4.95 19.08 4 18 4H6c-1.08 0-2.25.95-1.75 1.61z',
);
svg.appendChild(path);
shell.appendChild(svg);
icon.appendChild(shell);
return icon;
}
function labelForMode(mode) {
return mode === 'hide' ? t('hide') : t('blur');
}
function updateToggleLabel() {
const text = labelForMode(getMode());
document.querySelectorAll('#ytbf-toggle-wrap').forEach((wrap) => {
const label = wrap.querySelector('.ytbf-label');
const button = wrap.querySelector('button.ytbf-toggle-btn');
if (label) label.textContent = text;
if (button) {
button.title = t('tooltip');
button.setAttribute('aria-label', text);
}
});
}
function toggleMode() {
const next = getMode() === 'hide' ? 'blur' : 'hide';
setMode(next);
updateToggleLabel();
reapplyModeToFlaggedComments();
}
function buildToggle() {
const wrap = document.createElement('span');
wrap.id = 'ytbf-toggle-wrap';
wrap.className = 'style-scope ytd-comments-header-renderer';
const button = document.createElement('button');
button.type = 'button';
button.className = 'dropdown-trigger style-scope yt-dropdown-menu ytbf-toggle-btn';
const label = document.createElement('div');
label.className = 'style-scope yt-dropdown-menu ytbf-label';
button.appendChild(createToggleIcon());
button.appendChild(label);
wrap.appendChild(button);
button.addEventListener('click', toggleMode);
return wrap;
}
function ensureToggleInserted() {
const additionalSection = document.querySelector('ytd-comments-header-renderer #additional-section');
if (!additionalSection) return;
if (additionalSection.querySelector('#ytbf-toggle-wrap')) {
updateToggleLabel();
return;
}
const sortMenu = additionalSection.querySelector('#sort-menu');
const toggle = buildToggle();
if (sortMenu) {
sortMenu.insertAdjacentElement('afterend', toggle);
} else {
additionalSection.appendChild(toggle);
}
updateToggleLabel();
}
/* =========================================================================
* 7. DOM OBSERVATION (YouTube is an SPA, comments load dynamically)
* ========================================================================= */
let scheduled = false;
function scheduleTick() {
if (scheduled) return;
scheduled = true;
requestAnimationFrame(() => {
scheduled = false;
ensureToggleInserted();
processNewComments();
});
}
const observer = new MutationObserver(() => scheduleTick());
observer.observe(document.documentElement, { childList: true, subtree: true });
// Initial pass + safety-net interval in case a mutation is missed.
scheduleTick();
setInterval(scheduleTick, 2000);
})();