YouTube Boost Chat

完全替換 YouTube 聊天訊息列表

< 腳本YouTube Boost Chat的回應

評論:正評 - 腳本一切正常

§
發表於:2025-10-08

體驗好像還不錯,但很遺憾跟我的腳本完完全全不相容,因為是基於原本或者youtube-super-fast-chat聊天室勉強AI搞出來的,事到如今轉移代價好像挺大的,所以…遺憾。

(大致功能:自動著色用戶、非原生封鎖用戶、UI操作和功能切換、清理/標示洗版、發言次數統計、強化@體驗等鬼東西,
https://greasyfork.org/zh-TW/scripts/530998-youtube%E8%81%8A%E5%A4%A9%E5%AE%A4%E5%A2%9E%E5%BC%B7-youtube-chat-enhancement )

§
發表於:2025-10-08
編輯:2025-10-08

CSS 對照表

yt-live-chat-text-message-renderer

  • 每則直播聊天室文字訊息的根節點(大量用於掃描與處理)。
  • Boost Chat: bst-message-entry.bst-liveChatTextMessageRenderer

#author-name

  • 訊息內的作者名稱元素(比對用戶名、套高亮、加計數等)。
  • Boost Chat: .bst-message-username

#message

  • 訊息文字容器(加刪除線、附加留言計數)。
  • Boost Chat: .bst-message-body

yt-live-chat-banner-renderer

  • 置頂(Pin)訊息容器(用於自動移除/隱藏置頂)。
  • Boost Chat: 不變

#author-photo img

  • 作者頭像 <img>(點擊開啟色彩選單或前往頻道)。
  • Boost Chat: bst-profile-img

yt-live-chat-text-input-field-renderer [contenteditable]

  • 輸入框(插入 @用戶名 提及)。
  • Boost Chat: 不變

#chat

  • 聊天室主容器(作為 MutationObserver 的觀察根)。
  • Boost Chat: 不變

yt-live-chat-text-message-renderer:not([data-ytcm-handled])

  • 篩出尚未處理過的新訊息節點。
  • Boost Chat: bst-message-entry.bst-liveChatTextMessageRenderer:not([data-ytcm-handled])
§
發表於:2025-10-08
編輯:2025-10-08

你可以按這個CSS對照表改一下

大多都盡量有一對一對照的

你再看看那些沒對照到

§
發表於:2025-10-08

多謝幫助,不過對照調整後還是完全沒法運作,不只是CSS的問題所以超出個人能力,
本來就是只出意見、AI代勞的產品,我沒辦法進一步分析問題,還好用戶頂多就20個,應該不會有人跑來敲碗叫我相容ꉂ🤣𐤔。

§
發表於:2025-10-08

AI做了這個 第1036行暫時還不行。其他應該可以的。你看看吧

// ==UserScript==
// @name         YouTube聊天室增強(支援 Boost Chat)/ YouTube Chat Enhancement (+Boost Chat)
// @name:zh-tw   YouTube聊天室增強(支援 Boost Chat)
// @name:en      YouTube Chat Enhancement (with Boost Chat support)
// @namespace    http://tampermonkey.net/
// @version      19.9-bc1
// @description  多色自動化著色用戶;非原生封鎖用戶;UI操作和功能選擇自由度;移除礙眼置頂;清理/標示洗版;發言次數統計;強化@體驗等(已適配 Boost Chat)
// @match        *://www.youtube.com/live_chat*
// @grant        GM_registerMenuCommand
// @grant        GM_unregisterMenuCommand
// @grant        GM_addStyle
// @license      MIT
// ==/UserScript==

(function () {
  'use strict';

  // ---- 統一選擇器(優先 Boost Chat,其次原生) ----------------------------
  const SEL = {
    messageEntry:
      'bst-message-entry.bst-liveChatTextMessageRenderer, yt-live-chat-text-message-renderer',
    authorName: '.bst-message-username, #author-name',
    messageBody: '.bst-message-body, #message',
    banner: 'yt-live-chat-banner-renderer',
    authorImg: '.bst-profile-img, #author-photo img',
    inputField: 'yt-live-chat-text-input-field-renderer [contenteditable]',
    chatRoot: '#chat',
    unhandledMessage:
      'bst-message-entry.bst-liveChatTextMessageRenderer:not([data-ytcm-handled]), yt-live-chat-text-message-renderer:not([data-ytcm-handled])',
  };

  const LANG = {
    'zh-TW': {
      buttons: { '封鎖': '封鎖', '編輯': '編輯', '刪除': '刪除', '清除': '清除' },
      tooltips: {
        flag: '臨時模式: 切換至臨時用戶上色,開啟時上色不儲存',
        pin: '清除置頂: 開啟/關閉自動移除置頂訊息',
        highlight: (mode) => `高亮模式: ${mode} (雙擊切換模式)`,
        block: (mode) => `封鎖模式: ${mode} (雙擊切換模式)`,
        mention: (mode) => `提及高亮: ${mode} (雙擊切換模式)`,
        spam: (mode) => `洗版過濾: ${mode} (雙擊切換模式)`,
        counter: '留言計數: 顯示/隱藏用戶留言計數',
        clearConfirm: '確定清除所有設定?',
        clearButton: '確認',
      },
    },
    en: {
      buttons: { '封鎖': 'Block', '編輯': 'Edit', '刪除': 'Delete', '清除': 'Clear' },
      tooltips: {
        flag: 'Temporary mode: Switch to temporary user coloring, colors are not saved when enabled',
        pin: 'Pin removal: Toggle auto-remove pinned messages',
        highlight: (mode) => `Highlight mode: ${mode} (Double-click to switch)`,
        block: (mode) => `Block mode: ${mode} (Double-click to switch)`,
        mention: (mode) => `Mention highlight: ${mode} (Double-click to switch)`,
        spam: (mode) => `Spam filter: ${mode} (Double-click to switch)`,
        counter: 'Message counter: Show/hide user message counts',
        clearConfirm: 'Confirm reset all settings?',
        clearButton: 'Confirm',
      },
    },
  };

  const currentLang = navigator.language.startsWith('zh') ? 'zh-TW' : 'en';

  // 顏色選項對照表 (16進位色碼)
  const COLOR_OPTIONS = {
    '淺藍': '#A5CDF3',
    '藍色': '#62A8EA',
    '深藍': '#1C76CA',
    '紫色': '#FF00FF',
    '淺綠': '#98FB98',
    '綠色': '#00FF00',
    '深綠': '#00B300',
    '青色': '#00FFFF',
    '粉紅': '#FFC0CB',
    '淺紅': '#F08080',
    '紅色': '#FF0000',
    '深紅': '#8B0000',
    '橙色': '#FFA500',
    '金色': '#FFD700',
    '灰色': '#BDBDBD',
    '深灰': '#404040',
  };

  // 系統常數設定 (毫秒)
  const MENU_AUTO_CLOSE_DELAY = 30000,
    THROTTLE_DELAY = 200,
    TEMP_USER_EXPIRE_TIME = 300000,
    MAX_MESSAGE_CACHE_SIZE = 200,
    CLEANUP_INTERVAL = 40000,
    SPAM_CHECK_INTERVAL = 500,
    FLAG_DURATION = 600000,
    MESSAGE_CACHE_LIMIT = 600,
    DOUBLE_CLICK_DELAY = 350,
    PIN_CHECK_INTERVAL = 60000;

  const HIGHLIGHT_MODES = { BOTH: 0, NAME_ONLY: 1, MESSAGE_ONLY: 2 },
    SPAM_MODES = { MARK: 0, REMOVE: 1 },
    BLOCK_MODES = { MARK: 0, HIDE: 1 };

  let userColorSettings =
      JSON.parse(localStorage.getItem('userColorSettings')) || {},
    blockedUsers = JSON.parse(localStorage.getItem('blockedUsers')) || [],
    currentMenu = null,
    menuTimeoutId = null,
    featureSettings =
      JSON.parse(localStorage.getItem('featureSettings')) || {
        pinEnabled: false,
        highlightEnabled: false,
        blockEnabled: false,
        buttonsVisible: false,
        mentionHighlightEnabled: false,
        spamFilterEnabled: false,
        counterEnabled: false,
        spamMode: SPAM_MODES.MARK,
        blockMode: BLOCK_MODES.MARK,
        flagMode: false,
      },
    highlightSettings =
      JSON.parse(localStorage.getItem('highlightSettings')) || {
        defaultMode: HIGHLIGHT_MODES.BOTH,
        tempMode: HIGHLIGHT_MODES.BOTH,
      },
    tempUsers = JSON.parse(localStorage.getItem('tempUsers')) || {},
    flaggedUsers = {},
    lastTempUserCleanupTime = Date.now(),
    userMessageCounts = {},
    lastSpamCheckTime = 0,
    lastClickTime = 0,
    clickCount = 0,
    pinRemoved = false,
    lastPinCheckTime = 0;

  const userColorCache = new Map(),
    blockedUsersSet = new Set(blockedUsers),
    tempUserCache = new Map(),
    styleCache = new WeakMap();

  class LRUCache {
    constructor(limit) {
      this.limit = limit;
      this.cache = new Map();
    }
    has(key) {
      return this.cache.has(key);
    }
    get(key) {
      const value = this.cache.get(key);
      if (value) {
        this.cache.delete(key);
        this.cache.set(key, value);
      }
      return value;
    }
    set(key, value) {
      if (this.cache.has(key)) this.cache.delete(key);
      else if (this.cache.size >= this.limit) {
        const oldestKey = this.cache.keys().next().value;
        this.cache.delete(oldestKey);
      }
      this.cache.set(key, value);
    }
    delete(key) {
      this.cache.delete(key);
    }
    clear() {
      this.cache.clear();
    }
  }

  const messageCache = new LRUCache(MESSAGE_CACHE_LIMIT),
    processedMessages = new LRUCache(MAX_MESSAGE_CACHE_SIZE * 2);

  // ---- 樣式:同時支援原生 & Boost Chat --------------------------------------
  const style = document.createElement('style');
  style.textContent = `
:root{--highlight-color:inherit;--flagged-color:#FF0000}
.ytcm-menu{position:fixed;background-color:white;border:1px solid black;padding:5px;z-index:9999;box-shadow:2px 2px 5px rgba(0,0,0,0.2);border-radius:5px}
.ytcm-color-item{cursor:pointer;padding:0;border-radius:3px;margin:2px;border:1px solid #ddd;transition:transform 0.1s;min-width:40px;height:25px}
.ytcm-color-item:hover{transform:scale(1.1);box-shadow:0 0 5px rgba(0,0,0,0.3)}
.ytcm-list-item{cursor:pointer;padding:5px;background-color:#f0f0f0;border-radius:3px;margin:2px}
.ytcm-button{cursor:pointer;padding:5px 8px;margin:5px 2px 0 2px;border-radius:3px;border:1px solid #ccc;background-color:#f8f8f8}
.ytcm-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:5px}
.ytcm-button-row{display:flex;justify-content:space-between;margin-top:5px}
.ytcm-flex-wrap{display:flex;flex-wrap:wrap;gap:5px;margin-bottom:10px}
.ytcm-control-panel{position:fixed;left:0;bottom:75px;z-index:9998;display:flex;flex-direction:column;gap:8px;padding:0}
.ytcm-control-btn{padding:5px 0;cursor:pointer;text-align:left;min-width:20px;font-size:14px;font-weight:bold;color:white;-webkit-text-stroke:1px black;text-shadow:none;background:none;border:none;margin:0}
.ytcm-control-btn.active{-webkit-text-stroke:1px black}
.ytcm-control-btn.inactive{-webkit-text-stroke:1px red}
.ytcm-toggle-btn{padding:5px 0;cursor:pointer;text-align:left;min-width:20px;font-size:14px;font-weight:bold;color:white;-webkit-text-stroke:1px black;text-shadow:none;background:none;border:none;margin:0}
.ytcm-main-buttons{display:${featureSettings.buttonsVisible ? 'flex' : 'none'};flex-direction:column;gap:8px}
.ytcm-message-count{font-size:0.6em;opacity:0.7;margin-left:3px;display:inline-block}

/* 封鎖/洗版/標記(同時對應 Boost 與原生的名稱與訊息容器) */
[data-blocked="true"][data-block-mode="mark"] ${SEL.messageBody}{text-decoration:line-through!important;font-style:italic!important}
[data-blocked="true"][data-block-mode="hide"]{display:none!important}
[data-spam="true"] ${SEL.messageBody}{text-decoration:line-through!important}

/* 高亮與標記(作者名與訊息體) */
[data-highlight="name"] ${SEL.authorName},
[data-highlight="both"] ${SEL.authorName}{color:var(--highlight-color)!important;font-weight:bold!important}
[data-highlight="message"] ${SEL.messageBody},
[data-highlight="both"] ${SEL.messageBody}{color:var(--highlight-color)!important;font-weight:bold!important}

/* 标记(Flag)顏色 */
[data-flagged="true"] ${SEL.authorName},
[data-flagged="true"] ${SEL.messageBody}{color:var(--flagged-color)!important;font-weight:bold!important;font-style:italic!important;opacity:var(--flagged-opacity,1)}
`;
  document.head.appendChild(style);

  function $(root, selector) {
    return (root || document).querySelector(selector);
  }
  function $all(root, selector) {
    return Array.from((root || document).querySelectorAll(selector));
  }

  function initializeCaches() {
    Object.entries(userColorSettings).forEach(([user, color]) =>
      userColorCache.set(user, color)
    );
    Object.entries(tempUsers).forEach(([user, data]) =>
      tempUserCache.set(user, data)
    );
    Object.entries(flaggedUsers).forEach(([user, expireTime]) => {
      if (expireTime > Date.now()) updateAllMessages(user);
    });
  }

  function updateAllMessages(userName) {
    const messages = $all(document, SEL.messageEntry).filter((msg) => {
      const nameElement = $(msg, SEL.authorName);
      return (
        nameElement &&
        nameElement.textContent.trim() === userName &&
        msg.style.display !== 'none'
      );
    });
    messages.forEach((msg) => {
      processedMessages.delete(msg);
      styleCache.delete(msg);
      processMessage(msg, true);
    });
  }

  // ---- 控制面板(原樣,略微調整 updateAllMessages 呼叫) --------------------
  function createControlPanel() {
    const panel = document.createElement('div');
    panel.className = 'ytcm-control-panel';

    const mainButtons = document.createElement('div');
    mainButtons.className = 'ytcm-main-buttons';

    const buttons = [
      {
        text: '臨',
        className: `ytcm-control-btn ${featureSettings.flagMode ? 'active' : 'inactive'}`,
        title: LANG[currentLang].tooltips.flag,
        onClick: () => handleButtonClick('臨', () => {
          featureSettings.flagMode = !featureSettings.flagMode;
          updateButtonState('臨', featureSettings.flagMode);
          localStorage.setItem('featureSettings', JSON.stringify(featureSettings));
        })
      },
      {
        text: '頂',
        className: `ytcm-control-btn ${featureSettings.pinEnabled ? 'active' : 'inactive'}`,
        title: LANG[currentLang].tooltips.pin,
        onClick: () => handleButtonClick('頂', () => {
          featureSettings.pinEnabled = !featureSettings.pinEnabled;
          pinRemoved = false;
          updateButtonState('頂', featureSettings.pinEnabled);
          localStorage.setItem('featureSettings', JSON.stringify(featureSettings));
        })
      },
      {
        text: '亮',
        className: `ytcm-control-btn ${featureSettings.highlightEnabled ? 'active' : 'inactive'}`,
        title: LANG[currentLang].tooltips.highlight(getHighlightModeName(highlightSettings.defaultMode)),
        onClick: () => handleButtonClick(
          '亮',
          () => {
            featureSettings.highlightEnabled = !featureSettings.highlightEnabled;
            updateButtonState('亮', featureSettings.highlightEnabled);
            updateButtonTitle('亮', LANG[currentLang].tooltips.highlight(getHighlightModeName(highlightSettings.defaultMode)));
            localStorage.setItem('featureSettings', JSON.stringify(featureSettings));
            updateAllMessages();
          },
          () => {
            highlightSettings.defaultMode = (highlightSettings.defaultMode + 1) % 3;
            updateButtonTitle('亮', LANG[currentLang].tooltips.highlight(getHighlightModeName(highlightSettings.defaultMode)));
            localStorage.setItem('highlightSettings', JSON.stringify(highlightSettings));
            updateAllMessages();
          }
        )
      },
      {
        text: '封',
        className: `ytcm-control-btn ${featureSettings.blockEnabled ? 'active' : 'inactive'}`,
        title: LANG[currentLang].tooltips.block(
          featureSettings.blockMode === BLOCK_MODES.MARK
            ? (currentLang === 'zh-TW' ? '標記' : 'Mark')
            : (currentLang === 'zh-TW' ? '清除' : 'Clear')
        ),
        onClick: () => handleButtonClick(
          '封',
          () => {
            featureSettings.blockEnabled = !featureSettings.blockEnabled;
            updateButtonState('封', featureSettings.blockEnabled);
            updateButtonTitle('封', LANG[currentLang].tooltips.block(
              featureSettings.blockMode === BLOCK_MODES.MARK
                ? (currentLang === 'zh-TW' ? '標記' : 'Mark')
                : (currentLang === 'zh-TW' ? '清除' : 'Clear')
            ));
            localStorage.setItem('featureSettings', JSON.stringify(featureSettings));
            updateAllMessages();
          },
          () => {
            featureSettings.blockMode = (featureSettings.blockMode + 1) % 2;
            updateButtonTitle('封', LANG[currentLang].tooltips.block(
              featureSettings.blockMode === BLOCK_MODES.MARK
                ? (currentLang === 'zh-TW' ? '標記' : 'Mark')
                : (currentLang === 'zh-TW' ? '清除' : 'Clear')
            ));
            localStorage.setItem('featureSettings', JSON.stringify(featureSettings));
            updateAllMessages();
          }
        )
      },
      {
        text: '@',
        className: `ytcm-control-btn ${featureSettings.mentionHighlightEnabled ? 'active' : 'inactive'}`,
        title: LANG[currentLang].tooltips.mention(getHighlightModeName(highlightSettings.tempMode)),
        onClick: () => handleButtonClick(
          '@',
          () => {
            featureSettings.mentionHighlightEnabled = !featureSettings.mentionHighlightEnabled;
            updateButtonState('@', featureSettings.mentionHighlightEnabled);
            updateButtonTitle('@', LANG[currentLang].tooltips.mention(getHighlightModeName(highlightSettings.tempMode)));
            localStorage.setItem('featureSettings', JSON.stringify(featureSettings));
            if (!featureSettings.mentionHighlightEnabled) {
              tempUsers = {};
              tempUserCache.clear();
              localStorage.setItem('tempUsers', JSON.stringify(tempUsers));
            }
            updateAllMessages();
          },
          () => {
            highlightSettings.tempMode = (highlightSettings.tempMode + 1) % 3;
            updateButtonTitle('@', LANG[currentLang].tooltips.mention(getHighlightModeName(highlightSettings.tempMode)));
            localStorage.setItem('highlightSettings', JSON.stringify(highlightSettings));
            updateAllMessages();
          }
        )
      },
      {
        text: '洗',
        className: `ytcm-control-btn ${featureSettings.spamFilterEnabled ? 'active' : 'inactive'}`,
        title: LANG[currentLang].tooltips.spam(
          featureSettings.spamMode === SPAM_MODES.MARK
            ? (currentLang === 'zh-TW' ? '標記' : 'Mark')
            : (currentLang === 'zh-TW' ? '清除' : 'Clear')
        ),
        onClick: () => handleButtonClick(
          '洗',
          () => {
            featureSettings.spamFilterEnabled = !featureSettings.spamFilterEnabled;
            updateButtonState('洗', featureSettings.spamFilterEnabled);
            updateButtonTitle('洗', LANG[currentLang].tooltips.spam(
              featureSettings.spamMode === SPAM_MODES.MARK
                ? (currentLang === 'zh-TW' ? '標記' : 'Mark')
                : (currentLang === 'zh-TW' ? '清除' : 'Clear')
            ));
            localStorage.setItem('featureSettings', JSON.stringify(featureSettings));
            if (!featureSettings.spamFilterEnabled) messageCache.clear();
            updateAllMessages();
          },
          () => {
            featureSettings.spamMode = (featureSettings.spamMode + 1) % 2;
            updateButtonTitle('洗', LANG[currentLang].tooltips.spam(
              featureSettings.spamMode === SPAM_MODES.MARK
                ? (currentLang === 'zh-TW' ? '標記' : 'Mark')
                : (currentLang === 'zh-TW' ? '清除' : 'Clear')
            ));
            localStorage.setItem('featureSettings', JSON.stringify(featureSettings));
            updateAllMessages();
          }
        )
      },
      {
        text: '數',
        className: `ytcm-control-btn ${featureSettings.counterEnabled ? 'active' : 'inactive'}`,
        title: LANG[currentLang].tooltips.counter,
        onClick: () => handleButtonClick('數', () => {
          featureSettings.counterEnabled = !featureSettings.counterEnabled;
          updateButtonState('數', featureSettings.counterEnabled);
          localStorage.setItem('featureSettings', JSON.stringify(featureSettings));
          if (!featureSettings.counterEnabled) {
            $all(document, '.ytcm-message-count').forEach((el) => el.remove());
          } else {
            updateAllMessages();
          }
        })
      }
    ];

    buttons.forEach((btn) => {
      const button = document.createElement('div');
      button.className = btn.className;
      button.textContent = btn.text;
      button.title = btn.title;
      button.dataset.action = btn.text;
      button.addEventListener('click', btn.onClick);
      mainButtons.appendChild(button);
    });

    const toggleBtn = document.createElement('div');
    toggleBtn.className = 'ytcm-toggle-btn';
    toggleBtn.textContent = '☑';
    toggleBtn.title = currentLang === 'zh-TW' ? '顯示/隱藏控制按鈕' : 'Show/Hide Controls';
    toggleBtn.addEventListener('click', () => {
      featureSettings.buttonsVisible = !featureSettings.buttonsVisible;
      mainButtons.style.display = featureSettings.buttonsVisible ? 'flex' : 'none';
      localStorage.setItem('featureSettings', JSON.stringify(featureSettings));
    });

    panel.appendChild(mainButtons);
    panel.appendChild(toggleBtn);
    document.body.appendChild(panel);
    return panel;
  }

  function handleButtonClick(btnText, toggleAction, modeAction) {
    const now = Date.now();
    if (now - lastClickTime < DOUBLE_CLICK_DELAY) {
      clickCount++;
      if (clickCount === 2 && modeAction) {
        modeAction();
        clickCount = 0;
      }
    } else {
      clickCount = 1;
      setTimeout(() => {
        if (clickCount === 1) toggleAction();
        clickCount = 0;
      }, DOUBLE_CLICK_DELAY);
    }
    lastClickTime = now;
  }

  function getHighlightModeName(mode) {
    switch (mode) {
      case HIGHLIGHT_MODES.BOTH:
        return currentLang === 'zh-TW' ? '全部高亮' : 'Both';
      case HIGHLIGHT_MODES.NAME_ONLY:
        return currentLang === 'zh-TW' ? '僅暱稱' : 'Name Only';
      case HIGHLIGHT_MODES.MESSAGE_ONLY:
        return currentLang === 'zh-TW' ? '僅對話' : 'Message Only';
      default:
        return '';
    }
  }
  function updateButtonState(btnText, isActive) {
    const btn = document.querySelector(`.ytcm-control-btn[data-action="${btnText}"]`);
    if (btn) btn.className = `ytcm-control-btn ${isActive ? 'active' : 'inactive'}`;
  }
  function updateButtonTitle(btnText, title) {
    const btn = document.querySelector(`.ytcm-control-btn[data-action="${btnText}"]`);
    if (btn) btn.title = title;
  }

  function cleanupProcessedMessages() {
    requestIdleCallback(() => {
      const allMessages = new Set($all(document, SEL.messageEntry));
      const toDelete = [];
      processedMessages.cache.forEach((_, msg) => {
        if (!allMessages.has(msg)) {
          toDelete.push(msg);
        }
      });
      toDelete.forEach((msg) => {
        processedMessages.delete(msg);
        styleCache.delete(msg);
      });
    });
  }

  function processMentionedUsers(messageText, authorName, authorColor) {
    if (!featureSettings.mentionHighlightEnabled || !authorColor) return;
    const mentionRegex = /@([^\s].*?(?=\s|$|@|[\u200b]))/g;
    let match;
    const mentionedUsers = new Set();
    while ((match = mentionRegex.exec(messageText)) !== null) {
      const mentionedUser = match[1].trim();
      if (mentionedUser) mentionedUsers.add(mentionedUser);
    }
    if (mentionedUsers.size !== 1) return;

    const mentionedUser = Array.from(mentionedUsers)[0];
    const existingUsers = $all(document, SEL.authorName).map((el) =>
      el.textContent.trim()
    );
    const isExistingUser = existingUsers.some(
      (user) => user.toLowerCase() === mentionedUser.toLowerCase()
    );

    if (
      isExistingUser &&
      !userColorCache.has(mentionedUser) &&
      !tempUserCache.has(mentionedUser)
    ) {
      const expireTime = Date.now() + TEMP_USER_EXPIRE_TIME;
      tempUsers[mentionedUser] = { color: authorColor, expireTime };
      tempUserCache.set(mentionedUser, { color: authorColor, expireTime });
      updateAllMessages(mentionedUser);
      localStorage.setItem('tempUsers', JSON.stringify(tempUsers));
    }
  }

  function cleanupExpiredTempUsers() {
    const now = Date.now();
    if (now - lastTempUserCleanupTime < CLEANUP_INTERVAL) return;
    lastTempUserCleanupTime = now;
    let changed = false;
    for (const [user, data] of tempUserCache.entries()) {
      if (data.expireTime <= now) {
        tempUserCache.delete(user);
        if (Object.prototype.hasOwnProperty.call(tempUsers, user)) {
          delete tempUsers[user];
        }
        changed = true;
        updateAllMessages(user);
      }
    }
    if (changed) {
      localStorage.setItem('tempUsers', JSON.stringify(tempUsers));
    }
  }

  function cleanupExpiredFlags() {
    const now = Date.now();
    let changed = false;
    for (const user in flaggedUsers) {
      if (flaggedUsers[user] <= now) {
        delete flaggedUsers[user];
        changed = true;
        updateAllMessages(user);
      }
    }
  }

  function removePinnedMessage() {
    if (!featureSettings.pinEnabled) return;
    const now = Date.now();
    if (now - lastPinCheckTime < PIN_CHECK_INTERVAL) return;
    lastPinCheckTime = now;
    requestAnimationFrame(() => {
      const pinnedMessage = $(document, SEL.banner);
      if (pinnedMessage) pinnedMessage.style.display = 'none';
    });
  }

  function closeMenu() {
    if (currentMenu) {
      document.body.removeChild(currentMenu);
      currentMenu = null;
      clearTimeout(menuTimeoutId);
    }
  }

  function createColorMenu(targetElement, event) {
    closeMenu();
    const menu = document.createElement('div');
    menu.className = 'ytcm-menu';
    menu.style.top = `${event.clientY}px`;
    menu.style.left = `${event.clientX}px`;
    menu.style.width = '220px';

    const colorGrid = document.createElement('div');
    colorGrid.className = 'ytcm-grid';

    Object.entries(COLOR_OPTIONS).forEach(([colorName, colorValue]) => {
      const colorItem = document.createElement('div');
      colorItem.className = 'ytcm-color-item';
      colorItem.title = colorName;
      colorItem.style.backgroundColor = colorValue;
      colorItem.addEventListener('click', () => {
        if (targetElement.type === 'user') {
          userColorSettings[targetElement.name] = colorValue;
          userColorCache.set(targetElement.name, colorValue);
          updateAllMessages(targetElement.name);
          localStorage.setItem(
            'userColorSettings',
            JSON.stringify(userColorSettings)
          );
        } else if (targetElement.type === 'temp') {
          const expireTime = Date.now() + TEMP_USER_EXPIRE_TIME;
          tempUsers[targetElement.name] = { color: colorValue, expireTime };
          tempUserCache.set(targetElement.name, { color: colorValue, expireTime });
          updateAllMessages(targetElement.name);
        }
        closeMenu();
      });
      colorGrid.appendChild(colorItem);
    });

    const buttonRow = document.createElement('div');
    buttonRow.className = 'ytcm-button-row';

    const buttons = [
      {
        text: LANG[currentLang].buttons.封鎖,
        className: 'ytcm-button',
        onClick: () => {
          if (targetElement.type === 'user') {
            blockedUsers.push(targetElement.name);
            blockedUsersSet.add(targetElement.name);
            localStorage.setItem('blockedUsers', JSON.stringify(blockedUsers));
            updateAllMessages(targetElement.name);
          }
          closeMenu();
        },
      },
      {
        text: LANG[currentLang].buttons.編輯,
        className: 'ytcm-button',
        onClick: (e) => {
          e.stopPropagation();
          createEditMenu(targetElement, event);
        },
      },
      {
        text: LANG[currentLang].buttons.刪除,
        className: 'ytcm-button',
        onClick: () => {
          const userName = targetElement.name;
          let foundInList = false;

          if (userColorSettings[userName]) {
            delete userColorSettings[userName];
            userColorCache.delete(userName);
            foundInList = true;
          }
          if (blockedUsersSet.has(userName)) {
            blockedUsers = blockedUsers.filter((u) => u !== userName);
            blockedUsersSet.delete(userName);
            localStorage.setItem('blockedUsers', JSON.stringify(blockedUsers));
            foundInList = true;
          }
          if (tempUsers[userName]) {
            delete tempUsers[userName];
            tempUserCache.delete(userName);
            localStorage.setItem('tempUsers', JSON.stringify(tempUsers));
            foundInList = true;
          }
          if (flaggedUsers[userName]) {
            delete flaggedUsers[userName];
            localStorage.setItem('flaggedUsers', JSON.stringify(flaggedUsers));
            foundInList = true;
          }
          localStorage.setItem(
            'userColorSettings',
            JSON.stringify(userColorSettings)
          );

          const messages = $all(document, SEL.messageEntry).filter((msg) => {
            const nameElement = $(msg, SEL.authorName);
            return nameElement && nameElement.textContent.trim() === userName;
          });

          messages.forEach((msg) => {
            if (foundInList) {
              msg.removeAttribute('data-highlight');
              msg.removeAttribute('data-flagged');
              msg.removeAttribute('data-blocked');
              msg.removeAttribute('data-spam');
              msg.style.removeProperty('--highlight-color');
              msg.style.removeProperty('--flagged-color');
              $(msg, '.ytcm-message-count')?.remove();
            } else {
              msg.style.display = 'none';
            }
          });
          closeMenu();
        },
      },
      {
        text: LANG[currentLang].buttons.清除,
        className: 'ytcm-button',
        onClick: () => {
          const confirmMenu = document.createElement('div');
          confirmMenu.className = 'ytcm-menu';
          confirmMenu.style.top = `${event.clientY}px`;
          confirmMenu.style.left = `${event.clientX}px`;

          const confirmText = document.createElement('div');
          confirmText.textContent = LANG[currentLang].tooltips.clearConfirm;

          const confirmButton = document.createElement('button');
          confirmButton.className = 'ytcm-button';
          confirmButton.textContent = LANG[currentLang].tooltips.clearButton;
          confirmButton.addEventListener('click', () => {
            localStorage.removeItem('userColorSettings');
            localStorage.removeItem('blockedUsers');
            localStorage.removeItem('featureSettings');
            localStorage.removeItem('highlightSettings');
            localStorage.removeItem('tempUsers');
            localStorage.removeItem('flaggedUsers');
            window.location.reload();
          });

          confirmMenu.appendChild(confirmText);
          confirmMenu.appendChild(confirmButton);
          document.body.appendChild(confirmMenu);

          setTimeout(() => {
            if (document.body.contains(confirmMenu)) document.body.removeChild(confirmMenu);
          }, 5000);
        },
      },
    ];

    buttons.forEach((btn) => {
      const button = document.createElement('button');
      button.className = btn.className;
      button.textContent = btn.text;
      button.addEventListener('click', btn.onClick);
      buttonRow.appendChild(button);
    });

    menu.appendChild(colorGrid);
    menu.appendChild(buttonRow);
    document.body.appendChild(menu);
    currentMenu = menu;
    menuTimeoutId = setTimeout(closeMenu, MENU_AUTO_CLOSE_DELAY);
  }

  function createEditMenu(targetElement) {
    closeMenu();
    const menu = document.createElement('div');
    menu.className = 'ytcm-menu';
    menu.style.top = '10px';
    menu.style.left = '10px';
    menu.style.width = '90%';
    menu.style.maxHeight = '80vh';
    menu.style.overflowY = 'auto';

    const closeButton = document.createElement('button');
    closeButton.className = 'ytcm-button';
    closeButton.textContent = currentLang === 'zh-TW' ? '關閉' : 'Close';
    closeButton.style.width = '100%';
    closeButton.style.marginBottom = '10px';
    closeButton.addEventListener('click', closeMenu);
    menu.appendChild(closeButton);

    const importExportRow = document.createElement('div');
    importExportRow.className = 'ytcm-button-row';

    const exportButton = document.createElement('button');
    exportButton.className = 'ytcm-button';
    exportButton.textContent = currentLang === 'zh-TW' ? '匯出設定' : 'Export';
    exportButton.addEventListener('click', () => {
      const data = {
        userColorSettings,
        blockedUsers,
        featureSettings,
        highlightSettings,
        tempUsers: JSON.parse(localStorage.getItem('tempUsers')) || {},
      };
      const blob = new Blob([JSON.stringify(data)], { type: 'application/json' });
      const url = URL.createObjectURL(blob);
      const a = document.createElement('a');
      a.href = url;
      a.download = 'yt_chat_settings.json';
      a.click();
      URL.revokeObjectURL(url);
    });

    const importButton = document.createElement('input');
    importButton.type = 'file';
    importButton.className = 'ytcm-button';
    importButton.accept = '.json';
    importButton.style.width = '100px';
    importButton.addEventListener('change', (e) => {
      const file = e.target.files[0];
      if (!file) return;
      const reader = new FileReader();
      reader.onload = (event) => {
        try {
          const data = JSON.parse(event.target.result);
          localStorage.setItem('userColorSettings', JSON.stringify(data.userColorSettings));
          localStorage.setItem('blockedUsers', JSON.stringify(data.blockedUsers));
          localStorage.setItem('featureSettings', JSON.stringify(data.featureSettings));
          localStorage.setItem('highlightSettings', JSON.stringify(data.highlightSettings));
          localStorage.setItem('tempUsers', JSON.stringify(data.tempUsers));
          window.location.reload();
        } catch {
          alert(currentLang === 'zh-TW' ? '檔案格式錯誤' : 'Invalid file format');
        }
      };
      reader.readAsText(file);
    });

    importExportRow.appendChild(exportButton);
    importExportRow.appendChild(importButton);
    menu.appendChild(importExportRow);

    const blockedUserList = document.createElement('div');
    blockedUserList.textContent =
      currentLang === 'zh-TW' ? '封鎖用戶名單:' : 'Blocked Users:';
    blockedUserList.className = 'ytcm-flex-wrap';

    blockedUsers.forEach((user) => {
      const userItem = document.createElement('div');
      userItem.className = 'ytcm-list-item';
      userItem.textContent = user;
      userItem.addEventListener('click', () => {
        blockedUsers = blockedUsers.filter((u) => u !== user);
        blockedUsersSet.delete(user);
        localStorage.setItem('blockedUsers', JSON.stringify(blockedUsers));
        userItem.remove();
        updateAllMessages(user);
      });
      blockedUserList.appendChild(userItem);
    });
    menu.appendChild(blockedUserList);

    const coloredUserList = document.createElement('div');
    coloredUserList.textContent =
      currentLang === 'zh-TW' ? '被上色用戶名單:' : 'Colored Users:';
    coloredUserList.className = 'ytcm-flex-wrap';

    Object.keys(userColorSettings).forEach((user) => {
      const userItem = document.createElement('div');
      userItem.className = 'ytcm-list-item';
      userItem.textContent = user;
      userItem.addEventListener('click', () => {
        delete userColorSettings[user];
        userColorCache.delete(user);
        localStorage.setItem('userColorSettings', JSON.stringify(userColorSettings));
        userItem.remove();
        updateAllMessages(user);
      });
      coloredUserList.appendChild(userItem);
    });
    menu.appendChild(coloredUserList);

    document.body.appendChild(menu);
    currentMenu = menu;
    menuTimeoutId = setTimeout(closeMenu, MENU_AUTO_CLOSE_DELAY);
  }

  function checkForSpam(msg) {
    if (!featureSettings.spamFilterEnabled) return;
    const nameElement = $(msg, SEL.authorName);
    if (!nameElement) return;
    const userName = nameElement.textContent.trim();

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

    const messageElement = $(msg, SEL.messageBody);
    if (!messageElement) return;

    const textNodes = Array.from(messageElement.childNodes).filter(
      (node) => node.nodeType === Node.TEXT_NODE && !(node.parentElement && node.parentElement.classList?.contains('emoji'))
    );
    const messageText = textNodes.map((node) => node.textContent.trim()).join(' ');

    if (messageCache.has(messageText)) {
      if (featureSettings.spamMode === SPAM_MODES.MARK) {
        msg.setAttribute('data-spam', 'true');
        messageElement.style.textDecoration = 'line-through';
      } else {
        msg.style.display = 'none';
      }
      return;
    }
    messageCache.set(messageText, true);
  }

  function updateMessageCounter(msg) {
    if (!featureSettings.counterEnabled) return;
    const nameElement = $(msg, SEL.authorName);
    if (!nameElement) return;
    const userName = nameElement.textContent.trim();

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

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

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

    const messageElement = $(msg, SEL.messageBody);
    if (messageElement) messageElement.appendChild(counterSpan);
  }

  function processMessage(msg, isInitialLoad = false) {
    if (styleCache.has(msg)) return;

    const authorNameEl = $(msg, SEL.authorName);
    const messageElement = $(msg, SEL.messageBody);
    if (!authorNameEl || !messageElement) return;

    const userName = authorNameEl.textContent.trim();

    if (featureSettings.spamFilterEnabled) checkForSpam(msg);
    msg.removeAttribute('data-spam');

    if (featureSettings.blockEnabled && blockedUsersSet.has(userName)) {
      msg.setAttribute('data-blocked', 'true');
      msg.setAttribute(
        'data-block-mode',
        featureSettings.blockMode === BLOCK_MODES.MARK ? 'mark' : 'hide'
      );
      if (featureSettings.blockMode === BLOCK_MODES.HIDE) {
        msg.style.display = 'none';
      }
      styleCache.set(msg, true);
      return;
    }
    if (msg.hasAttribute('data-blocked')) {
      styleCache.set(msg, true);
      return;
    }

    msg.removeAttribute('data-highlight');
    msg.removeAttribute('data-flagged');

    // 若已因其他系統規則隱藏,略過
    if (msg.style.display === 'none' && !msg.hasAttribute('data-highlight')) {
      return;
    }

    if (featureSettings.flagMode && flaggedUsers[userName]) {
      msg.setAttribute('data-flagged', 'true');
      msg.style.setProperty('--highlight-color', userColorCache.get(userName) || COLOR_OPTIONS.紅色);
    }

    if (
      featureSettings.highlightEnabled &&
      (tempUserCache.has(userName) || userColorCache.get(userName))
    ) {
      const color = tempUserCache.has(userName)
        ? tempUserCache.get(userName).color
        : userColorCache.get(userName);

      const mode = tempUserCache.has(userName)
        ? highlightSettings.tempMode
        : highlightSettings.defaultMode;

      msg.style.setProperty('--highlight-color', color);

      if (
        mode === HIGHLIGHT_MODES.BOTH ||
        mode === HIGHLIGHT_MODES.NAME_ONLY ||
        mode === HIGHLIGHT_MODES.MESSAGE_ONLY
      ) {
        msg.setAttribute(
          'data-highlight',
          mode === HIGHLIGHT_MODES.BOTH ? 'both' : mode === HIGHLIGHT_MODES.NAME_ONLY ? 'name' : 'message'
        );
      }
    }

    updateMessageCounter(msg);

    if (featureSettings.mentionHighlightEnabled) {
      const textNodes = Array.from(messageElement.childNodes).filter(
        (node) => node.nodeType === Node.TEXT_NODE && !(node.parentElement && node.parentElement.classList?.contains('emoji'))
      );
      const messageText = textNodes.map((node) => node.textContent.trim()).join(' ');
      processMentionedUsers(
        messageText,
        userName,
        tempUserCache.has(userName) ? tempUserCache.get(userName).color : userColorCache.get(userName)
      );
    }

    styleCache.set(msg, true);
  }

  function highlightMessages(mutations) {
    cleanupProcessedMessages();

    const newMessages = [];

    // 直接從整個 chat root 掃描未處理的訊息,避免不同 DOM 結構漏抓
    const unhandled = $all(document, SEL.unhandledMessage);
    unhandled.forEach((msg) => {
      newMessages.push(msg);
      processedMessages.set(msg, true);
    });

    // 保障:若仍為空,嘗試抓取尾端若干訊息
    if (newMessages.length === 0) {
      const tail = $all(document, SEL.messageEntry).slice(-MAX_MESSAGE_CACHE_SIZE);
      tail.forEach((msg) => {
        if (!processedMessages.has(msg)) {
          newMessages.push(msg);
          processedMessages.set(msg, true);
        }
      });
    }

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

  function handleClick(event) {
    if (event.button !== 0) return;
    const msgElement = event.target.closest(SEL.messageEntry);
    if (!msgElement) return;

    const messageElement = $(msgElement, SEL.messageBody);
    if (messageElement) {
      const rect = messageElement.getBoundingClientRect();
      const isRightEdge = event.clientX > rect.right - 30;
      if (isRightEdge) return;
    }

    event.stopPropagation();
    event.preventDefault();
    if (currentMenu && !currentMenu.contains(event.target)) closeMenu();

    const authorName = $(msgElement, SEL.authorName);
    const authorImg = $(msgElement, SEL.authorImg);

    // 點擊頭像 -> 色彩選單或頻道(Boost Chat 未保證能抓到 channelId,僅保留原生回退)
    if (authorImg && authorImg.contains(event.target)) {
      if (event.ctrlKey) {
        const URL =
          authorName?.parentNode?.parentNode?.parentNode?.data?.authorExternalChannelId ||
          authorName?.parentNode?.parentNode?.parentNode?.parentNode?.parentNode?.parentNode?.parentNode?.parentNode?.data?.authorExternalChannelId;
        URL && window.open('https://www.youtube.com/channel/' + URL + '/about', '_blank');
      } else {
        if (featureSettings.flagMode) {
          createColorMenu({ type: 'temp', name: authorName.textContent.trim() }, event);
        } else {
          createColorMenu({ type: 'user', name: authorName.textContent.trim() }, event);
        }
      }
    }

    // 點擊作者名 -> 自動插入 @提及
    if (authorName && authorName.contains(event.target)) {
      const inputField = $(document, SEL.inputField);
      if (inputField) {
        setTimeout(() => {
          const userName = authorName.textContent.trim();
          const mentionText = `@${userName}\u2009`;
          const range = document.createRange();
          const selection = window.getSelection();
          range.selectNodeContents(inputField);
          range.collapse(false);
          selection.removeAllRanges();
          selection.addRange(range);
          inputField.focus();
          document.execCommand('insertText', false, mentionText);
          range.setStartAfter(inputField.lastChild);
          range.collapse(true);
          selection.removeAllRanges();
          selection.addRange(range);
        }, 200);
      }
    }
  }

  function overrideNativeListeners() {
    $all(document, SEL.messageEntry).forEach((msg) => {
      msg.addEventListener('click', handleClick, { capture: true });
    });
  }

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

    const observer = new MutationObserver(() => {
      // 直接交給 highlightMessages 自己掃,避免不同結構判斷複雜
      highlightMessages([]);
      // 標記新訊息,避免重複處理
      $all(document, SEL.unhandledMessage).forEach((msg) => {
        msg.setAttribute('data-ytcm-handled', 'true');
        msg.addEventListener('click', handleClick, { capture: true });
        if (!processedMessages.has(msg)) {
          processedMessages.set(msg, true);
          processMessage(msg, true);
        }
      });
    });

    const chatContainer = $(document, SEL.chatRoot);
    if (chatContainer) {
      observer.observe(chatContainer, { childList: true, subtree: true });

      const existingMessages = $all(chatContainer, SEL.messageEntry);
      existingMessages.forEach((msg) => {
        msg.setAttribute('data-ytcm-handled', 'true');
        msg.addEventListener('click', handleClick, { capture: true });
        if (!processedMessages.has(msg)) {
          processedMessages.set(msg, true);
          processMessage(msg, true);
        }
      });
    }

    const controlPanel = createControlPanel();

    return () => {
      observer.disconnect();
      if (controlPanel) controlPanel.remove();
      closeMenu();
    };
  }

  let cleanup = init();

  const checkChatContainer = setInterval(() => {
    if (document.querySelector(SEL.chatRoot) && !cleanup) {
      cleanup = init();
    }
  }, 1000);

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

§
發表於:2025-10-09
編輯:2025-10-09

實測是沒辦法,因為問題似乎有兩大項。
一是無法覆蓋操作,腳本設計的操作是點在頭像上開啟自訂面板、點ID去操作@ID至輸入欄,大概是替換每一則消息,等待我點下去後出現對應操作,但無論是自行嘗試和上述腳本,各版本腳本在boost chat底子上都只剩一處可以操作,就是直播頻道的置頂訊息。操作已經都是boost chat的形狀了。
二是假設腳本局部生效,那沿用記錄應該能自動修改已經在名單上用戶的訊息,實際完全不行,還是只有置頂消息,boost chat的改變真是好大。
也許嘗試動boost chat能夠使我的腳本能運作,不過要嘛損及boost chat,要嘛不適合散播,好像消費的時間成本略高/收益略低了點。

§
發表於:2025-10-09

因為您的版本似乎嘗試加了不同版本的相容,如圖所示聊天室增強腳本是整組害了了,跟我的嘗試嘗試修改也是89不離10,目前打算先放棄。

§
發表於:2025-10-09

暫時來說是沒辦法了。上面都是AI寫的。我沒空做這些處理

發表回覆

登入以回覆