YouTube Mobile 评论自动@用户名 / Auto @username in YouTube Mobile Replies

【中/英】在 YouTube 手机版点击“回复”时自动在输入框插入 @用户名。Automatically insert @username when replying to comments on YouTube Mobile.

// ==UserScript==
// @name         YouTube Mobile 评论自动@用户名 / Auto @username in YouTube Mobile Replies
// @namespace    yt-mobile-autoreply
// @version      1.0
// @description  【中/英】在 YouTube 手机版点击“回复”时自动在输入框插入 @用户名。Automatically insert @username when replying to comments on YouTube Mobile.
// @author       Kemcy BEST & ChatGPT
// @match        https://m.youtube.com/*
// @run-at       document-idle
// @license      MIT
// ==/UserScript==

(function() {
  'use strict';

  // ==== Detect language ====
  const lang = (navigator.language || navigator.userLanguage || 'en').toLowerCase().startsWith('zh') ? 'zh' : 'en';

  // ==== i18n strings ====
  const text = {
    zh: {
      loaded: '[YT 自动回复脚本] 已加载',
      capture: '[YT 自动回复脚本] 捕获用户名: ',
      clickReply: '[YT 自动回复脚本] 检测到点击 Reply 按钮',
      shadowReply: '[YT 自动回复脚本] Shadow DOM 内检测到 Reply 点击',
      dialogFound: '[YT 自动回复脚本] 回复输入框已出现',
      success: '[YT 自动回复脚本] 插入成功 ✅ ',
    },
    en: {
      loaded: '[YT AutoReply] Script loaded',
      capture: '[YT AutoReply] Captured username: ',
      clickReply: '[YT AutoReply] Detected Reply button click',
      shadowReply: '[YT AutoReply] Detected Reply click inside Shadow DOM',
      dialogFound: '[YT AutoReply] Reply input detected',
      success: '[YT AutoReply] Insert success ✅ ',
    }
  }[lang];

  // ====== 页面日志(3秒后自动消失) ======
  function log(msg) {
    let box = document.getElementById('yt-reply-debug');
    if (!box) {
      box = document.createElement('div');
      box.id = 'yt-reply-debug';
      Object.assign(box.style, {
        position: 'fixed',
        bottom: '0',
        left: '0',
        background: 'rgba(0,0,0,0.75)',
        color: '#0f0',
        fontSize: '11px',
        padding: '4px 6px',
        zIndex: 999999,
        fontFamily: 'monospace',
        pointerEvents: 'none'
      });
      document.body.appendChild(box);
    }
    const line = document.createElement('div');
    line.textContent = msg;
    line.style.transition = 'opacity 0.6s ease';
    box.appendChild(line);
    setTimeout(() => {
      line.style.opacity = '0';
      setTimeout(() => line.remove(), 600);
    }, 3000);
  }

  let lastClickedUser = null;

  // ====== 抓用户名,多 selector 兜底 ======
  function extractUsername(comment) {
    if (!comment) return null;
    const sel = [
      '.YtmCommentRendererTitle .yt-core-attributed-string',
      'a.yt-core-attributed-string',
      'yt-attributed-string',
      '[id*="author-text"]',
      '[role="link"] span'
    ];
    for (const s of sel) {
      const el = comment.querySelector(s);
      if (el && el.textContent.trim()) return el.textContent.trim();
    }
    return null;
  }

  // ====== 全局点击捕获 ======
  document.addEventListener('click', function(e) {
    const comment = e.target.closest('ytm-comment-renderer');
    if (comment) {
      lastClickedUser = extractUsername(comment);
      log(`${text.capture}${lastClickedUser || '(null)'}`);
    }

    const replyText = e.target.textContent?.trim();
    if (replyText === 'Reply' || replyText === '回复') {
      log(text.clickReply);
      waitForReplyDialog();
    }
  }, true);

  // ====== Shadow DOM 支持 ======
  const openShadow = Element.prototype.attachShadow;
  Element.prototype.attachShadow = function(init) {
    const shadow = openShadow.call(this, init);
    shadow.addEventListener('click', function(ev) {
      const textContent = ev.target.textContent?.trim();
      if (textContent === 'Reply' || textContent === '回复') {
        log(text.shadowReply);
        waitForReplyDialog();
      }
    }, true);
    return shadow;
  };

  // ====== 等待对话框出现 ======
  function waitForReplyDialog() {
    const observer = new MutationObserver(() => {
      const textarea = document.querySelector('dialog .YtmCommentReplyDialogRendererInput');
      if (textarea) {
        log(text.dialogFound);
        observer.disconnect();
        insertUsernameWithRetry(textarea);
      }
    });
    observer.observe(document.body, { childList: true, subtree: true });
  }

  // ====== 稳定插入逻辑 ======
  function insertUsernameWithRetry(textarea, attempt = 0) {
    if (!textarea || !lastClickedUser) return;
    const username = lastClickedUser.startsWith('@') ? lastClickedUser : '@' + lastClickedUser;

    setTimeout(() => {
      textarea.focus();
      textarea.value = `${username} `;
      textarea.dispatchEvent(new Event('input', { bubbles: true }));

      if (textarea.value.startsWith(username)) {
        log(`${text.success}${username}`);
      } else if (attempt < 15) {
        insertUsernameWithRetry(textarea, attempt + 1);
      }
    }, 100 + attempt * 100);
  }

  log(text.loaded);
})();