YouTube聊天室增強 YouTube Chat Enhancement

多色自動化著色用戶;非原生封鎖用戶;UI操作和功能選擇自由度;移除礙眼置頂;清理/標示洗版;發言次數統計;強化@體驗等

安裝腳本?
作者推薦腳本

您可能也會喜歡 YouTube Live Borderless

以使用者樣式安裝
// ==UserScript==
// @name         YouTube聊天室增強 YouTube Chat Enhancement
// @name:zh-tw   YouTube聊天室增強
// @name:en      YouTube Chat Enhancement
// @namespace    http://tampermonkey.net/
// @version      19.5
// @description  多色自動化著色用戶;非原生封鎖用戶;UI操作和功能選擇自由度;移除礙眼置頂;清理/標示洗版;發言次數統計;強化@體驗等
// @description:zh-tw  多色自動化著色用戶;非原生封鎖用戶;UI操作和功能選擇自由度;移除礙眼置頂;清理/標示洗版;發言次數統計;強化@體驗等
// @description:en Multi-color automated user highlighting;Non-native user blocking;Flexible UI operations and feature selection;Removal of distracting pinned messages;Spam cleanup/flagging;Message count statistics;Improved @mention experience
// @match        *://www.youtube.com/live_chat*
// @grant        GM_registerMenuCommand
// @grant        GM_unregisterMenuCommand
// @grant        GM_addStyle
// @license      MIT
// ==/UserScript==

(function(){'use strict';
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 = {
  "淺藍":"#5FA6E8", "藍色":"#2463D1", "深藍":"#0000FF", "紫色":"#FF00FF",
  "淺綠":"#98FB98", "綠色":"#00FF00", "深綠":"#006400", "青色":"#00FFFF",
  "粉紅":"#FFC0CB", "淺紅":"#F08080", "紅色":"#FF0000", "深紅":"#8B0000",
  "橙色":"#FFA500", "金色":"#FFD700", "灰色":"#BDBDBD", "深灰":"#404040"
};

// 系統常數設定 (單位:毫秒)
const MENU_AUTO_CLOSE_DELAY = 30000, // 選單自動關閉延遲
      THROTTLE_DELAY = 150, // 節流控制延遲
      TEMP_USER_EXPIRE_TIME = 40000, // 臨時用戶資料過期時間
      MAX_MESSAGE_CACHE_SIZE = 400, // 最大訊息快取數量
      CLEANUP_INTERVAL = 40000, // 系統清理間隔
      SPAM_CHECK_INTERVAL = 500, // 垃圾訊息檢查間隔
      FLAG_DURATION = 60000, // 標記用戶持續時間
      MESSAGE_CACHE_LIMIT = 400, // 訊息快取上限
      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);

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}[data-blocked="true"][data-block-mode="mark"] #message{text-decoration:line-through!important;font-style:italic!important}[data-blocked="true"][data-block-mode="hide"]{display:none!important}[data-spam="true"] #message{text-decoration:line-through!important}[data-flagged="true"]{--flagged-opacity:1}[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-flagged="true"] #author-name,[data-flagged="true"] #message{color:var(--flagged-color)!important;font-weight:bold!important;font-style:italic!important;opacity:var(--flagged-opacity,1)}`;document.head.appendChild(style);
document.head.appendChild(style);

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 = Array.from(document.querySelectorAll('yt-live-chat-text-message-renderer'))
        .filter(msg => {
            const nameElement = msg.querySelector('#author-name');
            return nameElement
                && nameElement.textContent.trim() === userName
                && msg.style.display !== 'none';
        });

    messages.forEach(msg => {
        processedMessages.delete(msg);
        styleCache.delete(msg);
        processMessage(msg, true);
    });
}
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) {
        document.querySelectorAll('.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 throttle(func, limit) {
    let lastFunc;
    let lastRan;
    return function() {
        const context = this;
        const args = arguments;
        if (!lastRan) {
            func.apply(context, args);
            lastRan = Date.now();
        } else {
            clearTimeout(lastFunc);
            lastFunc = setTimeout(function() {
                if ((Date.now() - lastRan) >= limit) {
                    func.apply(context, args);
                    lastRan = Date.now();
                }
            }, limit - (Date.now() - lastRan));
        }
    };
}

function cleanupProcessedMessages() {
    requestIdleCallback(() => {
        const allMessages = new Set(document.querySelectorAll('yt-live-chat-text-message-renderer'));
        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 allUsers = Array.from(document.querySelectorAll('#author-name'));
    const existingUsers = allUsers.map(el => el.textContent.trim());
    const isExistingUser = existingUsers.some(user => user.toLowerCase() === mentionedUser.toLowerCase());

    if (isExistingUser && !userColorCache.has(mentionedUser) && !tempUserCache.has(mentionedUser)) {
        tempUsers[mentionedUser] = {
            color: authorColor,
            expireTime: Date.now() + TEMP_USER_EXPIRE_TIME
        };
        tempUserCache.set(mentionedUser, {
            color: authorColor,
            expireTime: Date.now() + TEMP_USER_EXPIRE_TIME
        });
        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 (tempUsers.hasOwnProperty(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.querySelector('yt-live-chat-banner-renderer');
        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') {
                tempUsers[targetElement.name] = {
                    color: colorValue,
                    expireTime: Date.now() + TEMP_USER_EXPIRE_TIME
                };
                tempUserCache.set(targetElement.name, {
                    color: colorValue,
                    expireTime: Date.now() + TEMP_USER_EXPIRE_TIME
                });
                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 = Array.from(document.querySelectorAll('yt-live-chat-text-message-renderer')).filter(msg => {
                    const nameElement = msg.querySelector('#author-name');
                    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.querySelector('.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, 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 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 (err) {
                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.querySelector('#author-name');
  if (!nameElement) return;
  const userName = nameElement.textContent.trim();

  if (userColorCache.has(userName) || tempUserCache.has(userName) || flaggedUsers[userName]) return;

  const messageElement = msg.querySelector('#message');
  if (!messageElement) return;

  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(' ');

  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.querySelector('#author-name');
  if (!nameElement) return;
  const userName = nameElement.textContent.trim();

  if (!userMessageCounts[userName]) userMessageCounts[userName] = 0;
  userMessageCounts[userName]++;

  const existingCounter = msg.querySelector('.ytcm-message-count');
  if (existingCounter) existingCounter.remove();

  const counterSpan = document.createElement('span');
  counterSpan.className = 'ytcm-message-count';
  counterSpan.textContent = userMessageCounts[userName];

  const messageElement = msg.querySelector('#message');
  if (messageElement) messageElement.appendChild(counterSpan);
}

function processMessage(msg, isInitialLoad = false) {
  if (styleCache.has(msg)) return;
  const authorName = msg.querySelector('#author-name');
  const messageElement = msg.querySelector('#message');
  if (!authorName || !messageElement) return;

  const userName = authorName.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 (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) return;

    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.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 messages = [];

  mutations.forEach(mutation => {
    mutation.addedNodes.forEach(node => {
      if (node.nodeType === 1 &&
          node.matches('yt-live-chat-text-message-renderer') &&
          !processedMessages.has(node)) {
        messages.push(node);
        processedMessages.set(node, true);
      }
    });
  });

  if (messages.length === 0) {
    const allMessages = Array.from(document.querySelectorAll('yt-live-chat-text-message-renderer'))
      .slice(-MAX_MESSAGE_CACHE_SIZE);

    allMessages.forEach(msg => {
      if (!processedMessages.has(msg)) {
        messages.push(msg);
        processedMessages.set(msg, true);
      }
    });
  }

  requestAnimationFrame(() => {
    messages.forEach(msg => processMessage(msg));
    cleanupExpiredFlags();
    cleanupExpiredTempUsers();
    removePinnedMessage();
  });
}

function handleClick(event) {
    if (event.button !== 0) return;
    const msgElement = event.target.closest('yt-live-chat-text-message-renderer');
    if (!msgElement) return;
    const messageElement = msgElement.querySelector('#message');
    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.querySelector('#author-name');
    const authorImg = msgElement.querySelector('#author-photo img');
    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.querySelector('yt-live-chat-text-input-field-renderer [contenteditable]');
        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() {
    document.querySelectorAll('yt-live-chat-text-message-renderer').forEach(msg => {
        msg.addEventListener('click', handleClick, { capture: true });
    });
}

function init() {
    initializeCaches();
    overrideNativeListeners();

    const observer = new MutationObserver(mutations => {
        highlightMessages(mutations);

        document.querySelectorAll('yt-live-chat-text-message-renderer:not([data-ytcm-handled])').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.querySelector('#chat');
    if (chatContainer) {
        observer.observe(chatContainer, { childList: true, subtree: true });

        const existingMessages = Array.from(chatContainer.querySelectorAll('yt-live-chat-text-message-renderer'));
        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('#chat') && !cleanup) {
        cleanup = init();
    }
}, 1000);

window.addEventListener('beforeunload', () => {
    clearInterval(checkChatContainer);
    cleanup?.();
});
})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址