完全替换 YouTube 聊天消息列表
yt-live-chat-text-message-renderer
bst-message-entry.bst-liveChatTextMessageRenderer
#author-name
.bst-message-username
#message
.bst-message-body
yt-live-chat-banner-renderer
#author-photo img
<img>
(點擊開啟色彩選單或前往頻道)。bst-profile-img
yt-live-chat-text-input-field-renderer [contenteditable]
#chat
yt-live-chat-text-message-renderer:not([data-ytcm-handled])
bst-message-entry.bst-liveChatTextMessageRenderer:not([data-ytcm-handled])
你可以按這個CSS對照表改一下
大多都盡量有一對一對照的
你再看看那些沒對照到
多謝幫助,不過對照調整後還是完全沒法運作,不只是CSS的問題所以超出個人能力,
本來就是只出意見、AI代勞的產品,我沒辦法進一步分析問題,還好用戶頂多就20個,應該不會有人跑來敲碗叫我相容ꉂ🤣𐤔。
AI做了這個 第1036行暫時還不行。其他應該可以的。你看看吧
// ==UserScript==
// @name YouTube聊天室增強(支援 Boost Chat)/ YouTube Chat Enhancement (+Boost Chat)
// @name:zh-tw YouTube聊天室增強(支援 Boost Chat)
// @name:en YouTube Chat Enhancement (with Boost Chat support)
// @namespace http://tampermonkey.net/
// @version 19.9-bc1
// @description 多色自動化著色用戶;非原生封鎖用戶;UI操作和功能選擇自由度;移除礙眼置頂;清理/標示洗版;發言次數統計;強化@體驗等(已適配 Boost Chat)
// @match *://www.youtube.com/live_chat*
// @grant GM_registerMenuCommand
// @grant GM_unregisterMenuCommand
// @grant GM_addStyle
// @license MIT
// ==/UserScript==
(function () {
'use strict';
// ---- 統一選擇器(優先 Boost Chat,其次原生) ----------------------------
const SEL = {
messageEntry:
'bst-message-entry.bst-liveChatTextMessageRenderer, yt-live-chat-text-message-renderer',
authorName: '.bst-message-username, #author-name',
messageBody: '.bst-message-body, #message',
banner: 'yt-live-chat-banner-renderer',
authorImg: '.bst-profile-img, #author-photo img',
inputField: 'yt-live-chat-text-input-field-renderer [contenteditable]',
chatRoot: '#chat',
unhandledMessage:
'bst-message-entry.bst-liveChatTextMessageRenderer:not([data-ytcm-handled]), yt-live-chat-text-message-renderer:not([data-ytcm-handled])',
};
const LANG = {
'zh-TW': {
buttons: { '封鎖': '封鎖', '編輯': '編輯', '刪除': '刪除', '清除': '清除' },
tooltips: {
flag: '臨時模式: 切換至臨時用戶上色,開啟時上色不儲存',
pin: '清除置頂: 開啟/關閉自動移除置頂訊息',
highlight: (mode) => `高亮模式: ${mode} (雙擊切換模式)`,
block: (mode) => `封鎖模式: ${mode} (雙擊切換模式)`,
mention: (mode) => `提及高亮: ${mode} (雙擊切換模式)`,
spam: (mode) => `洗版過濾: ${mode} (雙擊切換模式)`,
counter: '留言計數: 顯示/隱藏用戶留言計數',
clearConfirm: '確定清除所有設定?',
clearButton: '確認',
},
},
en: {
buttons: { '封鎖': 'Block', '編輯': 'Edit', '刪除': 'Delete', '清除': 'Clear' },
tooltips: {
flag: 'Temporary mode: Switch to temporary user coloring, colors are not saved when enabled',
pin: 'Pin removal: Toggle auto-remove pinned messages',
highlight: (mode) => `Highlight mode: ${mode} (Double-click to switch)`,
block: (mode) => `Block mode: ${mode} (Double-click to switch)`,
mention: (mode) => `Mention highlight: ${mode} (Double-click to switch)`,
spam: (mode) => `Spam filter: ${mode} (Double-click to switch)`,
counter: 'Message counter: Show/hide user message counts',
clearConfirm: 'Confirm reset all settings?',
clearButton: 'Confirm',
},
},
};
const currentLang = navigator.language.startsWith('zh') ? 'zh-TW' : 'en';
// 顏色選項對照表 (16進位色碼)
const COLOR_OPTIONS = {
'淺藍': '#A5CDF3',
'藍色': '#62A8EA',
'深藍': '#1C76CA',
'紫色': '#FF00FF',
'淺綠': '#98FB98',
'綠色': '#00FF00',
'深綠': '#00B300',
'青色': '#00FFFF',
'粉紅': '#FFC0CB',
'淺紅': '#F08080',
'紅色': '#FF0000',
'深紅': '#8B0000',
'橙色': '#FFA500',
'金色': '#FFD700',
'灰色': '#BDBDBD',
'深灰': '#404040',
};
// 系統常數設定 (毫秒)
const MENU_AUTO_CLOSE_DELAY = 30000,
THROTTLE_DELAY = 200,
TEMP_USER_EXPIRE_TIME = 300000,
MAX_MESSAGE_CACHE_SIZE = 200,
CLEANUP_INTERVAL = 40000,
SPAM_CHECK_INTERVAL = 500,
FLAG_DURATION = 600000,
MESSAGE_CACHE_LIMIT = 600,
DOUBLE_CLICK_DELAY = 350,
PIN_CHECK_INTERVAL = 60000;
const HIGHLIGHT_MODES = { BOTH: 0, NAME_ONLY: 1, MESSAGE_ONLY: 2 },
SPAM_MODES = { MARK: 0, REMOVE: 1 },
BLOCK_MODES = { MARK: 0, HIDE: 1 };
let userColorSettings =
JSON.parse(localStorage.getItem('userColorSettings')) || {},
blockedUsers = JSON.parse(localStorage.getItem('blockedUsers')) || [],
currentMenu = null,
menuTimeoutId = null,
featureSettings =
JSON.parse(localStorage.getItem('featureSettings')) || {
pinEnabled: false,
highlightEnabled: false,
blockEnabled: false,
buttonsVisible: false,
mentionHighlightEnabled: false,
spamFilterEnabled: false,
counterEnabled: false,
spamMode: SPAM_MODES.MARK,
blockMode: BLOCK_MODES.MARK,
flagMode: false,
},
highlightSettings =
JSON.parse(localStorage.getItem('highlightSettings')) || {
defaultMode: HIGHLIGHT_MODES.BOTH,
tempMode: HIGHLIGHT_MODES.BOTH,
},
tempUsers = JSON.parse(localStorage.getItem('tempUsers')) || {},
flaggedUsers = {},
lastTempUserCleanupTime = Date.now(),
userMessageCounts = {},
lastSpamCheckTime = 0,
lastClickTime = 0,
clickCount = 0,
pinRemoved = false,
lastPinCheckTime = 0;
const userColorCache = new Map(),
blockedUsersSet = new Set(blockedUsers),
tempUserCache = new Map(),
styleCache = new WeakMap();
class LRUCache {
constructor(limit) {
this.limit = limit;
this.cache = new Map();
}
has(key) {
return this.cache.has(key);
}
get(key) {
const value = this.cache.get(key);
if (value) {
this.cache.delete(key);
this.cache.set(key, value);
}
return value;
}
set(key, value) {
if (this.cache.has(key)) this.cache.delete(key);
else if (this.cache.size >= this.limit) {
const oldestKey = this.cache.keys().next().value;
this.cache.delete(oldestKey);
}
this.cache.set(key, value);
}
delete(key) {
this.cache.delete(key);
}
clear() {
this.cache.clear();
}
}
const messageCache = new LRUCache(MESSAGE_CACHE_LIMIT),
processedMessages = new LRUCache(MAX_MESSAGE_CACHE_SIZE * 2);
// ---- 樣式:同時支援原生 & Boost Chat --------------------------------------
const style = document.createElement('style');
style.textContent = `
:root{--highlight-color:inherit;--flagged-color:#FF0000}
.ytcm-menu{position:fixed;background-color:white;border:1px solid black;padding:5px;z-index:9999;box-shadow:2px 2px 5px rgba(0,0,0,0.2);border-radius:5px}
.ytcm-color-item{cursor:pointer;padding:0;border-radius:3px;margin:2px;border:1px solid #ddd;transition:transform 0.1s;min-width:40px;height:25px}
.ytcm-color-item:hover{transform:scale(1.1);box-shadow:0 0 5px rgba(0,0,0,0.3)}
.ytcm-list-item{cursor:pointer;padding:5px;background-color:#f0f0f0;border-radius:3px;margin:2px}
.ytcm-button{cursor:pointer;padding:5px 8px;margin:5px 2px 0 2px;border-radius:3px;border:1px solid #ccc;background-color:#f8f8f8}
.ytcm-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:5px}
.ytcm-button-row{display:flex;justify-content:space-between;margin-top:5px}
.ytcm-flex-wrap{display:flex;flex-wrap:wrap;gap:5px;margin-bottom:10px}
.ytcm-control-panel{position:fixed;left:0;bottom:75px;z-index:9998;display:flex;flex-direction:column;gap:8px;padding:0}
.ytcm-control-btn{padding:5px 0;cursor:pointer;text-align:left;min-width:20px;font-size:14px;font-weight:bold;color:white;-webkit-text-stroke:1px black;text-shadow:none;background:none;border:none;margin:0}
.ytcm-control-btn.active{-webkit-text-stroke:1px black}
.ytcm-control-btn.inactive{-webkit-text-stroke:1px red}
.ytcm-toggle-btn{padding:5px 0;cursor:pointer;text-align:left;min-width:20px;font-size:14px;font-weight:bold;color:white;-webkit-text-stroke:1px black;text-shadow:none;background:none;border:none;margin:0}
.ytcm-main-buttons{display:${featureSettings.buttonsVisible ? 'flex' : 'none'};flex-direction:column;gap:8px}
.ytcm-message-count{font-size:0.6em;opacity:0.7;margin-left:3px;display:inline-block}
/* 封鎖/洗版/標記(同時對應 Boost 與原生的名稱與訊息容器) */
[data-blocked="true"][data-block-mode="mark"] ${SEL.messageBody}{text-decoration:line-through!important;font-style:italic!important}
[data-blocked="true"][data-block-mode="hide"]{display:none!important}
[data-spam="true"] ${SEL.messageBody}{text-decoration:line-through!important}
/* 高亮與標記(作者名與訊息體) */
[data-highlight="name"] ${SEL.authorName},
[data-highlight="both"] ${SEL.authorName}{color:var(--highlight-color)!important;font-weight:bold!important}
[data-highlight="message"] ${SEL.messageBody},
[data-highlight="both"] ${SEL.messageBody}{color:var(--highlight-color)!important;font-weight:bold!important}
/* 标记(Flag)顏色 */
[data-flagged="true"] ${SEL.authorName},
[data-flagged="true"] ${SEL.messageBody}{color:var(--flagged-color)!important;font-weight:bold!important;font-style:italic!important;opacity:var(--flagged-opacity,1)}
`;
document.head.appendChild(style);
function $(root, selector) {
return (root || document).querySelector(selector);
}
function $all(root, selector) {
return Array.from((root || document).querySelectorAll(selector));
}
function initializeCaches() {
Object.entries(userColorSettings).forEach(([user, color]) =>
userColorCache.set(user, color)
);
Object.entries(tempUsers).forEach(([user, data]) =>
tempUserCache.set(user, data)
);
Object.entries(flaggedUsers).forEach(([user, expireTime]) => {
if (expireTime > Date.now()) updateAllMessages(user);
});
}
function updateAllMessages(userName) {
const messages = $all(document, SEL.messageEntry).filter((msg) => {
const nameElement = $(msg, SEL.authorName);
return (
nameElement &&
nameElement.textContent.trim() === userName &&
msg.style.display !== 'none'
);
});
messages.forEach((msg) => {
processedMessages.delete(msg);
styleCache.delete(msg);
processMessage(msg, true);
});
}
// ---- 控制面板(原樣,略微調整 updateAllMessages 呼叫) --------------------
function createControlPanel() {
const panel = document.createElement('div');
panel.className = 'ytcm-control-panel';
const mainButtons = document.createElement('div');
mainButtons.className = 'ytcm-main-buttons';
const buttons = [
{
text: '臨',
className: `ytcm-control-btn ${featureSettings.flagMode ? 'active' : 'inactive'}`,
title: LANG[currentLang].tooltips.flag,
onClick: () => handleButtonClick('臨', () => {
featureSettings.flagMode = !featureSettings.flagMode;
updateButtonState('臨', featureSettings.flagMode);
localStorage.setItem('featureSettings', JSON.stringify(featureSettings));
})
},
{
text: '頂',
className: `ytcm-control-btn ${featureSettings.pinEnabled ? 'active' : 'inactive'}`,
title: LANG[currentLang].tooltips.pin,
onClick: () => handleButtonClick('頂', () => {
featureSettings.pinEnabled = !featureSettings.pinEnabled;
pinRemoved = false;
updateButtonState('頂', featureSettings.pinEnabled);
localStorage.setItem('featureSettings', JSON.stringify(featureSettings));
})
},
{
text: '亮',
className: `ytcm-control-btn ${featureSettings.highlightEnabled ? 'active' : 'inactive'}`,
title: LANG[currentLang].tooltips.highlight(getHighlightModeName(highlightSettings.defaultMode)),
onClick: () => handleButtonClick(
'亮',
() => {
featureSettings.highlightEnabled = !featureSettings.highlightEnabled;
updateButtonState('亮', featureSettings.highlightEnabled);
updateButtonTitle('亮', LANG[currentLang].tooltips.highlight(getHighlightModeName(highlightSettings.defaultMode)));
localStorage.setItem('featureSettings', JSON.stringify(featureSettings));
updateAllMessages();
},
() => {
highlightSettings.defaultMode = (highlightSettings.defaultMode + 1) % 3;
updateButtonTitle('亮', LANG[currentLang].tooltips.highlight(getHighlightModeName(highlightSettings.defaultMode)));
localStorage.setItem('highlightSettings', JSON.stringify(highlightSettings));
updateAllMessages();
}
)
},
{
text: '封',
className: `ytcm-control-btn ${featureSettings.blockEnabled ? 'active' : 'inactive'}`,
title: LANG[currentLang].tooltips.block(
featureSettings.blockMode === BLOCK_MODES.MARK
? (currentLang === 'zh-TW' ? '標記' : 'Mark')
: (currentLang === 'zh-TW' ? '清除' : 'Clear')
),
onClick: () => handleButtonClick(
'封',
() => {
featureSettings.blockEnabled = !featureSettings.blockEnabled;
updateButtonState('封', featureSettings.blockEnabled);
updateButtonTitle('封', LANG[currentLang].tooltips.block(
featureSettings.blockMode === BLOCK_MODES.MARK
? (currentLang === 'zh-TW' ? '標記' : 'Mark')
: (currentLang === 'zh-TW' ? '清除' : 'Clear')
));
localStorage.setItem('featureSettings', JSON.stringify(featureSettings));
updateAllMessages();
},
() => {
featureSettings.blockMode = (featureSettings.blockMode + 1) % 2;
updateButtonTitle('封', LANG[currentLang].tooltips.block(
featureSettings.blockMode === BLOCK_MODES.MARK
? (currentLang === 'zh-TW' ? '標記' : 'Mark')
: (currentLang === 'zh-TW' ? '清除' : 'Clear')
));
localStorage.setItem('featureSettings', JSON.stringify(featureSettings));
updateAllMessages();
}
)
},
{
text: '@',
className: `ytcm-control-btn ${featureSettings.mentionHighlightEnabled ? 'active' : 'inactive'}`,
title: LANG[currentLang].tooltips.mention(getHighlightModeName(highlightSettings.tempMode)),
onClick: () => handleButtonClick(
'@',
() => {
featureSettings.mentionHighlightEnabled = !featureSettings.mentionHighlightEnabled;
updateButtonState('@', featureSettings.mentionHighlightEnabled);
updateButtonTitle('@', LANG[currentLang].tooltips.mention(getHighlightModeName(highlightSettings.tempMode)));
localStorage.setItem('featureSettings', JSON.stringify(featureSettings));
if (!featureSettings.mentionHighlightEnabled) {
tempUsers = {};
tempUserCache.clear();
localStorage.setItem('tempUsers', JSON.stringify(tempUsers));
}
updateAllMessages();
},
() => {
highlightSettings.tempMode = (highlightSettings.tempMode + 1) % 3;
updateButtonTitle('@', LANG[currentLang].tooltips.mention(getHighlightModeName(highlightSettings.tempMode)));
localStorage.setItem('highlightSettings', JSON.stringify(highlightSettings));
updateAllMessages();
}
)
},
{
text: '洗',
className: `ytcm-control-btn ${featureSettings.spamFilterEnabled ? 'active' : 'inactive'}`,
title: LANG[currentLang].tooltips.spam(
featureSettings.spamMode === SPAM_MODES.MARK
? (currentLang === 'zh-TW' ? '標記' : 'Mark')
: (currentLang === 'zh-TW' ? '清除' : 'Clear')
),
onClick: () => handleButtonClick(
'洗',
() => {
featureSettings.spamFilterEnabled = !featureSettings.spamFilterEnabled;
updateButtonState('洗', featureSettings.spamFilterEnabled);
updateButtonTitle('洗', LANG[currentLang].tooltips.spam(
featureSettings.spamMode === SPAM_MODES.MARK
? (currentLang === 'zh-TW' ? '標記' : 'Mark')
: (currentLang === 'zh-TW' ? '清除' : 'Clear')
));
localStorage.setItem('featureSettings', JSON.stringify(featureSettings));
if (!featureSettings.spamFilterEnabled) messageCache.clear();
updateAllMessages();
},
() => {
featureSettings.spamMode = (featureSettings.spamMode + 1) % 2;
updateButtonTitle('洗', LANG[currentLang].tooltips.spam(
featureSettings.spamMode === SPAM_MODES.MARK
? (currentLang === 'zh-TW' ? '標記' : 'Mark')
: (currentLang === 'zh-TW' ? '清除' : 'Clear')
));
localStorage.setItem('featureSettings', JSON.stringify(featureSettings));
updateAllMessages();
}
)
},
{
text: '數',
className: `ytcm-control-btn ${featureSettings.counterEnabled ? 'active' : 'inactive'}`,
title: LANG[currentLang].tooltips.counter,
onClick: () => handleButtonClick('數', () => {
featureSettings.counterEnabled = !featureSettings.counterEnabled;
updateButtonState('數', featureSettings.counterEnabled);
localStorage.setItem('featureSettings', JSON.stringify(featureSettings));
if (!featureSettings.counterEnabled) {
$all(document, '.ytcm-message-count').forEach((el) => el.remove());
} else {
updateAllMessages();
}
})
}
];
buttons.forEach((btn) => {
const button = document.createElement('div');
button.className = btn.className;
button.textContent = btn.text;
button.title = btn.title;
button.dataset.action = btn.text;
button.addEventListener('click', btn.onClick);
mainButtons.appendChild(button);
});
const toggleBtn = document.createElement('div');
toggleBtn.className = 'ytcm-toggle-btn';
toggleBtn.textContent = '☑';
toggleBtn.title = currentLang === 'zh-TW' ? '顯示/隱藏控制按鈕' : 'Show/Hide Controls';
toggleBtn.addEventListener('click', () => {
featureSettings.buttonsVisible = !featureSettings.buttonsVisible;
mainButtons.style.display = featureSettings.buttonsVisible ? 'flex' : 'none';
localStorage.setItem('featureSettings', JSON.stringify(featureSettings));
});
panel.appendChild(mainButtons);
panel.appendChild(toggleBtn);
document.body.appendChild(panel);
return panel;
}
function handleButtonClick(btnText, toggleAction, modeAction) {
const now = Date.now();
if (now - lastClickTime < DOUBLE_CLICK_DELAY) {
clickCount++;
if (clickCount === 2 && modeAction) {
modeAction();
clickCount = 0;
}
} else {
clickCount = 1;
setTimeout(() => {
if (clickCount === 1) toggleAction();
clickCount = 0;
}, DOUBLE_CLICK_DELAY);
}
lastClickTime = now;
}
function getHighlightModeName(mode) {
switch (mode) {
case HIGHLIGHT_MODES.BOTH:
return currentLang === 'zh-TW' ? '全部高亮' : 'Both';
case HIGHLIGHT_MODES.NAME_ONLY:
return currentLang === 'zh-TW' ? '僅暱稱' : 'Name Only';
case HIGHLIGHT_MODES.MESSAGE_ONLY:
return currentLang === 'zh-TW' ? '僅對話' : 'Message Only';
default:
return '';
}
}
function updateButtonState(btnText, isActive) {
const btn = document.querySelector(`.ytcm-control-btn[data-action="${btnText}"]`);
if (btn) btn.className = `ytcm-control-btn ${isActive ? 'active' : 'inactive'}`;
}
function updateButtonTitle(btnText, title) {
const btn = document.querySelector(`.ytcm-control-btn[data-action="${btnText}"]`);
if (btn) btn.title = title;
}
function cleanupProcessedMessages() {
requestIdleCallback(() => {
const allMessages = new Set($all(document, SEL.messageEntry));
const toDelete = [];
processedMessages.cache.forEach((_, msg) => {
if (!allMessages.has(msg)) {
toDelete.push(msg);
}
});
toDelete.forEach((msg) => {
processedMessages.delete(msg);
styleCache.delete(msg);
});
});
}
function processMentionedUsers(messageText, authorName, authorColor) {
if (!featureSettings.mentionHighlightEnabled || !authorColor) return;
const mentionRegex = /@([^\s].*?(?=\s|$|@|[\u200b]))/g;
let match;
const mentionedUsers = new Set();
while ((match = mentionRegex.exec(messageText)) !== null) {
const mentionedUser = match[1].trim();
if (mentionedUser) mentionedUsers.add(mentionedUser);
}
if (mentionedUsers.size !== 1) return;
const mentionedUser = Array.from(mentionedUsers)[0];
const existingUsers = $all(document, SEL.authorName).map((el) =>
el.textContent.trim()
);
const isExistingUser = existingUsers.some(
(user) => user.toLowerCase() === mentionedUser.toLowerCase()
);
if (
isExistingUser &&
!userColorCache.has(mentionedUser) &&
!tempUserCache.has(mentionedUser)
) {
const expireTime = Date.now() + TEMP_USER_EXPIRE_TIME;
tempUsers[mentionedUser] = { color: authorColor, expireTime };
tempUserCache.set(mentionedUser, { color: authorColor, expireTime });
updateAllMessages(mentionedUser);
localStorage.setItem('tempUsers', JSON.stringify(tempUsers));
}
}
function cleanupExpiredTempUsers() {
const now = Date.now();
if (now - lastTempUserCleanupTime < CLEANUP_INTERVAL) return;
lastTempUserCleanupTime = now;
let changed = false;
for (const [user, data] of tempUserCache.entries()) {
if (data.expireTime <= now) {
tempUserCache.delete(user);
if (Object.prototype.hasOwnProperty.call(tempUsers, user)) {
delete tempUsers[user];
}
changed = true;
updateAllMessages(user);
}
}
if (changed) {
localStorage.setItem('tempUsers', JSON.stringify(tempUsers));
}
}
function cleanupExpiredFlags() {
const now = Date.now();
let changed = false;
for (const user in flaggedUsers) {
if (flaggedUsers[user] <= now) {
delete flaggedUsers[user];
changed = true;
updateAllMessages(user);
}
}
}
function removePinnedMessage() {
if (!featureSettings.pinEnabled) return;
const now = Date.now();
if (now - lastPinCheckTime < PIN_CHECK_INTERVAL) return;
lastPinCheckTime = now;
requestAnimationFrame(() => {
const pinnedMessage = $(document, SEL.banner);
if (pinnedMessage) pinnedMessage.style.display = 'none';
});
}
function closeMenu() {
if (currentMenu) {
document.body.removeChild(currentMenu);
currentMenu = null;
clearTimeout(menuTimeoutId);
}
}
function createColorMenu(targetElement, event) {
closeMenu();
const menu = document.createElement('div');
menu.className = 'ytcm-menu';
menu.style.top = `${event.clientY}px`;
menu.style.left = `${event.clientX}px`;
menu.style.width = '220px';
const colorGrid = document.createElement('div');
colorGrid.className = 'ytcm-grid';
Object.entries(COLOR_OPTIONS).forEach(([colorName, colorValue]) => {
const colorItem = document.createElement('div');
colorItem.className = 'ytcm-color-item';
colorItem.title = colorName;
colorItem.style.backgroundColor = colorValue;
colorItem.addEventListener('click', () => {
if (targetElement.type === 'user') {
userColorSettings[targetElement.name] = colorValue;
userColorCache.set(targetElement.name, colorValue);
updateAllMessages(targetElement.name);
localStorage.setItem(
'userColorSettings',
JSON.stringify(userColorSettings)
);
} else if (targetElement.type === 'temp') {
const expireTime = Date.now() + TEMP_USER_EXPIRE_TIME;
tempUsers[targetElement.name] = { color: colorValue, expireTime };
tempUserCache.set(targetElement.name, { color: colorValue, expireTime });
updateAllMessages(targetElement.name);
}
closeMenu();
});
colorGrid.appendChild(colorItem);
});
const buttonRow = document.createElement('div');
buttonRow.className = 'ytcm-button-row';
const buttons = [
{
text: LANG[currentLang].buttons.封鎖,
className: 'ytcm-button',
onClick: () => {
if (targetElement.type === 'user') {
blockedUsers.push(targetElement.name);
blockedUsersSet.add(targetElement.name);
localStorage.setItem('blockedUsers', JSON.stringify(blockedUsers));
updateAllMessages(targetElement.name);
}
closeMenu();
},
},
{
text: LANG[currentLang].buttons.編輯,
className: 'ytcm-button',
onClick: (e) => {
e.stopPropagation();
createEditMenu(targetElement, event);
},
},
{
text: LANG[currentLang].buttons.刪除,
className: 'ytcm-button',
onClick: () => {
const userName = targetElement.name;
let foundInList = false;
if (userColorSettings[userName]) {
delete userColorSettings[userName];
userColorCache.delete(userName);
foundInList = true;
}
if (blockedUsersSet.has(userName)) {
blockedUsers = blockedUsers.filter((u) => u !== userName);
blockedUsersSet.delete(userName);
localStorage.setItem('blockedUsers', JSON.stringify(blockedUsers));
foundInList = true;
}
if (tempUsers[userName]) {
delete tempUsers[userName];
tempUserCache.delete(userName);
localStorage.setItem('tempUsers', JSON.stringify(tempUsers));
foundInList = true;
}
if (flaggedUsers[userName]) {
delete flaggedUsers[userName];
localStorage.setItem('flaggedUsers', JSON.stringify(flaggedUsers));
foundInList = true;
}
localStorage.setItem(
'userColorSettings',
JSON.stringify(userColorSettings)
);
const messages = $all(document, SEL.messageEntry).filter((msg) => {
const nameElement = $(msg, SEL.authorName);
return nameElement && nameElement.textContent.trim() === userName;
});
messages.forEach((msg) => {
if (foundInList) {
msg.removeAttribute('data-highlight');
msg.removeAttribute('data-flagged');
msg.removeAttribute('data-blocked');
msg.removeAttribute('data-spam');
msg.style.removeProperty('--highlight-color');
msg.style.removeProperty('--flagged-color');
$(msg, '.ytcm-message-count')?.remove();
} else {
msg.style.display = 'none';
}
});
closeMenu();
},
},
{
text: LANG[currentLang].buttons.清除,
className: 'ytcm-button',
onClick: () => {
const confirmMenu = document.createElement('div');
confirmMenu.className = 'ytcm-menu';
confirmMenu.style.top = `${event.clientY}px`;
confirmMenu.style.left = `${event.clientX}px`;
const confirmText = document.createElement('div');
confirmText.textContent = LANG[currentLang].tooltips.clearConfirm;
const confirmButton = document.createElement('button');
confirmButton.className = 'ytcm-button';
confirmButton.textContent = LANG[currentLang].tooltips.clearButton;
confirmButton.addEventListener('click', () => {
localStorage.removeItem('userColorSettings');
localStorage.removeItem('blockedUsers');
localStorage.removeItem('featureSettings');
localStorage.removeItem('highlightSettings');
localStorage.removeItem('tempUsers');
localStorage.removeItem('flaggedUsers');
window.location.reload();
});
confirmMenu.appendChild(confirmText);
confirmMenu.appendChild(confirmButton);
document.body.appendChild(confirmMenu);
setTimeout(() => {
if (document.body.contains(confirmMenu)) document.body.removeChild(confirmMenu);
}, 5000);
},
},
];
buttons.forEach((btn) => {
const button = document.createElement('button');
button.className = btn.className;
button.textContent = btn.text;
button.addEventListener('click', btn.onClick);
buttonRow.appendChild(button);
});
menu.appendChild(colorGrid);
menu.appendChild(buttonRow);
document.body.appendChild(menu);
currentMenu = menu;
menuTimeoutId = setTimeout(closeMenu, MENU_AUTO_CLOSE_DELAY);
}
function createEditMenu(targetElement) {
closeMenu();
const menu = document.createElement('div');
menu.className = 'ytcm-menu';
menu.style.top = '10px';
menu.style.left = '10px';
menu.style.width = '90%';
menu.style.maxHeight = '80vh';
menu.style.overflowY = 'auto';
const closeButton = document.createElement('button');
closeButton.className = 'ytcm-button';
closeButton.textContent = currentLang === 'zh-TW' ? '關閉' : 'Close';
closeButton.style.width = '100%';
closeButton.style.marginBottom = '10px';
closeButton.addEventListener('click', closeMenu);
menu.appendChild(closeButton);
const importExportRow = document.createElement('div');
importExportRow.className = 'ytcm-button-row';
const exportButton = document.createElement('button');
exportButton.className = 'ytcm-button';
exportButton.textContent = currentLang === 'zh-TW' ? '匯出設定' : 'Export';
exportButton.addEventListener('click', () => {
const data = {
userColorSettings,
blockedUsers,
featureSettings,
highlightSettings,
tempUsers: JSON.parse(localStorage.getItem('tempUsers')) || {},
};
const blob = new Blob([JSON.stringify(data)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'yt_chat_settings.json';
a.click();
URL.revokeObjectURL(url);
});
const importButton = document.createElement('input');
importButton.type = 'file';
importButton.className = 'ytcm-button';
importButton.accept = '.json';
importButton.style.width = '100px';
importButton.addEventListener('change', (e) => {
const file = e.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (event) => {
try {
const data = JSON.parse(event.target.result);
localStorage.setItem('userColorSettings', JSON.stringify(data.userColorSettings));
localStorage.setItem('blockedUsers', JSON.stringify(data.blockedUsers));
localStorage.setItem('featureSettings', JSON.stringify(data.featureSettings));
localStorage.setItem('highlightSettings', JSON.stringify(data.highlightSettings));
localStorage.setItem('tempUsers', JSON.stringify(data.tempUsers));
window.location.reload();
} catch {
alert(currentLang === 'zh-TW' ? '檔案格式錯誤' : 'Invalid file format');
}
};
reader.readAsText(file);
});
importExportRow.appendChild(exportButton);
importExportRow.appendChild(importButton);
menu.appendChild(importExportRow);
const blockedUserList = document.createElement('div');
blockedUserList.textContent =
currentLang === 'zh-TW' ? '封鎖用戶名單:' : 'Blocked Users:';
blockedUserList.className = 'ytcm-flex-wrap';
blockedUsers.forEach((user) => {
const userItem = document.createElement('div');
userItem.className = 'ytcm-list-item';
userItem.textContent = user;
userItem.addEventListener('click', () => {
blockedUsers = blockedUsers.filter((u) => u !== user);
blockedUsersSet.delete(user);
localStorage.setItem('blockedUsers', JSON.stringify(blockedUsers));
userItem.remove();
updateAllMessages(user);
});
blockedUserList.appendChild(userItem);
});
menu.appendChild(blockedUserList);
const coloredUserList = document.createElement('div');
coloredUserList.textContent =
currentLang === 'zh-TW' ? '被上色用戶名單:' : 'Colored Users:';
coloredUserList.className = 'ytcm-flex-wrap';
Object.keys(userColorSettings).forEach((user) => {
const userItem = document.createElement('div');
userItem.className = 'ytcm-list-item';
userItem.textContent = user;
userItem.addEventListener('click', () => {
delete userColorSettings[user];
userColorCache.delete(user);
localStorage.setItem('userColorSettings', JSON.stringify(userColorSettings));
userItem.remove();
updateAllMessages(user);
});
coloredUserList.appendChild(userItem);
});
menu.appendChild(coloredUserList);
document.body.appendChild(menu);
currentMenu = menu;
menuTimeoutId = setTimeout(closeMenu, MENU_AUTO_CLOSE_DELAY);
}
function checkForSpam(msg) {
if (!featureSettings.spamFilterEnabled) return;
const nameElement = $(msg, SEL.authorName);
if (!nameElement) return;
const userName = nameElement.textContent.trim();
if (userColorCache.has(userName) || tempUserCache.has(userName) || flaggedUsers[userName]) return;
const messageElement = $(msg, SEL.messageBody);
if (!messageElement) return;
const textNodes = Array.from(messageElement.childNodes).filter(
(node) => node.nodeType === Node.TEXT_NODE && !(node.parentElement && node.parentElement.classList?.contains('emoji'))
);
const messageText = textNodes.map((node) => node.textContent.trim()).join(' ');
if (messageCache.has(messageText)) {
if (featureSettings.spamMode === SPAM_MODES.MARK) {
msg.setAttribute('data-spam', 'true');
messageElement.style.textDecoration = 'line-through';
} else {
msg.style.display = 'none';
}
return;
}
messageCache.set(messageText, true);
}
function updateMessageCounter(msg) {
if (!featureSettings.counterEnabled) return;
const nameElement = $(msg, SEL.authorName);
if (!nameElement) return;
const userName = nameElement.textContent.trim();
if (!userMessageCounts[userName]) userMessageCounts[userName] = 0;
userMessageCounts[userName]++;
const existingCounter = $(msg, '.ytcm-message-count');
if (existingCounter) existingCounter.remove();
const counterSpan = document.createElement('span');
counterSpan.className = 'ytcm-message-count';
counterSpan.textContent = userMessageCounts[userName];
const messageElement = $(msg, SEL.messageBody);
if (messageElement) messageElement.appendChild(counterSpan);
}
function processMessage(msg, isInitialLoad = false) {
if (styleCache.has(msg)) return;
const authorNameEl = $(msg, SEL.authorName);
const messageElement = $(msg, SEL.messageBody);
if (!authorNameEl || !messageElement) return;
const userName = authorNameEl.textContent.trim();
if (featureSettings.spamFilterEnabled) checkForSpam(msg);
msg.removeAttribute('data-spam');
if (featureSettings.blockEnabled && blockedUsersSet.has(userName)) {
msg.setAttribute('data-blocked', 'true');
msg.setAttribute(
'data-block-mode',
featureSettings.blockMode === BLOCK_MODES.MARK ? 'mark' : 'hide'
);
if (featureSettings.blockMode === BLOCK_MODES.HIDE) {
msg.style.display = 'none';
}
styleCache.set(msg, true);
return;
}
if (msg.hasAttribute('data-blocked')) {
styleCache.set(msg, true);
return;
}
msg.removeAttribute('data-highlight');
msg.removeAttribute('data-flagged');
// 若已因其他系統規則隱藏,略過
if (msg.style.display === 'none' && !msg.hasAttribute('data-highlight')) {
return;
}
if (featureSettings.flagMode && flaggedUsers[userName]) {
msg.setAttribute('data-flagged', 'true');
msg.style.setProperty('--highlight-color', userColorCache.get(userName) || COLOR_OPTIONS.紅色);
}
if (
featureSettings.highlightEnabled &&
(tempUserCache.has(userName) || userColorCache.get(userName))
) {
const color = tempUserCache.has(userName)
? tempUserCache.get(userName).color
: userColorCache.get(userName);
const mode = tempUserCache.has(userName)
? highlightSettings.tempMode
: highlightSettings.defaultMode;
msg.style.setProperty('--highlight-color', color);
if (
mode === HIGHLIGHT_MODES.BOTH ||
mode === HIGHLIGHT_MODES.NAME_ONLY ||
mode === HIGHLIGHT_MODES.MESSAGE_ONLY
) {
msg.setAttribute(
'data-highlight',
mode === HIGHLIGHT_MODES.BOTH ? 'both' : mode === HIGHLIGHT_MODES.NAME_ONLY ? 'name' : 'message'
);
}
}
updateMessageCounter(msg);
if (featureSettings.mentionHighlightEnabled) {
const textNodes = Array.from(messageElement.childNodes).filter(
(node) => node.nodeType === Node.TEXT_NODE && !(node.parentElement && node.parentElement.classList?.contains('emoji'))
);
const messageText = textNodes.map((node) => node.textContent.trim()).join(' ');
processMentionedUsers(
messageText,
userName,
tempUserCache.has(userName) ? tempUserCache.get(userName).color : userColorCache.get(userName)
);
}
styleCache.set(msg, true);
}
function highlightMessages(mutations) {
cleanupProcessedMessages();
const newMessages = [];
// 直接從整個 chat root 掃描未處理的訊息,避免不同 DOM 結構漏抓
const unhandled = $all(document, SEL.unhandledMessage);
unhandled.forEach((msg) => {
newMessages.push(msg);
processedMessages.set(msg, true);
});
// 保障:若仍為空,嘗試抓取尾端若干訊息
if (newMessages.length === 0) {
const tail = $all(document, SEL.messageEntry).slice(-MAX_MESSAGE_CACHE_SIZE);
tail.forEach((msg) => {
if (!processedMessages.has(msg)) {
newMessages.push(msg);
processedMessages.set(msg, true);
}
});
}
requestAnimationFrame(() => {
newMessages.forEach((msg) => processMessage(msg));
cleanupExpiredFlags();
cleanupExpiredTempUsers();
removePinnedMessage();
});
}
function handleClick(event) {
if (event.button !== 0) return;
const msgElement = event.target.closest(SEL.messageEntry);
if (!msgElement) return;
const messageElement = $(msgElement, SEL.messageBody);
if (messageElement) {
const rect = messageElement.getBoundingClientRect();
const isRightEdge = event.clientX > rect.right - 30;
if (isRightEdge) return;
}
event.stopPropagation();
event.preventDefault();
if (currentMenu && !currentMenu.contains(event.target)) closeMenu();
const authorName = $(msgElement, SEL.authorName);
const authorImg = $(msgElement, SEL.authorImg);
// 點擊頭像 -> 色彩選單或頻道(Boost Chat 未保證能抓到 channelId,僅保留原生回退)
if (authorImg && authorImg.contains(event.target)) {
if (event.ctrlKey) {
const URL =
authorName?.parentNode?.parentNode?.parentNode?.data?.authorExternalChannelId ||
authorName?.parentNode?.parentNode?.parentNode?.parentNode?.parentNode?.parentNode?.parentNode?.parentNode?.data?.authorExternalChannelId;
URL && window.open('https://www.youtube.com/channel/' + URL + '/about', '_blank');
} else {
if (featureSettings.flagMode) {
createColorMenu({ type: 'temp', name: authorName.textContent.trim() }, event);
} else {
createColorMenu({ type: 'user', name: authorName.textContent.trim() }, event);
}
}
}
// 點擊作者名 -> 自動插入 @提及
if (authorName && authorName.contains(event.target)) {
const inputField = $(document, SEL.inputField);
if (inputField) {
setTimeout(() => {
const userName = authorName.textContent.trim();
const mentionText = `@${userName}\u2009`;
const range = document.createRange();
const selection = window.getSelection();
range.selectNodeContents(inputField);
range.collapse(false);
selection.removeAllRanges();
selection.addRange(range);
inputField.focus();
document.execCommand('insertText', false, mentionText);
range.setStartAfter(inputField.lastChild);
range.collapse(true);
selection.removeAllRanges();
selection.addRange(range);
}, 200);
}
}
}
function overrideNativeListeners() {
$all(document, SEL.messageEntry).forEach((msg) => {
msg.addEventListener('click', handleClick, { capture: true });
});
}
function init() {
initializeCaches();
overrideNativeListeners();
const observer = new MutationObserver(() => {
// 直接交給 highlightMessages 自己掃,避免不同結構判斷複雜
highlightMessages([]);
// 標記新訊息,避免重複處理
$all(document, SEL.unhandledMessage).forEach((msg) => {
msg.setAttribute('data-ytcm-handled', 'true');
msg.addEventListener('click', handleClick, { capture: true });
if (!processedMessages.has(msg)) {
processedMessages.set(msg, true);
processMessage(msg, true);
}
});
});
const chatContainer = $(document, SEL.chatRoot);
if (chatContainer) {
observer.observe(chatContainer, { childList: true, subtree: true });
const existingMessages = $all(chatContainer, SEL.messageEntry);
existingMessages.forEach((msg) => {
msg.setAttribute('data-ytcm-handled', 'true');
msg.addEventListener('click', handleClick, { capture: true });
if (!processedMessages.has(msg)) {
processedMessages.set(msg, true);
processMessage(msg, true);
}
});
}
const controlPanel = createControlPanel();
return () => {
observer.disconnect();
if (controlPanel) controlPanel.remove();
closeMenu();
};
}
let cleanup = init();
const checkChatContainer = setInterval(() => {
if (document.querySelector(SEL.chatRoot) && !cleanup) {
cleanup = init();
}
}, 1000);
window.addEventListener('beforeunload', () => {
clearInterval(checkChatContainer);
cleanup?.();
});
})();
實測是沒辦法,因為問題似乎有兩大項。
一是無法覆蓋操作,腳本設計的操作是點在頭像上開啟自訂面板、點ID去操作@ID至輸入欄,大概是替換每一則消息,等待我點下去後出現對應操作,但無論是自行嘗試和上述腳本,各版本腳本在boost chat底子上都只剩一處可以操作,就是直播頻道的置頂訊息。操作已經都是boost chat的形狀了。
二是假設腳本局部生效,那沿用記錄應該能自動修改已經在名單上用戶的訊息,實際完全不行,還是只有置頂消息,boost chat的改變真是好大。
也許嘗試動boost chat能夠使我的腳本能運作,不過要嘛損及boost chat,要嘛不適合散播,好像消費的時間成本略高/收益略低了點。
暫時來說是沒辦法了。上面都是AI寫的。我沒空做這些處理
體驗好像還不錯,但很遺憾跟我的腳本完完全全不相容,因為是基於原本或者youtube-super-fast-chat聊天室勉強AI搞出來的,事到如今轉移代價好像挺大的,所以…遺憾。
(大致功能:自動著色用戶、非原生封鎖用戶、UI操作和功能切換、清理/標示洗版、發言次數統計、強化@體驗等鬼東西,
https://greasyfork.org/zh-TW/scripts/530998-youtube%E8%81%8A%E5%A4%A9%E5%AE%A4%E5%A2%9E%E5%BC%B7-youtube-chat-enhancement )