A one-of-a-kind chat management design that automatically applies the following to specified users: color-coded usernames and messages, real-time chat history, message count statistics, user blacklist, and enhanced @mentions. For other users: replaces @handles with display names, spam detection (hideable), donation/chat history overview, and auto-scroll to the latest messages.
// ==UserScript==
// @name YouTube Chat Message Manager
// @name:zh-TW Youtube 聊天消息管理者
// @author Dxzy
// @namespace http://tampermonkey.net/
// @version 21.3.11
// @description:zh-TW 僅此一家的消息管理設計,指定用戶後自動運作以下行為:用戶名稱和消息上色、發言履歷即時一覽、次數統計,用戶黑名單,@改進。對於非特定用戶:以用戶暱稱取代@帳號,檢測洗版(可隱藏),抖內消息/聊天室對話歷史一覽,自動回捲最新進度。
// @description A one-of-a-kind chat management design that automatically applies the following to specified users: color-coded usernames and messages, real-time chat history, message count statistics, user blacklist, and enhanced @mentions. For other users: replaces @handles with display names, spam detection (hideable), donation/chat history overview, and auto-scroll to the latest messages.
// @match https://www.youtube.com/live_chat*
// @grant GM_addStyle
// @license MIT
// ==/UserScript==
(function(){'use strict';
const CALLOUT_USER_EXPIRE_TIME = 30000,
EPHEMERAL_USER_DURATION = Infinity,
MAX_MESSAGE_CACHE_SIZE = 300,
CACHE_CLEANUP_INTERVAL = 60000,
DOUBLE_CLICK_DELAY = 350,
STATS_UPDATE_INTERVAL = 10000,
DISPLAY_NAME_FETCH_TIMEOUT = 20000,
DISPLAY_NAME_RETRY_INTERVAL = 60000,
DISPLAY_NAME_REQUEST_LIMIT = 10,
DISPLAY_NAME_REQUEST_WINDOW = 1000,
MAX_DISPLAY_NAME_CACHE_SIZE = 500,
MAX_SPAM_CACHE_SIZE = 4000,
AUTO_RESUME_THRESHOLD_PX = 24,
AUTO_RESUME_DELAY_MS = 2500,
AUTO_RESUME_COOLDOWN_MS = 2000,
AUTO_RESUME_CHECK_INTERVAL_MS = 5000;
const LANG = {
'zh-TW': {
buttons: {'封鎖':'封鎖','編輯':'編輯','刪除':'刪除','清除':'清除', '對話':'對話'},
tooltips: {
ephemeral: '短暫模式:切換至短暫用戶上色,開啟時上色不儲存',
pin: '清除置頂:開啟/關閉自動移除置頂訊息',
highlight: mode => `高亮模式:${mode} (雙擊切換模式)`,
block: mode => `封鎖模式:${mode} (雙擊切換模式)`,
callout: mode => `呼叫高亮:${mode} (雙擊切換模式)`,
spam: mode => `洗版過濾:${mode} (雙擊切換模式)`,
counter: '留言計數:顯示/隱藏用戶留言計數',
clearConfirm: '確定清除所有設定?',
clearButton: '確認',
pauseSingle: '隱藏控制按鈕',
pauseDouble: '雙擊暫停腳本運作',
statsTotal: '點擊切換臨時視圖:僅顯示高亮、抖內',
statsFilter: '點擊切換臨時視圖:僅顯示過濾消息'
}
},
'en': {
buttons: {'封鎖':'Block','編輯':'Edit','刪除':'Delete','清除':'Clear', '對話':'Convo'},
tooltips: {
ephemeral: 'Ephemeral mode: Switch to ephemeral 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)`,
callout: mode => `Callout 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',
pauseSingle: 'Hide control buttons',
pauseDouble: 'Double-click to pause script',
statsTotal: 'Click for temp view: Show Highlight & SC only',
statsFilter: 'Click for temp view: Show filtered messages only'
}
}
};
const currentLang = navigator.language.startsWith('zh') ? 'zh-TW' : 'en';
const COLOR_OPTIONS = {
"淺藍":"#A5CDF3", "藍色":"#62A8EA", "深藍":"#1C76CA", "紫色":"#FF00FF",
"淺綠":"#98FB98", "綠色":"#00FF00", "深綠":"#00B300", "青色":"#00FFFF",
"粉紅":"#FFC0CB", "淺紅":"#F08080", "紅色":"#FF0000", "深紅":"#8B0000",
"橙色":"#FFA500", "金色":"#FFD700", "灰色":"#BDBDBD", "深灰":"#404040"
};
const HIGHLIGHT_MODES = { BOTH:0, NAME_ONLY:1, MESSAGE_ONLY:2 },
SPAM_MODES = { MARK:0, REMOVE:1 },
BLOCK_MODES = { MARK:0, HIDE:1 };
const STORAGE_KEYS = {
USER_COLOR_SETTINGS: 'ytcm_userColorSettings',
BLOCKED_USERS: 'ytcm_blockedUsers',
FEATURE_SETTINGS: 'ytcm_featureSettings'
};
let userColorSettings = JSON.parse(localStorage.getItem(STORAGE_KEYS.USER_COLOR_SETTINGS)) || {},
blockedUsers = JSON.parse(localStorage.getItem(STORAGE_KEYS.BLOCKED_USERS)) || [],
featureSettings = JSON.parse(localStorage.getItem(STORAGE_KEYS.FEATURE_SETTINGS)) || {};
let localEphemeralMode = false;
let currentMenu = null,
ephemeralUsers = {},
ephemeralColorSettings = {},
ephemeralBlockedUsers = new Set(),
lastClickTime = 0,
clickCount = 0,
isUpdatingFromStorage = false,
mainButtonsElement = null,
controlButtonRefs = {},
isScriptPaused = false;
let statsBox = null,
frequencyDisplay = null,
spamStatsDisplay = null,
statsTotalSpan = null,
statsFilterSpan = null,
authorHoverTimer = null,
currentTooltip = null;
let cacheSizeHistory = [],
lastCacheSize = 0,
lastTotalMessageCount = 0;
let statsUpdaterInterval = null,
cacheCleanupInterval = null,
displayNameRetryInterval = null,
autoResumeInterval = null;
let totalMessageCount = 0,
totalSpamCount = 0;
let tempViewMode = null;
let lastAutoResumeTime = 0;
let autoResumePending = false;
let autoResumeDeviationStartTime = 0;
let isMouseInChat = false;
let cachedScroller = null;
let lastScrollerCheckTime = 0;
let isConfirmedNearBottom = true;
const userColorCache = new Map(),
blockedUsersSet = new Set(blockedUsers),
calloutUserCache = new Map(),
spamCache = new Map(),
processedMessages = new Map(),
processedMessageIds = new Set();
const displayNameCache = new Map(),
displayNamePending = new Map(),
displayNameFailed = new Map(),
displayNameRequestTimes = [];
const cleanupTasks = [];
function registerCleanup(fn) {
cleanupTasks.push(fn);
}
function setWithLimit(map, key, value, limit) {
if (map.size >= limit) {
const firstKey = map.keys().next().value;
map.delete(firstKey);
}
map.set(key, value);
}
function decodeHtmlEntities(text) {
if (!text) return text;
const entities = {
'&': '&',
'<': '<',
'>': '>',
'"': '"',
''': "'",
''': "'",
''': "'",
' ': ' '
};
return text.replace(/&(#(?:x[0-9a-f]+|\d+)|[a-z]+);/gi, (match) => {
return entities[match.toLowerCase()] || match;
});
}
function extractDisplayName(html) {
try {
const patterns = [
/<title>([^<]+?)\s*-\s*YouTube<\/title>/i,
/<title>([^<]+?)<\/title>/i
];
for (const pattern of patterns) {
const m = html.match(pattern);
if (m && m[1]) {
const name = decodeHtmlEntities(m[1].trim());
if (name && !name.includes('404') && !name.includes('Not Found') && name.length > 0 && name !== 'YouTube') {
return name;
}
}
}
return null;
} catch (e) {
return null;
}
}
function canMakeDisplayNameRequest() {
const now = Date.now();
while (displayNameRequestTimes.length > 0 && now - displayNameRequestTimes[0] > DISPLAY_NAME_REQUEST_WINDOW) {
displayNameRequestTimes.shift();
}
return displayNameRequestTimes.length < DISPLAY_NAME_REQUEST_LIMIT;
}
function recordDisplayNameRequest() {
displayNameRequestTimes.push(Date.now());
}
function fetchWithTimeout(url, timeout = DISPLAY_NAME_FETCH_TIMEOUT) {
return Promise.race([
fetch(url, { credentials: 'omit', cache: 'force-cache' }),
new Promise((_, reject) => setTimeout(() => reject(new Error('Timeout')), timeout))
]);
}
function batchUpdateDisplayNames(handle, displayName) {
const normalizedHandle = normalizeUserName(handle);
const messages = Array.from(document.querySelectorAll('yt-live-chat-text-message-renderer, .super-fast-chat-message, yt-live-chat-paid-message-renderer, yt-live-chat-membership-item-renderer'));
messages.forEach(msg => {
const authorNameElement = msg.querySelector('#author-name') || msg.querySelector('.author-name');
if (!authorNameElement) return;
const msgHandle = getMessageHandle(msg);
if (msgHandle && msgHandle === normalizedHandle) {
if (authorNameElement.textContent.trim() !== displayName) {
authorNameElement.textContent = displayName;
authorNameElement.setAttribute('data-original-handle', handle);
authorNameElement.setAttribute('data-ytcm-name-replaced', 'true');
}
}
});
}
function fetchDisplayName(handle) {
const normalizedHandle = handle.startsWith('@') ? handle : `@${handle}`;
if (displayNameCache.has(normalizedHandle)) {
batchUpdateDisplayNames(normalizedHandle, displayNameCache.get(normalizedHandle));
return Promise.resolve(displayNameCache.get(normalizedHandle));
}
if (displayNamePending.has(normalizedHandle)) {
return displayNamePending.get(normalizedHandle);
}
if (!canMakeDisplayNameRequest()) {
return new Promise(resolve => {
setTimeout(() => {
fetchDisplayName(handle).then(resolve);
}, DISPLAY_NAME_REQUEST_WINDOW);
});
}
recordDisplayNameRequest();
const p = fetchWithTimeout(`https://www.youtube.com/${normalizedHandle}`)
.then(r => {
if (!r.ok) {
displayNameFailed.set(normalizedHandle, Date.now());
return null;
}
return r.text();
})
.then(html => {
if (!html) {
displayNameFailed.set(normalizedHandle, Date.now());
return null;
}
const name = extractDisplayName(html);
if (name) {
setWithLimit(displayNameCache, normalizedHandle, name, MAX_DISPLAY_NAME_CACHE_SIZE);
displayNameFailed.delete(normalizedHandle);
batchUpdateDisplayNames(normalizedHandle, name);
return name;
}
displayNameFailed.set(normalizedHandle, Date.now());
return null;
})
.catch(() => {
displayNameFailed.set(normalizedHandle, Date.now());
return null;
})
.finally(() => {
displayNamePending.delete(normalizedHandle);
});
displayNamePending.set(normalizedHandle, p);
return p;
}
function retryFailedDisplayNames() {
const now = Date.now();
for (const [handle, lastFailTime] of displayNameFailed.entries()) {
if (now - lastFailTime >= DISPLAY_NAME_RETRY_INTERVAL) {
fetchDisplayName(handle);
}
}
}
function getChatScroller() {
const now = Date.now();
if (cachedScroller && now - lastScrollerCheckTime < 5000) return cachedScroller;
cachedScroller = document.querySelector('yt-live-chat-item-list-renderer #item-scroller, #chat #item-scroller') ||
document.querySelector('yt-live-chat-item-list-renderer #items, #chat #items');
lastScrollerCheckTime = now;
return cachedScroller;
}
function isNearBottom() {
const scroller = getChatScroller();
if (!scroller) return true;
return scroller.scrollHeight - scroller.scrollTop - scroller.clientHeight <= AUTO_RESUME_THRESHOLD_PX;
}
function isMenuOrDialogOpen() {
return !!currentMenu || !!currentTooltip ||
document.querySelector('.ytcm-menu') ||
document.querySelector('.ytcm-full-convo-window') ||
document.querySelector('.ytcm-notification');
}
function forceScrollToBottom() {
const scroller = getChatScroller();
if (scroller) {
scroller.scrollTop = scroller.scrollHeight;
}
}
function resetAutoResumeTimer() {
autoResumeDeviationStartTime = 0;
autoResumePending = false;
isConfirmedNearBottom = true;
}
function checkAutoResume() {
if (isScriptPaused) return;
if (isConfirmedNearBottom) {
if (!isNearBottom()) {
isConfirmedNearBottom = false;
autoResumeDeviationStartTime = Date.now();
}
return;
}
if (Date.now() - autoResumeDeviationStartTime > 30000) {
forceScrollToBottom();
resetAutoResumeTimer();
return;
}
if (isMouseInChat || isMenuOrDialogOpen()) {
autoResumePending = false;
return;
}
if (autoResumePending) return;
const now = Date.now();
if (now - lastAutoResumeTime < AUTO_RESUME_COOLDOWN_MS) return;
autoResumePending = true;
setTimeout(() => {
if (!isScriptPaused && !isNearBottom() && !isMouseInChat && !isMenuOrDialogOpen()) {
forceScrollToBottom();
lastAutoResumeTime = Date.now();
}
autoResumePending = false;
}, AUTO_RESUME_DELAY_MS);
if (isNearBottom()) {
resetAutoResumeTimer();
}
}
function startTimers() {
if (!displayNameRetryInterval) {
displayNameRetryInterval = setInterval(retryFailedDisplayNames, DISPLAY_NAME_RETRY_INTERVAL);
registerCleanup(() => { if (displayNameRetryInterval) clearInterval(displayNameRetryInterval); displayNameRetryInterval = null; });
}
if (!cacheCleanupInterval) {
cacheCleanupInterval = setInterval(() => {
const now = Date.now();
for (const user in ephemeralUsers) { if (EPHEMERAL_USER_DURATION !== Infinity && ephemeralUsers[user] <= now) { delete ephemeralUsers[user]; delete ephemeralColorSettings[user]; updateAllMessages(user); } }
for (const [user, data] of calloutUserCache.entries()) { if (data.expireTime <= now) { calloutUserCache.delete(user); updateAllMessages(user); } }
if (featureSettings.pinRemovalEnabled) { requestAnimationFrame(() => { const pinnedMessage = document.querySelector('yt-live-chat-banner-renderer'); if (pinnedMessage) pinnedMessage.style.display = 'none'; }); }
if (processedMessageIds.size > 10000) processedMessageIds.clear();
}, CACHE_CLEANUP_INTERVAL);
registerCleanup(() => { if (cacheCleanupInterval) { clearInterval(cacheCleanupInterval); cacheCleanupInterval = null; } });
}
if (!statsUpdaterInterval) {
statsUpdaterInterval = setInterval(() => {
const delta = totalMessageCount - lastTotalMessageCount;
cacheSizeHistory.push(delta);
if (cacheSizeHistory.length > 6) cacheSizeHistory.shift();
lastTotalMessageCount = totalMessageCount;
updateStatsDisplay();
}, STATS_UPDATE_INTERVAL);
registerCleanup(() => { if (statsUpdaterInterval) { clearInterval(statsUpdaterInterval); statsUpdaterInterval = null; } });
}
if (!autoResumeInterval) {
autoResumeInterval = setInterval(checkAutoResume, AUTO_RESUME_CHECK_INTERVAL_MS);
registerCleanup(() => { if (autoResumeInterval) { clearInterval(autoResumeInterval); autoResumeInterval = null; } });
}
}
function stopTimers() {
if (displayNameRetryInterval) { clearInterval(displayNameRetryInterval); displayNameRetryInterval = null; }
if (cacheCleanupInterval) { clearInterval(cacheCleanupInterval); cacheCleanupInterval = null; }
if (statsUpdaterInterval) { clearInterval(statsUpdaterInterval); statsUpdaterInterval = null; }
if (autoResumeInterval) { clearInterval(autoResumeInterval); autoResumeInterval = null; }
}
function updateMessageDisplayName(msg) {
const authorNameElement = msg.querySelector('#author-name') || msg.querySelector('.author-name');
if (!authorNameElement) return;
const originalText = authorNameElement.textContent.trim();
if (!originalText) return;
const handle = originalText.startsWith('@') ? originalText : `@${originalText}`;
if (authorNameElement.hasAttribute('data-ytcm-name-replaced')) {
if (displayNameCache.has(handle)) {
const cachedName = displayNameCache.get(handle);
if (authorNameElement.textContent !== cachedName) {
authorNameElement.textContent = cachedName;
authorNameElement.setAttribute('data-original-handle', handle);
}
}
return;
}
if (displayNameCache.has(handle)) {
batchUpdateDisplayNames(handle, displayNameCache.get(handle));
return;
}
if (!displayNamePending.has(handle)) {
fetchDisplayName(handle);
}
}
function getMessageHandle(msg) {
const authorNameElement = msg.querySelector('#author-name') || msg.querySelector('.author-name');
if (!authorNameElement) return null;
const originalHandle = authorNameElement.getAttribute('data-original-handle');
if (originalHandle) return normalizeUserName(originalHandle);
const text = authorNameElement.textContent.trim();
return normalizeUserName(text.startsWith('@') ? text : `@${text}`);
}
const defaultFeatureSettings = {
pinRemovalEnabled: true,
highlightEnabled: true,
blockEnabled: true,
buttonsVisible: true,
calloutHighlightEnabled: true,
spamFilterEnabled: true,
counterEnabled: true,
spamMode: SPAM_MODES.MARK,
blockMode: BLOCK_MODES.MARK,
defaultMode: HIGHLIGHT_MODES.BOTH,
calloutMode: HIGHLIGHT_MODES.BOTH
};
for (const [key, value] of Object.entries(defaultFeatureSettings)) {
if (featureSettings[key] === undefined) {
featureSettings[key] = value;
}
}
localStorage.setItem(STORAGE_KEYS.FEATURE_SETTINGS, JSON.stringify(featureSettings));
Object.entries(userColorSettings).forEach(([user, color]) => userColorCache.set(user, color));
function updateControlButtonStyles() {
if (!featureSettings.buttonsVisible || isScriptPaused) return;
const btnConfigs = [
{ action: '臨', enabled: localEphemeralMode },
{ action: '頂', enabled: featureSettings.pinRemovalEnabled },
{ action: '亮', enabled: featureSettings.highlightEnabled },
{ action: '封', enabled: featureSettings.blockEnabled },
{ action: '@', enabled: featureSettings.calloutHighlightEnabled },
{ action: '洗', enabled: featureSettings.spamFilterEnabled },
{ action: '數', enabled: featureSettings.counterEnabled }
];
btnConfigs.forEach(cfg => {
const btn = controlButtonRefs[cfg.action];
if (btn) {
btn.className = `ytcm-control-btn ${cfg.enabled ? 'active' : 'inactive'}`;
if (cfg.action === '亮') {
btn.title = LANG[currentLang].tooltips.highlight(
featureSettings.defaultMode === HIGHLIGHT_MODES.BOTH ? (currentLang === 'zh-TW' ? "全部高亮" : "Both") :
featureSettings.defaultMode === HIGHLIGHT_MODES.NAME_ONLY ? (currentLang === 'zh-TW' ? "僅暱稱" : "Name Only") :
(currentLang === 'zh-TW' ? "僅對話" : "Message Only")
);
} else if (cfg.action === '封') {
btn.title = LANG[currentLang].tooltips.block(
featureSettings.blockMode === BLOCK_MODES.MARK
? (currentLang === 'zh-TW' ? '標記' : 'Mark')
: (currentLang === 'zh-TW' ? '清除' : 'Clear')
);
} else if (cfg.action === '@') {
btn.title = LANG[currentLang].tooltips.callout(
featureSettings.calloutMode === HIGHLIGHT_MODES.BOTH ? (currentLang === 'zh-TW' ? "全部高亮" : "Both") :
featureSettings.calloutMode === HIGHLIGHT_MODES.NAME_ONLY ? (currentLang === 'zh-TW' ? "僅暱稱" : "Name Only") :
(currentLang === 'zh-TW' ? "僅對話" : "Message Only")
);
} else if (cfg.action === '洗') {
btn.title = LANG[currentLang].tooltips.spam(
featureSettings.spamMode === SPAM_MODES.MARK
? (currentLang === 'zh-TW' ? '標記' : 'Mark')
: (currentLang === 'zh-TW' ? '清除' : 'Clear')
);
}
}
});
}
function updateStatsDisplay() {
if (statsBox && frequencyDisplay) {
const msgsPerMin = cacheSizeHistory.reduce((a, b) => a + b, 0);
frequencyDisplay.textContent = `Msgs: ${msgsPerMin}`;
}
if (statsTotalSpan) statsTotalSpan.textContent = `Total: ${totalMessageCount}`;
if (statsFilterSpan) statsFilterSpan.textContent = ` Filter: ${totalSpamCount}`;
}
function syncStorage(key, skipUpdate = false) {
if (isUpdatingFromStorage) return;
isUpdatingFromStorage = true;
try {
if (key === STORAGE_KEYS.USER_COLOR_SETTINGS || key === null) {
const data = JSON.parse(localStorage.getItem(STORAGE_KEYS.USER_COLOR_SETTINGS)) || {};
userColorSettings = data;
userColorCache.clear();
Object.entries(userColorSettings).forEach(([user, color]) => userColorCache.set(user, color));
}
if (key === STORAGE_KEYS.BLOCKED_USERS || key === null) {
const data = JSON.parse(localStorage.getItem(STORAGE_KEYS.BLOCKED_USERS)) || [];
blockedUsers = data;
blockedUsersSet.clear();
blockedUsers.forEach(user => blockedUsersSet.add(user));
}
if (key === STORAGE_KEYS.FEATURE_SETTINGS || key === null) {
const data = JSON.parse(localStorage.getItem(STORAGE_KEYS.FEATURE_SETTINGS)) || {};
featureSettings = data;
if (mainButtonsElement) {
mainButtonsElement.classList.toggle('hidden', !featureSettings.buttonsVisible || isScriptPaused);
}
updateControlButtonStyles();
}
if (!skipUpdate && (key === STORAGE_KEYS.USER_COLOR_SETTINGS || key === STORAGE_KEYS.BLOCKED_USERS)) {
updateAllMessages();
}
} finally {
isUpdatingFromStorage = false;
}
}
function saveAndApplyFeatures(needUpdateMessages = true) {
localStorage.setItem(STORAGE_KEYS.FEATURE_SETTINGS, JSON.stringify(featureSettings));
updateControlButtonStyles();
updateStatsDisplay();
if (needUpdateMessages) {
updateAllMessages();
}
}
window.addEventListener('storage', (e) => {
if (e.key === STORAGE_KEYS.USER_COLOR_SETTINGS || e.key === STORAGE_KEYS.BLOCKED_USERS || e.key === STORAGE_KEYS.FEATURE_SETTINGS) {
syncStorage(e.key, true);
if (e.key === STORAGE_KEYS.USER_COLOR_SETTINGS || e.key === STORAGE_KEYS.BLOCKED_USERS) {
updateAllMessages();
}
if (e.key === STORAGE_KEYS.FEATURE_SETTINGS) {
if (mainButtonsElement) {
mainButtonsElement.classList.toggle('hidden', !featureSettings.buttonsVisible || isScriptPaused);
}
updateControlButtonStyles();
updateAllMessages();
updateStatsDisplay();
}
}
});
GM_addStyle(`
:root{--highlight-color:inherit}
.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;contain:content}
.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:3px;background-color:#f0f0f0;border-radius:3px;margin:2px;font-size:12px}
.ytcm-button{cursor:pointer;padding:5px 8px;margin:5px 2px 0 2px;border-radius:3px;border:1px solid #ccc;background-color:#f8f8f8;font-size:12px}
.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:0;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:flex;flex-direction:column;gap:0}
.ytcm-main-buttons.hidden{display:none!important}
.ytcm-message-count{position:absolute;top:4px;right:4px;font-size:8px;background:#000;color:#fff!important;font-weight:normal!important;padding:1px 4px;border-radius:3px;z-index:10;line-height:1.2}
.ytcm-stats-box{position:fixed;top:-4px;left:100px;z-index:9999;font-size:12px;font-weight:bold;color:white;background:rgba(0,0,0,0.7);padding:4px 8px;border-radius:4px;cursor:default;contain:content;pointer-events:none;white-space:nowrap}
.ytcm-stats-box div{margin: 2px 0;}
.ytcm-stat-clickable{cursor:pointer;text-decoration:none;pointer-events:auto;}
.ytcm-stat-clickable:hover, .ytcm-stat-clickable.active{color:#ffd700;}
.ytcm-temp-hide{display:none!important;}
.ytcm-temp-view-filter[data-blocked="true"][data-block-mode="hide"]{display:flex!important}
.ytcm-temp-view-filter[data-blocked="true"][data-block-mode="mark"]{background-color:transparent!important}
.ytcm-temp-view-filter[data-spam="true"].spam-marked #author-name,
.ytcm-temp-view-filter[data-spam="true"].spam-marked #message{color:inherit!important}
.ytcm-temp-view-filter[data-highlight] #author-name, .ytcm-temp-view-filter[data-highlight] #message { color: inherit !important; font-weight: normal !important; }
.ytcm-temp-view-filter[data-ephemeral] #author-name, .ytcm-temp-view-filter[data-ephemeral] #message { color: inherit !important; font-weight: normal !important; }
.ytcm-temp-view-filter[data-callout-highlight] #author-name, .ytcm-temp-view-filter[data-callout-highlight] #message { color: inherit !important; font-weight: normal !important; }
yt-live-chat-text-message-renderer, .super-fast-chat-message, yt-live-chat-paid-message-renderer, yt-live-chat-membership-item-renderer{position:relative}
#message{position:relative}
[data-blocked="true"][data-block-mode="mark"]{background-color:#303030 !important}
[data-blocked="true"][data-block-mode="hide"]{display:none!important}
[data-spam="true"]{display:none!important}
[data-spam="true"].spam-marked{display:flex!important;}
[data-spam="true"].spam-marked #author-name,[data-spam="true"].spam-marked #message{color:#404040!important}
[data-highlight="name"] #author-name,[data-highlight="both"] #author-name{color:var(--highlight-color)!important;font-weight:bold!important}
[data-highlight="message"] #message,[data-highlight="both"] #message{color:var(--highlight-color)!important;font-weight:bold!important}
[data-ephemeral="true"] #author-name,[data-ephemeral="true"] #message{color:var(--ephemeral-color)!important;font-weight:bold!important;opacity:var(--ephemeral-opacity,1)}
[data-callout-highlight="name"] #author-name,[data-callout-highlight="both"] #author-name{color:var(--highlight-color)!important;font-weight:bold!important}
[data-callout-highlight="message"] #message,[data-callout-highlight="both"] #message{color:var(--highlight-color)!important;font-weight:bold!important}
.ytcm-spam-tooltip{position:absolute;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;font-size:12px;max-width:90vw;max-height:80vh;overflow-y:auto;word-wrap:break-word;white-space:normal;box-sizing:border-box;contain:content}
.ytcm-notification{position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);background-color:rgba(0,0,0,0.7);color:white;padding:10px 20px;border-radius:5px;z-index:10000;font-size:14px;contain:content}
.ytcm-full-convo-window{position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.9);z-index:10001;color:white;font-size:14px;overflow:hidden;display:flex;flex-direction:column;padding:20px;box-sizing:border-box;contain:layout}
.ytcm-full-convo-header{display:flex;justify-content:flex-end;margin-bottom:10px;align-items:center;}
.ytcm-full-convo-btn{margin-left:10px;padding:5px 10px;background-color:#333;color:white;border:none;cursor:pointer;border-radius:3px}
.ytcm-full-convo-content{overflow-y:auto;flex-grow:1;line-height:1.4;white-space:pre-wrap}
.ytcm-convo-filter{margin-left:10px;padding:5px;background-color:#333;color:white;border:none;border-radius:3px;}
`);
function normalizeUserName(userName) {
return userName.startsWith('@') ? userName.substring(1) : userName;
}
function parseSpamKey(key) {
const firstColonIndex = key.indexOf(':');
if (firstColonIndex === -1) {
return { user: key, message: '' };
}
const user = key.substring(0, firstColonIndex);
const message = key.substring(firstColonIndex + 1);
return { user, message };
}
function getContrastColor(hexColor) {
const r = parseInt(hexColor.substr(1, 2), 16);
const g = parseInt(hexColor.substr(3, 2), 16);
const b = parseInt(hexColor.substr(5, 2), 16);
const brightness = (r * 299 + g * 587 + b * 114) / 1000;
return brightness > 128 ? '#000000' : '#ffffff';
}
function updateAllMessages(userName) {
if (isScriptPaused) return;
const normalizedUserName = userName ? normalizeUserName(userName) : null;
const messages = Array.from(document.querySelectorAll('yt-live-chat-text-message-renderer, .super-fast-chat-message, yt-live-chat-paid-message-renderer, yt-live-chat-membership-item-renderer'))
.filter(msg => {
const nameElement = msg.querySelector('#author-name') || msg.querySelector('.author-name');
if (!nameElement) return false;
if (normalizedUserName) {
const currentText = nameElement.textContent.trim();
const originalHandle = nameElement.getAttribute('data-original-handle');
const msgHandle = originalHandle ? normalizeUserName(originalHandle) : normalizeUserName(currentText);
return msgHandle === normalizedUserName;
}
return true;
});
messages.forEach(msg => {
processedMessages.delete(msg);
const counterEl = msg.querySelector('.ytcm-message-count');
if (counterEl) counterEl.remove();
});
requestAnimationFrame(() => {
messages.forEach(msg => processMessage(msg, false));
});
}
function ensureStatsDisplay() {
if (!statsBox) {
statsBox = document.createElement('div');
statsBox.className = 'ytcm-stats-box';
frequencyDisplay = document.createElement('div');
frequencyDisplay.textContent = `Msgs: 0`;
spamStatsDisplay = document.createElement('div');
statsTotalSpan = document.createElement('span');
statsTotalSpan.className = 'ytcm-stat-clickable ytcm-stat-total';
statsTotalSpan.textContent = `Total: 0`;
statsTotalSpan.title = LANG[currentLang].tooltips.statsTotal;
statsTotalSpan.onclick = () => {
if (tempViewMode === 'total') {
tempViewMode = null;
} else {
tempViewMode = 'total';
}
if (tempViewMode === 'total') {
statsTotalSpan.classList.add('active');
statsFilterSpan.classList.remove('active');
} else {
statsTotalSpan.classList.remove('active');
if (tempViewMode === 'filter') statsFilterSpan.classList.add('active');
else statsFilterSpan.classList.remove('active');
}
updateAllMessages();
};
statsFilterSpan = document.createElement('span');
statsFilterSpan.className = 'ytcm-stat-clickable ytcm-stat-filter';
statsFilterSpan.textContent = ` Filter: 0`;
statsFilterSpan.title = LANG[currentLang].tooltips.statsFilter;
statsFilterSpan.onclick = () => {
if (tempViewMode === 'filter') {
tempViewMode = null;
} else {
tempViewMode = 'filter';
}
if (tempViewMode === 'filter') {
statsFilterSpan.classList.add('active');
statsTotalSpan.classList.remove('active');
} else {
statsFilterSpan.classList.remove('active');
if (tempViewMode === 'total') statsTotalSpan.classList.add('active');
else statsTotalSpan.classList.remove('active');
}
updateAllMessages();
};
spamStatsDisplay.appendChild(statsTotalSpan);
spamStatsDisplay.appendChild(statsFilterSpan);
statsBox.appendChild(frequencyDisplay);
statsBox.appendChild(spamStatsDisplay);
document.body.appendChild(statsBox);
}
startTimers();
}
function createControlPanel() {
const panel = document.createElement('div');
panel.className = 'ytcm-control-panel';
const mainButtons = document.createElement('div');
mainButtons.className = 'ytcm-main-buttons';
if (!featureSettings.buttonsVisible) mainButtons.classList.add('hidden');
mainButtonsElement = mainButtons;
const buttons = [
{
text: '臨',
className: `ytcm-control-btn ${localEphemeralMode ? 'active' : 'inactive'}`,
title: LANG[currentLang].tooltips.ephemeral,
onClick: () => handleButtonClick('臨', () => {
localEphemeralMode = !localEphemeralMode;
updateControlButtonStyles();
})
},
{
text: '頂',
className: `ytcm-control-btn ${featureSettings.pinRemovalEnabled ? 'active' : 'inactive'}`,
title: LANG[currentLang].tooltips.pin,
onClick: () => handleButtonClick('頂', () => {
featureSettings.pinRemovalEnabled = !featureSettings.pinRemovalEnabled;
saveAndApplyFeatures(false);
const pinnedMessage = document.querySelector('yt-live-chat-banner-renderer');
if (pinnedMessage) pinnedMessage.style.display = featureSettings.pinRemovalEnabled ? 'none' : '';
})
},
{
text: '亮',
className: `ytcm-control-btn ${featureSettings.highlightEnabled ? 'active' : 'inactive'}`,
title: LANG[currentLang].tooltips.highlight(
featureSettings.defaultMode === HIGHLIGHT_MODES.BOTH ? (currentLang === 'zh-TW' ? "全部高亮" : "Both") :
featureSettings.defaultMode === HIGHLIGHT_MODES.NAME_ONLY ? (currentLang === 'zh-TW' ? "僅暱稱" : "Name Only") :
(currentLang === 'zh-TW' ? "僅對話" : "Message Only")
),
onClick: () => handleButtonClick(
'亮',
() => {
featureSettings.highlightEnabled = !featureSettings.highlightEnabled;
saveAndApplyFeatures(true);
},
() => {
featureSettings.defaultMode = (featureSettings.defaultMode + 1) % 3;
saveAndApplyFeatures(true);
}
)
},
{
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(
'封',
() => {
const wasEnabled = featureSettings.blockEnabled;
const wasHideMode = featureSettings.blockMode === BLOCK_MODES.HIDE;
featureSettings.blockEnabled = !featureSettings.blockEnabled;
saveAndApplyFeatures(true);
if (wasEnabled && !featureSettings.blockEnabled && wasHideMode) {
document.querySelectorAll('[data-blocked="true"][data-block-mode="hide"]').forEach(msg => {
msg.style.display = '';
msg.setAttribute('data-block-mode', 'mark');
});
}
},
() => {
const oldMode = featureSettings.blockMode;
featureSettings.blockMode = (featureSettings.blockMode + 1) % 2;
saveAndApplyFeatures(true);
if (featureSettings.blockEnabled) {
const selector = oldMode === BLOCK_MODES.MARK ? '[data-block-mode="mark"]' : '[data-block-mode="hide"]';
const newMode = featureSettings.blockMode === BLOCK_MODES.MARK ? 'mark' : 'hide';
document.querySelectorAll(`[data-blocked="true"]${selector}`).forEach(msg => {
msg.setAttribute('data-block-mode', newMode);
});
}
}
)
},
{
text: '@',
className: `ytcm-control-btn ${featureSettings.calloutHighlightEnabled ? 'active' : 'inactive'}`,
title: LANG[currentLang].tooltips.callout(
featureSettings.calloutMode === HIGHLIGHT_MODES.BOTH ? (currentLang === 'zh-TW' ? "全部高亮" : "Both") :
featureSettings.calloutMode === HIGHLIGHT_MODES.NAME_ONLY ? (currentLang === 'zh-TW' ? "僅暱稱" : "Name Only") :
(currentLang === 'zh-TW' ? "僅對話" : "Message Only")
),
onClick: () => handleButtonClick(
'@',
() => {
featureSettings.calloutHighlightEnabled = !featureSettings.calloutHighlightEnabled;
saveAndApplyFeatures(true);
if (!featureSettings.calloutHighlightEnabled) calloutUserCache.clear();
},
() => {
const oldMode = featureSettings.calloutMode;
featureSettings.calloutMode = (featureSettings.calloutMode + 1) % 3;
saveAndApplyFeatures(true);
if (featureSettings.calloutHighlightEnabled) {
let newAttr = 'both';
if (oldMode === HIGHLIGHT_MODES.BOTH) newAttr = 'name';
else if (oldMode === HIGHLIGHT_MODES.NAME_ONLY) newAttr = 'message';
document.querySelectorAll('[data-callout-highlight]').forEach(msg => {
msg.setAttribute('data-callout-highlight', newAttr);
msg.setAttribute('data-highlight', newAttr);
});
}
}
)
},
{
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(
'洗',
() => {
const wasEnabled = featureSettings.spamFilterEnabled;
featureSettings.spamFilterEnabled = !featureSettings.spamFilterEnabled;
saveAndApplyFeatures(true);
if (wasEnabled && !featureSettings.spamFilterEnabled) {
document.querySelectorAll('[data-spam="true"]').forEach(msg => {
msg.removeAttribute('data-spam');
msg.classList.remove('spam-marked');
});
}
},
() => {
featureSettings.spamMode = (featureSettings.spamMode + 1) % 2;
saveAndApplyFeatures(true);
}
)
},
{
text: '數',
className: `ytcm-control-btn ${featureSettings.counterEnabled ? 'active' : 'inactive'}`,
title: LANG[currentLang].tooltips.counter,
onClick: () => handleButtonClick('數', () => {
featureSettings.counterEnabled = !featureSettings.counterEnabled;
saveAndApplyFeatures(true);
if (!featureSettings.counterEnabled) {
document.querySelectorAll('.ytcm-message-count').forEach(el => el.remove());
}
})
}
];
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);
controlButtonRefs[btn.text] = button;
mainButtons.appendChild(button);
});
const toggleBtn = document.createElement('div');
toggleBtn.className = 'ytcm-toggle-btn';
toggleBtn.textContent = '☑';
toggleBtn.title = currentLang === 'zh-TW' ? LANG[currentLang].tooltips.pauseSingle + '\n' + LANG[currentLang].tooltips.pauseDouble : LANG.en.tooltips.pauseSingle + '\n' + LANG.en.tooltips.pauseDouble;
toggleBtn.addEventListener('click', () => {
if (isScriptPaused) {
isScriptPaused = false;
mainButtons.classList.toggle('hidden', !featureSettings.buttonsVisible);
if (statsBox) statsBox.style.display = '';
updateControlButtonStyles();
updateStatsDisplay();
startTimers();
document.querySelectorAll('yt-live-chat-text-message-renderer, .super-fast-chat-message, yt-live-chat-paid-message-renderer, yt-live-chat-membership-item-renderer').forEach(msg => {
if (!processedMessages.has(msg)) {
processMessage(msg, true);
}
});
return;
}
const now = Date.now();
if (now - lastClickTime < DOUBLE_CLICK_DELAY) {
clickCount++;
if (clickCount === 2) {
isScriptPaused = true;
mainButtons.classList.add('hidden');
if (statsBox) statsBox.style.display = 'none';
stopTimers();
clickCount = 0;
}
} else {
clickCount = 1;
setTimeout(() => {
if (clickCount === 1) {
featureSettings.buttonsVisible = !featureSettings.buttonsVisible;
localStorage.setItem(STORAGE_KEYS.FEATURE_SETTINGS, JSON.stringify(featureSettings));
mainButtons.classList.toggle('hidden', !featureSettings.buttonsVisible);
}
clickCount = 0;
}, DOUBLE_CLICK_DELAY);
}
lastClickTime = now;
});
panel.appendChild(mainButtons);
panel.appendChild(toggleBtn);
document.body.appendChild(panel);
return panel;
}
function handleButtonClick(btnText, toggleAction, modeAction) {
if (isScriptPaused) return;
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 cleanupProcessedMessages() {
const allMessages = new Set(document.querySelectorAll('yt-live-chat-text-message-renderer, .super-fast-chat-message, yt-live-chat-paid-message-renderer, yt-live-chat-membership-item-renderer'));
const toDelete = [];
processedMessages.forEach((_, msg) => {
if (!allMessages.has(msg)) toDelete.push(msg);
});
toDelete.forEach(msg => {
processedMessages.delete(msg);
});
}
function processCalloutUsers(messageText, authorName, authorColor) {
if (!featureSettings.calloutHighlightEnabled || !authorColor) return;
const mentionRegex = /@([^\s].*?(?=\s|$|@|[\u200b]))/g;
let match;
const mentionedUsers = new Set();
while ((match = mentionRegex.exec(messageText)) !== null) {
if (match[1]) mentionedUsers.add(match[1].trim());
}
if (mentionedUsers.size !== 1) return;
const mentionedUser = Array.from(mentionedUsers)[0];
const allUsers = Array.from(document.querySelectorAll('#author-name, .author-name'));
const existingUsers = allUsers.map(el => {
const originalHandle = el.getAttribute('data-original-handle');
return originalHandle ? normalizeUserName(originalHandle) : normalizeUserName(el.textContent.trim());
});
const cleanMentionedUser = normalizeUserName(mentionedUser);
const isExistingUser = existingUsers.some(user => user.toLowerCase() === cleanMentionedUser.toLowerCase());
if (isExistingUser && !userColorCache.has(cleanMentionedUser) && !calloutUserCache.has(cleanMentionedUser)) {
calloutUserCache.set(cleanMentionedUser, {
color: authorColor,
expireTime: Date.now() + CALLOUT_USER_EXPIRE_TIME,
highlightMode: featureSettings.calloutMode
});
updateAllMessages(cleanMentionedUser);
}
}
function closeMenu() {
if (currentMenu) {
document.body.removeChild(currentMenu);
currentMenu = null;
}
}
function createFullConvoWindow() {
closeMenu();
const windowEl = document.createElement('div');
windowEl.className = 'ytcm-full-convo-window';
const header = document.createElement('div');
header.className = 'ytcm-full-convo-header';
const closeBtn = document.createElement('button');
closeBtn.className = 'ytcm-full-convo-btn';
closeBtn.textContent = currentLang === 'zh-TW' ? '關閉' : 'Close';
closeBtn.onclick = () => document.body.removeChild(windowEl);
const copyBtn = document.createElement('button');
copyBtn.className = 'ytcm-full-convo-btn';
copyBtn.textContent = currentLang === 'zh-TW' ? '複製' : 'Copy';
copyBtn.onclick = () => {
const filter = convoFilterSelect.value;
const textToCopy = Array.from(spamCache.entries())
.filter(([key, val]) => {
if (filter === 'superchat') return val.type === 'superchat';
return true;
})
.sort((a, b) => a[1].t - b[1].t)
.map(([key, val]) => {
const { user, message } = parseSpamKey(key);
return `[${new Date(val.t).toLocaleString()}] ${normalizeUserName(user)}: ${message}`;
})
.join('\n');
navigator.clipboard.writeText(textToCopy).then(() => {
showTemporaryNotification(currentLang === 'zh-TW' ? '對話記錄已複製' : 'Conversation copied');
}).catch(err => console.error('Failed to copy conversation:', err));
};
const exportBtn = document.createElement('button');
exportBtn.className = 'ytcm-full-convo-btn';
exportBtn.textContent = currentLang === 'zh-TW' ? '匯出' : 'Export';
exportBtn.onclick = () => {
const filter = convoFilterSelect.value;
const textToExport = Array.from(spamCache.entries())
.filter(([key, val]) => {
if (filter === 'superchat') return val.type === 'superchat';
return true;
})
.sort((a, b) => a[1].t - b[1].t)
.map(([key, val]) => {
const { user, message } = parseSpamKey(key);
return `[${new Date(val.t).toLocaleString()}] ${normalizeUserName(user)}: ${message}`;
})
.join('\n');
const blob = new Blob([textToExport], { type: 'text/plain' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
const now = new Date();
a.download = `yt_convo_${now.getFullYear()}${String(now.getMonth() + 1).padStart(2, '0')}${String(now.getDate()).padStart(2, '0')}_${String(now.getHours()).padStart(2, '0')}${String(now.getMinutes()).padStart(2, '0')}.txt`;
a.click();
URL.revokeObjectURL(url);
};
const convoFilterSelect = document.createElement('select');
convoFilterSelect.className = 'ytcm-convo-filter';
const options = [
{ value: 'default', text: currentLang === 'zh-TW' ? '所有紀錄' : 'All Records' },
{ value: 'superchat', text: currentLang === 'zh-TW' ? '抖內' : 'SuperChat' }
];
options.forEach(opt => {
const option = document.createElement('option');
option.value = opt.value;
option.textContent = opt.text;
convoFilterSelect.appendChild(option);
});
convoFilterSelect.onchange = () => {
renderConvoContent(convoFilterSelect.value, content);
};
header.appendChild(convoFilterSelect);
header.appendChild(exportBtn);
header.appendChild(copyBtn);
header.appendChild(closeBtn);
const content = document.createElement('div');
content.className = 'ytcm-full-convo-content';
windowEl.appendChild(header);
windowEl.appendChild(content);
document.body.appendChild(windowEl);
renderConvoContent('default', content);
setTimeout(() => content.scrollTop = content.scrollHeight, 10);
}
function renderConvoContent(filter, contentEl) {
contentEl.textContent = Array.from(spamCache.entries())
.filter(([key, val]) => {
if (filter === 'superchat') return val.type === 'superchat';
return true;
})
.sort((a, b) => a[1].t - b[1].t)
.map(([key, val]) => {
const { user, message } = parseSpamKey(key);
return `${normalizeUserName(user)}: ${message}`;
}).join('\n');
}
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', () => {
const userName = normalizeUserName(targetElement.name);
if (targetElement.type === 'user') {
userColorSettings[userName] = colorValue;
userColorCache.set(userName, colorValue);
updateAllMessages(userName);
localStorage.setItem(STORAGE_KEYS.USER_COLOR_SETTINGS, JSON.stringify(userColorSettings));
} else if (targetElement.type === 'temp') {
if (localEphemeralMode) {
ephemeralUsers[userName] = Date.now() + EPHEMERAL_USER_DURATION;
ephemeralColorSettings[userName] = colorValue;
calloutUserCache.set(userName, { color: colorValue, expireTime: Date.now() + EPHEMERAL_USER_DURATION, highlightMode: HIGHLIGHT_MODES.BOTH });
} else {
calloutUserCache.set(userName, { color: colorValue, expireTime: Date.now() + CALLOUT_USER_EXPIRE_TIME, highlightMode: HIGHLIGHT_MODES.BOTH });
}
updateAllMessages(userName);
}
closeMenu();
});
colorGrid.appendChild(colorItem);
});
const buttonRow = document.createElement('div');
buttonRow.className = 'ytcm-button-row';
const buttons = [
{
text: LANG[currentLang].buttons.封鎖,
className: 'ytcm-button',
onClick: () => {
const userName = normalizeUserName(targetElement.name);
if (targetElement.type === 'user') {
blockedUsers.push(userName);
blockedUsersSet.add(userName);
localStorage.setItem(STORAGE_KEYS.BLOCKED_USERS, JSON.stringify(blockedUsers));
} else if (targetElement.type === 'temp' && localEphemeralMode) {
ephemeralBlockedUsers.add(userName);
}
updateAllMessages(userName);
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 = normalizeUserName(targetElement.name);
let foundInList = false;
if (userColorSettings[userName]) { delete userColorSettings[userName]; userColorCache.delete(userName); foundInList = true; localStorage.setItem(STORAGE_KEYS.USER_COLOR_SETTINGS, JSON.stringify(userColorSettings)); }
if (blockedUsersSet.has(userName)) { blockedUsers = blockedUsers.filter(u => normalizeUserName(u) !== userName); blockedUsersSet.delete(userName); foundInList = true; localStorage.setItem(STORAGE_KEYS.BLOCKED_USERS, JSON.stringify(blockedUsers)); }
if (calloutUserCache.has(userName)) { calloutUserCache.delete(userName); foundInList = true; }
if (ephemeralUsers[userName]) { delete ephemeralUsers[userName]; foundInList = true; }
if (ephemeralColorSettings[userName]) { delete ephemeralColorSettings[userName]; foundInList = true; }
if (ephemeralBlockedUsers.has(userName)) { ephemeralBlockedUsers.delete(userName); foundInList = true; }
const messages = Array.from(document.querySelectorAll('yt-live-chat-text-message-renderer, .super-fast-chat-message, yt-live-chat-paid-message-renderer, yt-live-chat-membership-item-renderer')).filter(msg => {
const nameElement = msg.querySelector('#author-name') || msg.querySelector('.author-name');
const msgHandle = getMessageHandle(msg);
return msgHandle && msgHandle === userName;
});
messages.forEach(msg => {
if (foundInList) {
msg.removeAttribute('data-highlight'); msg.removeAttribute('data-ephemeral'); msg.removeAttribute('data-blocked'); msg.removeAttribute('data-spam');
msg.classList.remove('spam-marked');
msg.style.removeProperty('--highlight-color'); msg.style.removeProperty('--ephemeral-color');
msg.querySelector('.ytcm-message-count')?.remove();
} else { msg.style.display = 'none'; }
});
closeMenu();
}
},
{
text: LANG[currentLang].buttons.對話,
className: 'ytcm-button',
onClick: () => createFullConvoWindow()
}
];
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;
const menuRect = menu.getBoundingClientRect();
const viewportHeight = window.innerHeight;
let adjustedTop = parseFloat(menu.style.top);
if (menuRect.bottom > viewportHeight) adjustedTop = adjustedTop - (menuRect.bottom - viewportHeight) - 10;
if (adjustedTop < 0) adjustedTop = 10;
menu.style.top = `${adjustedTop}px`;
let isMouseOverMenu = false;
const mouseEnterHandler = () => { isMouseOverMenu = true; };
const mouseLeaveHandler = () => {
isMouseOverMenu = false;
setTimeout(() => {
if (!isMouseOverMenu) {
closeMenu();
}
}, 100);
};
menu.addEventListener('mouseenter', mouseEnterHandler);
menu.addEventListener('mouseleave', mouseLeaveHandler);
}
function showTemporaryNotification(text) {
const notification = document.createElement('div');
notification.textContent = text;
notification.className = 'ytcm-notification';
document.body.appendChild(notification);
setTimeout(() => { if (notification.parentNode) notification.parentNode.removeChild(notification); }, 2000);
}
function createEditMenu(targetElement, event) {
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 buttonRow = document.createElement('div');
buttonRow.className = 'ytcm-button-row';
const buttons = [
{ text: currentLang === 'zh-TW' ? '關閉' : 'Close', className: 'ytcm-button', onClick: closeMenu },
{
text: currentLang === 'zh-TW' ? '匯出設定' : 'Export', className: 'ytcm-button',
onClick: () => {
const data = { userColorSettings, blockedUsers };
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);
}
},
{
text: currentLang === 'zh-TW' ? '選擇檔案' : 'Import', className: 'ytcm-button',
onClick: () => {
const fileInput = document.createElement('input'); fileInput.type = 'file'; fileInput.accept = '.json';
fileInput.onchange = (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);
const newUserColorSettings = {};
for (const [key, value] of Object.entries(data.userColorSettings)) newUserColorSettings[normalizeUserName(key)] = value;
const newBlockedUsers = data.blockedUsers.map(normalizeUserName);
localStorage.setItem(STORAGE_KEYS.USER_COLOR_SETTINGS, JSON.stringify(newUserColorSettings));
localStorage.setItem(STORAGE_KEYS.BLOCKED_USERS, JSON.stringify(newBlockedUsers));
localStorage.setItem(STORAGE_KEYS.FEATURE_SETTINGS, JSON.stringify(featureSettings));
userColorSettings = newUserColorSettings; blockedUsers = newBlockedUsers;
userColorCache.clear(); Object.entries(userColorSettings).forEach(([user, color]) => userColorCache.set(user, color));
blockedUsersSet.clear(); blockedUsers.forEach(user => blockedUsersSet.add(user));
updateAllMessages();
} catch (err) { console.error("Invalid file format:", err); }
};
reader.readAsText(file); closeMenu(); showTemporaryNotification(currentLang === 'zh-TW' ? '名單已匯入' : 'Lists imported');
};
fileInput.click();
}
},
{
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(STORAGE_KEYS.USER_COLOR_SETTINGS); localStorage.removeItem(STORAGE_KEYS.BLOCKED_USERS); localStorage.removeItem(STORAGE_KEYS.FEATURE_SETTINGS); 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.style.flex = '1'; button.style.margin = '0 1px'; button.addEventListener('click', btn.onClick); buttonRow.appendChild(button);
});
menu.appendChild(buttonRow);
const ephemeralUserList = document.createElement('div');
ephemeralUserList.textContent = currentLang === 'zh-TW' ? '臨時名單:' : 'Temporary List:';
ephemeralUserList.className = 'ytcm-flex-wrap';
const ephemeralUsersSet = new Set([...Object.keys(ephemeralColorSettings), ...ephemeralBlockedUsers]);
ephemeralUsersSet.forEach(user => {
const userItem = document.createElement('div');
userItem.className = 'ytcm-list-item';
userItem.textContent = user;
const color = ephemeralColorSettings[user];
if (color) {
userItem.style.backgroundColor = color;
userItem.style.color = getContrastColor(color);
}
userItem.addEventListener('click', () => {
if (ephemeralBlockedUsers.has(user)) {
blockedUsers.push(user);
blockedUsersSet.add(user);
ephemeralBlockedUsers.delete(user);
localStorage.setItem(STORAGE_KEYS.BLOCKED_USERS, JSON.stringify(blockedUsers));
}
if (ephemeralColorSettings[user]) {
userColorSettings[user] = ephemeralColorSettings[user];
userColorCache.set(user, ephemeralColorSettings[user]);
delete ephemeralColorSettings[user];
localStorage.setItem(STORAGE_KEYS.USER_COLOR_SETTINGS, JSON.stringify(userColorSettings));
}
if (ephemeralUsers[user]) delete ephemeralUsers[user];
updateAllMessages();
userItem.remove();
});
ephemeralUserList.appendChild(userItem);
});
menu.appendChild(ephemeralUserList);
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 => normalizeUserName(u) !== user); blockedUsersSet.delete(user); localStorage.setItem(STORAGE_KEYS.BLOCKED_USERS, 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;
const userColor = userColorCache.get(user) || userColorSettings[user];
if (userColor) { userItem.style.backgroundColor = userColor; userItem.style.color = getContrastColor(userColor); }
userItem.addEventListener('click', () => { delete userColorSettings[user]; userColorCache.delete(user); localStorage.setItem(STORAGE_KEYS.USER_COLOR_SETTINGS, JSON.stringify(userColorSettings)); userItem.remove(); updateAllMessages(user); });
coloredUserList.appendChild(userItem);
});
menu.appendChild(coloredUserList);
document.body.appendChild(menu); currentMenu = menu;
let isMouseOverMenu = false;
const mouseEnterHandler = () => { isMouseOverMenu = true; };
const mouseLeaveHandler = () => {
isMouseOverMenu = false;
setTimeout(() => {
if (!isMouseOverMenu) {
closeMenu();
}
}, 100);
};
menu.addEventListener('mouseenter', mouseEnterHandler);
menu.addEventListener('mouseleave', mouseLeaveHandler);
}
function updateMessageCounter(msg) {
if (!featureSettings.counterEnabled) return;
const userName = getMessageHandle(msg); if (!userName) return;
let count = 0;
for (const [key, val] of spamCache.entries()) {
if (typeof val === 'object' && key.startsWith(`${userName}:`)) count++;
}
const existingCounter = msg.querySelector('.ytcm-message-count'); if (existingCounter) existingCounter.remove();
const counterSpan = document.createElement('span'); counterSpan.className = 'ytcm-message-count'; counterSpan.textContent = count; msg.appendChild(counterSpan);
}
function processMessage(msg, updateStats = true) {
if (processedMessages.has(msg)) return;
if (isScriptPaused) {
processedMessages.set(msg, true);
return;
}
if (updateStats) totalMessageCount++;
const isSuperChatOrMembership = msg.matches('yt-live-chat-paid-message-renderer') || msg.matches('yt-live-chat-membership-item-renderer');
const authorNameElement = msg.querySelector('#author-name') || msg.querySelector('.author-name');
const messageElement = msg.querySelector('#message') || msg.querySelector('.message-text');
const userName = authorNameElement ? (getMessageHandle(msg) || normalizeUserName(authorNameElement.textContent.trim())) : '';
let isSpam = false;
if (!isSuperChatOrMembership && featureSettings.spamFilterEnabled && authorNameElement && messageElement) {
isSpam = checkForSpam(msg, userName, messageElement);
}
if (!isSpam && msg.hasAttribute('data-spam')) isSpam = true;
if (isSpam && featureSettings.spamFilterEnabled) {
msg.setAttribute('data-spam', 'true');
if (featureSettings.spamMode === SPAM_MODES.MARK) msg.classList.add('spam-marked'); else msg.classList.remove('spam-marked');
if (updateStats) totalSpamCount++;
} else { msg.removeAttribute('data-spam'); msg.classList.remove('spam-marked'); }
const isPermanentlyBlocked = blockedUsersSet.has(userName);
const isEphemeralBlocked = ephemeralBlockedUsers.has(userName);
const isBlocked = featureSettings.blockEnabled && (isPermanentlyBlocked || isEphemeralBlocked);
if (isBlocked) {
msg.setAttribute('data-blocked', 'true');
msg.setAttribute('data-block-mode', featureSettings.blockMode === BLOCK_MODES.MARK ? 'mark' : 'hide');
if (!isSpam && updateStats) totalSpamCount++;
}
msg.removeAttribute('data-highlight'); msg.removeAttribute('data-ephemeral');
let isHighlight = false;
if (!isSuperChatOrMembership) {
if (localEphemeralMode && ephemeralUsers[userName]) {
msg.setAttribute('data-ephemeral', 'true');
isHighlight = true;
if (calloutUserCache.has(userName)) {
const tempData = calloutUserCache.get(userName);
msg.style.setProperty('--highlight-color', tempData.color); msg.style.setProperty('--ephemeral-color', tempData.color);
msg.setAttribute('data-highlight', tempData.highlightMode === HIGHLIGHT_MODES.BOTH ? 'both' : tempData.highlightMode === HIGHLIGHT_MODES.NAME_ONLY ? 'name' : 'message');
} else {
const color = userColorCache.get(userName) || ephemeralColorSettings[userName] || COLOR_OPTIONS.紅色;
msg.style.setProperty('--highlight-color', color); msg.style.setProperty('--ephemeral-color', color); msg.setAttribute('data-highlight', 'both');
}
} else if (featureSettings.highlightEnabled && (calloutUserCache.has(userName) || userColorCache.get(userName))) {
const color = calloutUserCache.has(userName) ? calloutUserCache.get(userName).color : userColorCache.get(userName);
const mode = calloutUserCache.has(userName) ? calloutUserCache.get(userName).highlightMode || featureSettings.defaultMode : featureSettings.defaultMode;
msg.style.setProperty('--highlight-color', color);
isHighlight = true;
if (mode !== HIGHLIGHT_MODES.BOTH && mode !== HIGHLIGHT_MODES.NAME_ONLY && mode !== HIGHLIGHT_MODES.MESSAGE_ONLY) return;
msg.setAttribute('data-highlight', mode === HIGHLIGHT_MODES.BOTH ? 'both' : mode === HIGHLIGHT_MODES.NAME_ONLY ? 'name' : 'message');
}
}
if (isSuperChatOrMembership) {
const amountElement = msg.querySelector('#purchase-amount');
if (authorNameElement && messageElement) {
const messageText = messageElement.textContent.trim();
const amountText = amountElement ? amountElement.textContent.trim() : '';
const combinedContent = (amountText ? `(${amountText})` : '') + `${userName}:${messageText}`;
setWithLimit(spamCache, `${userName}:${combinedContent}`, { t: Date.now(), type: 'superchat', spam: false }, MAX_SPAM_CACHE_SIZE);
const userColor = userColorCache.get(userName) || userColorSettings[userName];
if (userColor) {
msg.style.backgroundColor = userColor;
} else {
msg.style.removeProperty('background-color');
}
}
}
updateMessageCounter(msg);
if (featureSettings.calloutHighlightEnabled && authorNameElement && messageElement && !isSuperChatOrMembership) {
const textNodes = Array.from(messageElement.childNodes).filter(node => node.nodeType === Node.TEXT_NODE && !node.parentElement.classList.contains('emoji'));
const messageText = textNodes.map(node => node.textContent.trim()).join(' ');
processCalloutUsers(messageText, userName, calloutUserCache.has(userName) ? calloutUserCache.get(userName).color : userColorCache.get(userName));
}
processedMessages.set(msg, true);
updateMessageDisplayName(msg);
if (tempViewMode === 'total') {
if (isBlocked || isSpam || (!isSuperChatOrMembership && !isHighlight)) {
msg.classList.add('ytcm-temp-hide');
msg.classList.remove('ytcm-temp-view-filter');
} else {
msg.classList.remove('ytcm-temp-hide');
msg.classList.remove('ytcm-temp-view-filter');
}
} else if (tempViewMode === 'filter') {
if ((isBlocked || isSpam) && !isSuperChatOrMembership && !isHighlight) {
msg.classList.remove('ytcm-temp-hide');
msg.classList.add('ytcm-temp-view-filter');
} else {
msg.classList.add('ytcm-temp-hide');
msg.classList.remove('ytcm-temp-view-filter');
}
} else {
msg.classList.remove('ytcm-temp-hide');
msg.classList.remove('ytcm-temp-view-filter');
}
}
function checkForSpam(msg, userName, messageElement) {
if (!featureSettings.spamFilterEnabled || !userName || !messageElement) return false;
const messageId = msg.getAttribute('timestamp') || msg.id || '';
if (messageId && processedMessageIds.has(messageId)) return false;
if (messageId) {
processedMessageIds.add(messageId);
}
const textNodes = Array.from(messageElement.childNodes).filter(node => node.nodeType === Node.TEXT_NODE);
let messageText = textNodes.map(node => node.textContent).join(' ').trim();
if (!messageText) return false;
messageText = messageText.replace(/\p{Extended_Pictographic}/gu, '').replace(/[\u200B-\u200D\uFEFF]/g, '').replace(/\s+/g, ' ').trim();
if (!messageText) return false;
const spamKey = `${userName}:${messageText}`;
if (!spamCache.has(spamKey)) { setWithLimit(spamCache, spamKey, { t: Date.now(), type: 'normal', spam: false }, MAX_SPAM_CACHE_SIZE); return false; }
setWithLimit(spamCache, spamKey, { t: Date.now(), type: 'normal', spam: true }, MAX_SPAM_CACHE_SIZE);
return true;
}
function highlightMessages(mutations) {
if (isScriptPaused) return;
cleanupProcessedMessages();
const messages = [];
mutations.forEach(mutation => {
mutation.addedNodes.forEach(node => {
if (node.nodeType === 1 && (node.matches('yt-live-chat-text-message-renderer') || node.matches('.super-fast-chat-message') || node.matches('yt-live-chat-paid-message-renderer') || node.matches('yt-live-chat-membership-item-renderer')) && !processedMessages.has(node)) {
messages.push(node);
}
});
});
if (messages.length === 0) {
const allMessages = Array.from(document.querySelectorAll('yt-live-chat-text-message-renderer, .super-fast-chat-message, yt-live-chat-paid-message-renderer, yt-live-chat-membership-item-renderer')).slice(-MAX_MESSAGE_CACHE_SIZE);
allMessages.forEach(msg => { if (!processedMessages.has(msg)) { messages.push(msg); } });
}
requestAnimationFrame(() => { messages.forEach(msg => processMessage(msg, true)); });
}
function attachMessageListeners(msg) {
msg.addEventListener('click', handleClick, { capture: true });
const authorImg = msg.querySelector('#author-photo img') || msg.querySelector('.author-photo img');
if (authorImg) {
authorImg.addEventListener('mouseenter', handleAuthorPhotoEnter);
authorImg.addEventListener('mouseleave', handleAuthorPhotoLeave);
}
}
function handleClick(event) {
if (isScriptPaused) return;
if (event.button !== 0) return;
const msgElement = event.target.closest('yt-live-chat-text-message-renderer, .super-fast-chat-message, yt-live-chat-paid-message-renderer, yt-live-chat-membership-item-renderer');
if (!msgElement) return;
const authorName = msgElement.querySelector('#author-name') || msgElement.querySelector('.author-name');
const authorImg = msgElement.querySelector('#author-photo img') || msgElement.querySelector('.author-photo img');
if (authorImg && authorImg.contains(event.target)) {
event.stopPropagation(); event.preventDefault();
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 {
const userName = getMessageHandle(msgElement) || normalizeUserName(authorName.textContent.trim());
createColorMenu({ type: localEphemeralMode ? 'temp' : 'user', name: userName }, event);
}
return;
}
if (authorName && authorName.contains(event.target)) {
event.stopPropagation(); event.preventDefault();
const inputField = document.querySelector('yt-live-chat-text-message-renderer #text, .super-fast-chat-message #text, yt-live-chat-text-input-field-renderer [contenteditable]');
if (inputField) {
setTimeout(() => {
const userName = getMessageHandle(msgElement) || normalizeUserName(authorName.textContent.trim());
const currentText = inputField.textContent || inputField.innerText || '';
if (!currentText.includes('@' + userName)) {
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);
}
return;
}
}
function handleAuthorPhotoHover(event) {
if (isScriptPaused) return;
const imgElement = event.target;
const msgElement = imgElement.closest('yt-live-chat-text-message-renderer, .super-fast-chat-message, yt-live-chat-paid-message-renderer, yt-live-chat-membership-item-renderer');
if (!msgElement) return;
const authorNameElement = msgElement.querySelector('#author-name') || msgElement.querySelector('.author-name');
if (!authorNameElement) return;
const userName = getMessageHandle(msgElement) || normalizeUserName(authorNameElement.textContent.trim());
const userSpamEntries = [];
for (const [key, val] of spamCache.entries()) {
if (typeof val === 'object' && key.startsWith(`${userName}:`)) userSpamEntries.push(key.substring(userName.length + 1));
}
if (userSpamEntries.length === 0) return;
if (currentTooltip) { document.body.removeChild(currentTooltip); currentTooltip = null; }
const tooltip = document.createElement('div'); tooltip.className = 'ytcm-spam-tooltip'; tooltip.style.position = 'absolute'; tooltip.style.top = `${imgElement.getBoundingClientRect().top}px`; tooltip.style.left = `${imgElement.getBoundingClientRect().right + 10}px`;
const title = document.createElement('div'); title.textContent = `${userName}:`; title.style.fontWeight = 'bold'; title.style.marginBottom = '4px'; tooltip.appendChild(title);
const messagesDiv = document.createElement('div');
userSpamEntries.reverse().forEach(text => { const msgSpan = document.createElement('div'); msgSpan.textContent = text; msgSpan.style.borderBottom = '1px solid #ccc'; msgSpan.style.paddingBottom = '2px'; msgSpan.style.marginBottom = '2px'; messagesDiv.appendChild(msgSpan); });
tooltip.appendChild(messagesDiv);
let isMouseOverTooltip = false, isMouseOverImg = true, isMouseOverExtendedArea = false, tooltipTimeoutId = null;
const resetTooltipTimeout = () => { clearTimeout(tooltipTimeoutId); tooltipTimeoutId = setTimeout(() => { if (!isMouseOverTooltip && !isMouseOverImg && !isMouseOverExtendedArea) { if (document.body.contains(tooltip)) document.body.removeChild(tooltip); currentTooltip = null; } }, 100); };
const handleMouseOverTooltip = () => { isMouseOverTooltip = true; };
const handleMouseOutTooltip = () => { isMouseOverTooltip = false; resetTooltipTimeout(); };
const handleMouseOverImg = () => { isMouseOverImg = true; };
const handleMouseOutImg = () => { isMouseOverImg = false; resetTooltipTimeout(); };
const handleMouseEnterExtendedArea = () => { isMouseOverExtendedArea = true; };
const handleMouseLeaveExtendedArea = () => { isMouseOverExtendedArea = false; resetTooltipTimeout(); };
tooltip.addEventListener('mouseenter', handleMouseOverTooltip); tooltip.addEventListener('mouseleave', handleMouseOutTooltip);
imgElement.addEventListener('mouseenter', handleMouseOverImg); imgElement.addEventListener('mouseleave', handleMouseOutImg);
const handleMouseMoveExtendedArea = (e) => {
const imgRect = imgElement.getBoundingClientRect(); const tooltipRect = tooltip.getBoundingClientRect();
const extendedAreaLeft = imgRect.right; const extendedAreaRight = tooltipRect.left;
const extendedAreaTop = Math.min(imgRect.top, tooltipRect.top); const extendedAreaBottom = Math.max(imgRect.bottom, tooltipRect.bottom);
const isWithinExtendedArea = localEphemeralMode && e.clientX >= extendedAreaLeft && e.clientX <= extendedAreaRight && e.clientY >= extendedAreaTop && e.clientY <= extendedAreaBottom;
if (isWithinExtendedArea) { if (!isMouseOverExtendedArea) { handleMouseEnterExtendedArea(); document.addEventListener('mouseleave', handleMouseLeaveExtendedArea, { once: true }); } } else { if (isMouseOverExtendedArea) handleMouseLeaveExtendedArea(); }
};
document.addEventListener('mousemove', handleMouseMoveExtendedArea); registerCleanup(() => { document.removeEventListener('mousemove', handleMouseMoveExtendedArea); });
resetTooltipTimeout(); document.body.appendChild(tooltip); currentTooltip = tooltip;
const _ = tooltip.offsetHeight; const tooltipRect = tooltip.getBoundingClientRect();
const viewportHeight = window.innerHeight; const viewportWidth = window.innerWidth;
let adjustedTop = tooltipRect.top; let adjustedLeft = tooltipRect.left;
if (tooltipRect.bottom > viewportHeight) adjustedTop = viewportHeight - tooltipRect.height - 10;
if (adjustedTop < 0) adjustedTop = 10;
if (tooltipRect.right > viewportWidth) adjustedLeft = viewportWidth - tooltipRect.width - 10;
if (adjustedLeft < 0) adjustedLeft = 10;
tooltip.style.top = `${adjustedTop}px`; tooltip.style.left = `${adjustedLeft}px`;
const observer = new MutationObserver(() => {
const updatedMsgElement = imgElement.closest('yt-live-chat-text-message-renderer, .super-fast-chat-message, yt-live-chat-paid-message-renderer, yt-live-chat-membership-item-renderer');
if (!updatedMsgElement || updatedMsgElement.getAttribute('data-block-mode') === 'hide') { if (document.body.contains(tooltip)) document.body.removeChild(tooltip); currentTooltip = null; observer.disconnect(); }
});
observer.observe(document.body, { childList: true, subtree: true }); registerCleanup(() => { observer.disconnect(); });
}
function handleAuthorPhotoEnter(event) { if (isScriptPaused) return; if (authorHoverTimer) clearTimeout(authorHoverTimer); authorHoverTimer = setTimeout(() => { handleAuthorPhotoHover(event); }, 100); }
function handleAuthorPhotoLeave(event) { if (authorHoverTimer) clearTimeout(authorHoverTimer); }
function bindChatMouseEvents() {
const chatContainer = document.querySelector('yt-live-chat-renderer, #chat');
if (!chatContainer) return;
chatContainer.addEventListener('mouseenter', () => { isMouseInChat = true; });
chatContainer.addEventListener('mouseleave', () => { isMouseInChat = false; });
}
function bindInteractionReset() {
const resetFn = () => { if (autoResumeDeviationStartTime > 0) resetAutoResumeTimer(); };
const scrollHandler = (e) => {
if (e.target.closest('#chat, yt-live-chat-item-list-renderer')) resetFn();
};
const clickHandler = (e) => {
if (e.target.closest('#chat, yt-live-chat-renderer')) resetFn();
};
const keydownHandler = (e) => {
if (e.target.closest('yt-live-chat-text-input-field-renderer')) resetFn();
};
window.addEventListener('scroll', scrollHandler, { capture: true, passive: true });
document.addEventListener('click', clickHandler);
document.addEventListener('keydown', keydownHandler);
registerCleanup(() => {
window.removeEventListener('scroll', scrollHandler, { capture: true });
document.removeEventListener('click', clickHandler);
document.removeEventListener('keydown', keydownHandler);
});
}
function init() {
document.querySelectorAll('yt-live-chat-text-message-renderer, .super-fast-chat-message, yt-live-chat-paid-message-renderer, yt-live-chat-membership-item-renderer').forEach(msg => {
attachMessageListeners(msg);
msg.setAttribute('data-ytcm-handled', 'true');
if (!processedMessages.has(msg)) { processMessage(msg, true); }
});
const observer = new MutationObserver(mutations => {
highlightMessages(mutations);
document.querySelectorAll('yt-live-chat-text-message-renderer:not([data-ytcm-handled]), .super-fast-chat-message:not([data-ytcm-handled]), yt-live-chat-paid-message-renderer:not([data-ytcm-handled]), yt-live-chat-membership-item-renderer:not([data-ytcm-handled])').forEach(msg => {
msg.setAttribute('data-ytcm-handled', 'true');
attachMessageListeners(msg);
if (!processedMessages.has(msg)) { processMessage(msg, true); }
});
});
const chatContainer = document.querySelector('#chat');
if (chatContainer) {
observer.observe(chatContainer, { childList: true, subtree: true });
const existingMessages = Array.from(chatContainer.querySelectorAll('yt-live-chat-text-message-renderer, .super-fast-chat-message, yt-live-chat-paid-message-renderer, yt-live-chat-membership-item-renderer'));
existingMessages.forEach(msg => {
msg.setAttribute('data-ytcm-handled', 'true');
attachMessageListeners(msg);
if (!processedMessages.has(msg)) { processMessage(msg, true); }
});
}
registerCleanup(() => { observer.disconnect(); });
bindChatMouseEvents();
bindInteractionReset();
const controlPanel = createControlPanel();
ensureStatsDisplay();
return () => {
cleanupTasks.forEach(fn => { try { fn(); } catch (e) { console.warn('Cleanup task error:', e); } });
cleanupTasks.length = 0; if (controlPanel) controlPanel.remove(); closeMenu();
if (statsBox && statsBox.parentNode) statsBox.parentNode.removeChild(statsBox);
};
}
let cleanup = null;
function main() { if (window.location.pathname.includes('/live_chat')) cleanup = init(); }
main();
window.addEventListener('beforeunload', () => { cleanup?.(); });
})();