// ==UserScript==
// @name Douyin Live Cleaner
// @namespace https://live.douyin.com/
// @version 0.3
// @description 精简抖音直播页面干扰元素,支持悬停弹幕一键+1/复制,并可自动最高画质与网页全屏。
// @author Codex
// @match https://live.douyin.com/*
// @match https://www.douyin.com/*
// @run-at document-idle
// @grant none
// @license MIT
// @downloadURL https://update.greasyfork.icu/scripts/570262/Douyin%20Live%20Cleaner.user.js
// @updateURL https://update.greasyfork.icu/scripts/570262/Douyin%20Live%20Cleaner.meta.js
// ==/UserScript==
(function () {
'use strict';
const HIDE_ATTR = 'data-dy-clean-hidden';
const SAFE_ATTR = 'data-dy-clean-safe';
const UI_ATTR = 'data-dy-clean-ui';
const CHAT_BADGE_STYLE_ATTR = 'data-dy-clean-chat-badges';
const CHAT_BADGE_ROW_MARK_ATTR = 'data-dy-clean-chat-badge-row';
const CHAT_CONTENT_MARK_ATTR = 'data-dy-clean-chat-content';
const CHAT_NICKNAME_MARK_ATTR = 'data-dy-clean-chat-nickname';
const state = {
enabled: true,
panelVisible: true,
hiddenCount: 0,
mainMediaRect: null,
modules: {},
settingsLoaded: false,
scanTimer: 0,
observer: null,
uiBound: false,
roomKey: '',
lastPlayerWakeAt: 0,
lastBestQualityAttemptAt: 0,
lastWebFullscreenAttemptAt: 0,
autoBestQualityDone: false,
autoWebFullscreenDone: false,
chatReplay: {
actionHost: null,
button: null,
copyButton: null,
sourceArea: null,
sourceList: null,
lockTop: -1,
lockRaf: 0,
resumeTimer: 0,
pointerX: -1,
pointerY: -1,
activeText: '',
activeContentKey: '',
activeContentElement: null,
},
};
const SAFE_TEXT_PATTERNS = [
/说点什么/,
/发送/,
/聊天/,
/公告/,
];
const GIFT_BAR_PATTERNS = [
/礼物/,
/送礼/,
/赠送/,
/说点什么/,
/发送/,
/表情/,
/福袋/,
/红包/,
/粉丝团/,
/灯牌/,
/守护/,
];
const CHAT_JOIN_NOTICE_PATTERN = /(?:加入|进入)了直播间/;
const CHAT_BOTTOM_NOTICE_PATTERN = /(?:来了|为主播点赞了)$/;
const CHAT_BOTTOM_NOTICE_SELECTOR =
'[class*="webcast-chatroom___bottom-message"], [class*="webcast-chatroom___bottom_message"]';
const CHAT_NOTICE_REASONS = new Set(['chat-join-notice', 'chat-bottom-notice']);
const CHAT_BADGE_REASONS = new Set(['chat-badge']);
const CHAT_TEXT_CONTENT_SELECTOR =
`[${CHAT_CONTENT_MARK_ATTR}], .webcast-chatroom___content-with-emoji-text, [class*="webcast-chatroom___content-with-emoji-text"]`;
const CHAT_MESSAGE_ITEM_SELECTOR = '.webcast-chatroom___item';
const CHAT_NICKNAME_SELECTOR =
'.webcast-chatroom___nickname, [class*="webcast-chatroom___nickname"]';
const CHAT_NICKNAME_BADGE_SELECTOR =
'.webcast-chatroom___badge, [class*="webcast-chatroom___badge"]';
const CHAT_DISPLAY_TEXT_SELECTOR =
'.webcast-chatroom___display-text, [class*="webcast-chatroom___display-text"]';
const CHAT_REPLAY_LIST_SELECTOR =
'.webcast-chatroom___list, [class*="webcast-chatroom___list"]';
const CHAT_UNREAD_SELECTOR =
'.webcast-chatroom___unread, [class*="webcast-chatroom___unread"]';
const CHAT_FOLD_SELECTOR = '[class*="chat_room_fold"]';
const CHAT_REPLAY_TEXT_MAX_LENGTH = 120;
const TOP_WIDGET_TOOLTIP_PATTERN =
/(?:帮主播完成心愿吧|心愿|差\d+个完成).*(?:赠送|送\d+个|\d+钻)|(?:赠送|送\d+个|\d+钻).*(?:帮主播完成心愿吧|心愿|差\d+个完成)/;
const SETTINGS_STORAGE_KEY = 'dy-live-cleaner-settings';
const MODULE_CONFIGS = [
{
id: 'giftBar',
title: '礼物区域',
description: '统一控制底部礼物栏、礼物托盘和礼物菜单层',
},
{
id: 'topWidgets',
title: '右上角广告/榜单',
description: '统一隐藏展开按钮左侧的广告位、集结位和榜单入口',
},
{
id: 'productButton',
title: '全部商品按钮',
description: '隐藏悬浮在直播视频上的“全部商品”按钮',
},
{
id: 'chatNotices',
title: '入场/点赞提示',
description: '隐藏弹幕区顶部“加入直播间”和底部“来了/点赞了”提示',
},
{
id: 'chatBadges',
title: '昵称前等级牌',
description: '隐藏弹幕区用户名左侧的等级和粉丝团徽章',
},
{
id: 'autoBestQuality',
title: '自动最高画质',
description: '自动展开画质列表并选择当前列表第一项',
},
{
id: 'autoWebFullscreen',
title: '自动网页全屏',
description: '自动展开播放器并切到网页全屏',
},
{
id: 'hoverPlusOne',
title: '悬停弹幕+1',
description: '鼠标移入弹幕时冻结列表,并支持一键填充输入框',
},
];
const HIDE_MODULE_IDS = new Set(['giftBar', 'topWidgets', 'productButton', 'chatNotices', 'chatBadges']);
const DEFAULT_SETTINGS = {
enabled: true,
panelVisible: true,
modules: {
giftBar: true,
topWidgets: true,
productButton: true,
chatNotices: true,
chatBadges: true,
autoBestQuality: true,
autoWebFullscreen: true,
hoverPlusOne: true,
},
};
const EXPLICIT_SAFE_SELECTORS = [
'#room_info_bar',
'[data-e2e="fullscreen-back"]',
'[data-e2e="rooom-info-bar-anchor"]',
'[data-e2e="live-followbutton"]',
'[class*="douyin-player-controls"]',
'[class*="douyin-player-popup"]',
'[class*="douyin-player-tooltips"]',
'[class*="douyin-player-icon"]',
'[data-e2e="quality"]',
'[data-e2e="quality-selector"]',
'[data-e2e="xgplayer-page-full-screen"]',
'.xgplayer-page-full-screen',
'#RightPanelLayout',
'#chatInput',
'[data-e2e="live-chatting"]',
'[class*="webcast-chatroom"]',
CHAT_FOLD_SELECTOR,
'#DanmakuLayout',
'.CanvasDanmakuPlugin',
'#PlayerControlLayout',
];
const MODULE_HIDE_SELECTORS = {
giftBar: [
'#BottomLayout',
'#GiftTrayLayout',
'#GiftMenuLayout',
'[data-e2e="gifts-container"]',
'#giftPanelEntrance',
'[data-e2e="gift-btn"]',
],
topWidgets: [
'[data-e2e="hour-rank-entrance"]',
'[data-e2e="exhibition-banner"]',
'iframe[src*="live_pc_indicator_container_douyin/pages/index.html"]',
],
productButton: [
'[data-e2e="yellowCart-container"]',
],
};
const SAFE_SELECTOR_GROUP = EXPLICIT_SAFE_SELECTORS.join(', ');
const HIDE_SELECTOR_GROUPS = Object.fromEntries(
Object.entries(MODULE_HIDE_SELECTORS).map(([key, selectors]) => [key, selectors.join(', ')]),
);
const MODULE_HIDE_REASONS = {
giftBar: new Set(['explicit-giftBar', 'matched']),
topWidgets: new Set(['explicit-topWidgets', 'indicator-match', 'chat-fold-sibling', 'top-widget-tooltip']),
productButton: new Set(['explicit-productButton']),
chatNotices: CHAT_NOTICE_REASONS,
chatBadges: CHAT_BADGE_REASONS,
};
function loadSettings() {
try {
const raw = localStorage.getItem(SETTINGS_STORAGE_KEY);
const parsed = raw ? JSON.parse(raw) : {};
const modules = parsed.modules || {};
const resolveModule = (moduleId) => {
if (typeof modules[moduleId] === 'boolean') {
return modules[moduleId];
}
if (moduleId === 'giftBar') {
const bottomGiftBar =
typeof modules.bottomGiftBar === 'boolean'
? modules.bottomGiftBar
: DEFAULT_SETTINGS.modules.giftBar;
const giftLayers =
typeof modules.giftLayers === 'boolean' ? modules.giftLayers : bottomGiftBar;
return bottomGiftBar && giftLayers;
}
if (moduleId === 'topWidgets') {
const rankEntry =
typeof modules.rankEntry === 'boolean'
? modules.rankEntry
: DEFAULT_SETTINGS.modules.topWidgets;
const topBanner =
typeof modules.topBanner === 'boolean' ? modules.topBanner : rankEntry;
return rankEntry && topBanner;
}
return DEFAULT_SETTINGS.modules[moduleId];
};
return {
enabled:
typeof parsed.enabled === 'boolean' ? parsed.enabled : DEFAULT_SETTINGS.enabled,
panelVisible:
typeof parsed.panelVisible === 'boolean'
? parsed.panelVisible
: DEFAULT_SETTINGS.panelVisible,
modules: Object.fromEntries(
MODULE_CONFIGS.map((module) => [
module.id,
resolveModule(module.id),
]),
),
};
} catch (_error) {
return {
enabled: DEFAULT_SETTINGS.enabled,
panelVisible: DEFAULT_SETTINGS.panelVisible,
modules: { ...DEFAULT_SETTINGS.modules },
};
}
}
function saveSettings() {
try {
localStorage.setItem(
SETTINGS_STORAGE_KEY,
JSON.stringify({
enabled: state.enabled,
panelVisible: state.panelVisible,
modules: state.modules,
}),
);
} catch (_error) {
// ignore storage failures
}
}
function hydrateSettings() {
if (state.settingsLoaded) {
return;
}
const settings = loadSettings();
state.enabled = settings.enabled;
state.panelVisible = settings.panelVisible;
state.modules = settings.modules;
state.settingsLoaded = true;
}
function isModuleEnabled(moduleId) {
return state.modules[moduleId] !== false;
}
function isHideModule(moduleId) {
return HIDE_MODULE_IDS.has(moduleId);
}
function resetAutomationState(moduleId = '') {
if (!moduleId || moduleId === 'autoBestQuality') {
state.lastBestQualityAttemptAt = 0;
state.autoBestQualityDone = false;
}
if (!moduleId || moduleId === 'autoWebFullscreen') {
state.lastWebFullscreenAttemptAt = 0;
state.autoWebFullscreenDone = false;
}
}
function syncRoomContext() {
let nextRoomKey = '';
if (location.host === 'live.douyin.com') {
nextRoomKey = location.pathname;
} else if (location.host === 'www.douyin.com') {
const match = location.pathname.match(/^\/follow\/live\/[^/?#]+/);
nextRoomKey = match ? match[0] : '';
}
if (state.roomKey === nextRoomKey) {
return;
}
const hadRoomContext = Boolean(state.roomKey);
state.roomKey = nextRoomKey;
syncStyleState();
resetAutomationState();
closeChatReplayOverlay();
if (!nextRoomKey && hadRoomContext) {
revealAll();
}
}
function setCleanerEnabled(nextValue) {
state.enabled = nextValue;
syncStyleState();
resetAutomationState();
saveSettings();
if (!state.enabled) {
if (state.chatReplay.sourceList) {
resumeChatReplayList();
} else {
closeChatReplayOverlay();
}
revealAll();
} else {
scheduleScan(document.body);
}
updateUi();
}
function setPanelVisible(nextValue) {
state.panelVisible = nextValue;
saveSettings();
updateUi();
}
function setModuleEnabled(moduleId, nextValue) {
state.modules[moduleId] = nextValue;
syncStyleState();
resetAutomationState(moduleId);
saveSettings();
if (moduleId === 'hoverPlusOne' && !nextValue) {
if (state.chatReplay.sourceList) {
resumeChatReplayList();
} else {
closeChatReplayOverlay();
}
}
if (isHideModule(moduleId) && !nextValue) {
revealModuleHides(moduleId);
}
if (state.enabled) {
scheduleScan(document.body);
} else {
updateUi();
}
}
function disableAutomationModuleByUser(moduleId) {
if (!state.enabled || !isModuleEnabled(moduleId)) {
return false;
}
setModuleEnabled(moduleId, false);
return true;
}
function normalizeText(value) {
return String(value || '').replace(/\s+/g, ' ').trim();
}
function syncStyleState() {
if (!(document.documentElement instanceof HTMLElement)) {
return;
}
document.documentElement.toggleAttribute(
CHAT_BADGE_STYLE_ATTR,
state.enabled && isModuleEnabled('chatBadges') && Boolean(state.roomKey),
);
}
function getTextBundle(element) {
const parts = [
element.innerText,
element.getAttribute('aria-label'),
element.getAttribute('title'),
element.getAttribute('placeholder'),
element.getAttribute('alt'),
];
return normalizeText(parts.filter(Boolean).join(' '));
}
function isVisible(element) {
if (!(element instanceof HTMLElement)) {
return false;
}
const style = window.getComputedStyle(element);
if (
style.display === 'none' ||
style.visibility === 'hidden' ||
Number.parseFloat(style.opacity || '1') <= 0.02
) {
return false;
}
return element.getClientRects().length > 0;
}
function markSafe(element, reason, depth = 6) {
let current = element;
let remaining = depth;
while (current && current !== document.body && remaining > 0) {
if (!current.hasAttribute(SAFE_ATTR)) {
current.setAttribute(SAFE_ATTR, reason);
}
current = current.parentElement;
remaining -= 1;
}
}
function updateMainMediaRect() {
let rect = null;
let maxArea = 0;
document.querySelectorAll('video, canvas').forEach((node) => {
if (!isVisible(node)) {
return;
}
const currentRect = node.getBoundingClientRect();
const area = currentRect.width * currentRect.height;
if (
currentRect.width >= Math.max(220, window.innerWidth * 0.18) &&
currentRect.height >= Math.max(140, window.innerHeight * 0.18) &&
area > maxArea
) {
maxArea = area;
rect = currentRect;
}
});
state.mainMediaRect = rect;
}
function detectSafeZones() {
updateMainMediaRect();
document.querySelectorAll(SAFE_SELECTOR_GROUP).forEach((node) => {
markSafe(node, 'explicit-safe', 24);
});
document.querySelectorAll('video, canvas').forEach((node) => {
const rect = node.getBoundingClientRect();
if (rect.width >= window.innerWidth * 0.2 && rect.height >= window.innerHeight * 0.2) {
markSafe(node, 'media', 1);
}
});
document
.querySelectorAll('input, textarea, [contenteditable="true"]')
.forEach((node) => {
const text = getTextBundle(node);
if (SAFE_TEXT_PATTERNS.some((pattern) => pattern.test(text))) {
markSafe(node, 'chat-input', 2);
}
});
}
function containsMedia(element) {
return Boolean(element.querySelector('video, canvas'));
}
function isUserScriptUi(element) {
return Boolean(element.closest(`[${UI_ATTR}]`));
}
function matchesSelectorGroup(element, selectorGroup) {
if (!selectorGroup) {
return false;
}
try {
return Boolean(element.closest(selectorGroup));
} catch (_error) {
return false;
}
}
function isExplicitSafeElement(element) {
return matchesSelectorGroup(element, SAFE_SELECTOR_GROUP);
}
function isExplicitHideElement(element) {
return Object.entries(HIDE_SELECTOR_GROUPS).some(
([moduleId, selectorGroup]) => isModuleEnabled(moduleId) && matchesSelectorGroup(element, selectorGroup),
);
}
function findCompactNoticeContainer(element, stopNode) {
const baseText = getTextBundle(element);
let target = element;
let current = element.parentElement;
while (current && current !== stopNode && current !== document.body) {
const currentText = getTextBundle(current);
if (
!currentText ||
!currentText.includes(baseText) ||
currentText.length > baseText.length + 24 ||
current.querySelector('#chatInput, [data-e2e="live-chatting"]')
) {
break;
}
target = current;
current = current.parentElement;
}
return target;
}
function isChatJoinNoticeElement(element) {
if (!(element instanceof HTMLElement) || !element.closest('#chatroom')) {
return false;
}
const text = getTextBundle(element);
const rect = element.getBoundingClientRect();
if (
!CHAT_JOIN_NOTICE_PATTERN.test(text) ||
text.length > 96 ||
rect.height <= 0 ||
rect.height > 72 ||
element.closest(CHAT_BOTTOM_NOTICE_SELECTOR) ||
element.querySelector(`#chatInput, [data-e2e="live-chatting"], ${CHAT_REPLAY_LIST_SELECTOR}`)
) {
return false;
}
return true;
}
function isChatBottomNoticeContainer(element) {
if (!(element instanceof HTMLElement)) {
return false;
}
const container = element.matches(CHAT_BOTTOM_NOTICE_SELECTOR)
? element
: element.closest(CHAT_BOTTOM_NOTICE_SELECTOR);
if (!(container instanceof HTMLElement)) {
return false;
}
const text = getTextBundle(container);
return CHAT_BOTTOM_NOTICE_PATTERN.test(text);
}
function isChatReplayEnabled() {
return state.enabled && isModuleEnabled('hoverPlusOne') && Boolean(state.roomKey);
}
function clearChatReplayActiveContent() {
const replay = state.chatReplay;
if (replay.activeContentElement instanceof HTMLElement) {
replay.activeContentElement.removeAttribute('data-dy-clean-replay-active');
}
replay.activeContentElement = null;
replay.activeContentKey = '';
}
function cancelChatReplayResume() {
window.clearTimeout(state.chatReplay.resumeTimer || 0);
state.chatReplay.resumeTimer = 0;
}
function cancelChatReplayScrollLock() {
window.cancelAnimationFrame(state.chatReplay.lockRaf || 0);
state.chatReplay.lockRaf = 0;
}
function removeChatReplayUiNodes() {
document
.querySelectorAll(
`[${UI_ATTR}="chat-replay-actions"], [${UI_ATTR}="chat-replay-plus"], [${UI_ATTR}="chat-replay-copy"]`,
)
.forEach((node) => {
node.remove();
});
state.chatReplay.actionHost = null;
state.chatReplay.button = null;
state.chatReplay.copyButton = null;
}
function hideChatReplayButton() {
const replay = state.chatReplay;
clearChatReplayActiveContent();
replay.activeText = '';
if (replay.actionHost instanceof HTMLElement) {
replay.actionHost.hidden = true;
}
}
function closeChatReplayOverlay() {
const replay = state.chatReplay;
cancelChatReplayResume();
cancelChatReplayScrollLock();
clearChatReplayActiveContent();
replay.activeText = '';
removeChatReplayUiNodes();
replay.lockTop = -1;
replay.pointerX = -1;
replay.pointerY = -1;
replay.sourceArea = null;
replay.sourceList = null;
}
function getChatReplayListFromElement(element) {
if (!(element instanceof HTMLElement)) {
return null;
}
const directList = element.closest(CHAT_REPLAY_LIST_SELECTOR);
if (directList instanceof HTMLElement) {
return directList;
}
let current = getChatReplayMessageItem(element)?.parentElement || element.parentElement;
while (current && current !== document.body) {
if (
current instanceof HTMLElement &&
current.closest('#chatroom') &&
current.querySelector('[data-index]')
) {
return current;
}
current = current.parentElement;
}
return null;
}
function getChatReplayMessageItem(element) {
if (!(element instanceof Element)) {
return null;
}
const item = element.closest(CHAT_MESSAGE_ITEM_SELECTOR);
return item instanceof HTMLElement ? item : null;
}
function getChatReplayContentElement(element) {
if (!(element instanceof Element)) {
return null;
}
const item = getChatReplayMessageItem(element);
const primaryContent = getPrimaryChatContentElement(item);
if (
primaryContent instanceof HTMLElement &&
(primaryContent === element || primaryContent.contains(element))
) {
return primaryContent;
}
const directContent = element.closest(CHAT_TEXT_CONTENT_SELECTOR);
return directContent instanceof HTMLElement ? directContent : null;
}
function getChatReplayText(item) {
if (!(item instanceof HTMLElement)) {
return '';
}
if (
item.closest(CHAT_BOTTOM_NOTICE_SELECTOR) ||
item.closest('[data-dy-clean-hidden="chat-bottom-notice"]')
) {
return '';
}
const content = getPrimaryChatContentElement(item);
if (!(content instanceof HTMLElement)) {
return '';
}
const itemText = normalizeText(getTextBundle(item));
const contentText = normalizeText(getTextBundle(content));
if (!contentText || contentText.length > CHAT_REPLAY_TEXT_MAX_LENGTH) {
return '';
}
if (
CHAT_BOTTOM_NOTICE_PATTERN.test(contentText) ||
CHAT_JOIN_NOTICE_PATTERN.test(itemText) ||
/为主播点赞了/.test(itemText) ||
/送出了/.test(itemText)
) {
return '';
}
return contentText;
}
function findChatReplayScrollContainer(list) {
if (!(list instanceof HTMLElement)) {
return null;
}
const candidates = [list];
let current = list.parentElement;
while (current && current !== document.body) {
if (!current.closest('#chatroom')) {
break;
}
candidates.push(current);
current = current.parentElement;
}
let fallback = null;
for (const candidate of candidates) {
if (!(candidate instanceof HTMLElement) || !isVisible(candidate)) {
continue;
}
const style = window.getComputedStyle(candidate);
const allowsScroll =
/auto|scroll/.test(style.overflowY || '') ||
/auto|scroll/.test(style.overflow || '') ||
candidate.scrollHeight > candidate.clientHeight + 4;
if (!allowsScroll) {
continue;
}
if (!fallback) {
fallback = candidate;
}
const previousTop = candidate.scrollTop;
candidate.scrollTop = previousTop + 1;
const didScroll = candidate.scrollTop !== previousTop;
candidate.scrollTop = previousTop;
if (didScroll) {
return candidate;
}
}
return fallback;
}
function getChatReplayContentKey(content, text) {
if (!(content instanceof HTMLElement)) {
return normalizeText(text);
}
const indexHost = content.closest('[data-index]');
const dataIndex = indexHost instanceof HTMLElement ? indexHost.getAttribute('data-index') : '';
return `${dataIndex || ''}|${normalizeText(text)}`;
}
function ensureChatReplayPaused(content) {
const list = getChatReplayListFromElement(content);
const container = findChatReplayScrollContainer(list);
if (!(container instanceof HTMLElement) || !(list instanceof HTMLElement)) {
return false;
}
if (
state.chatReplay.sourceList instanceof HTMLElement &&
state.chatReplay.sourceList !== container
) {
resumeChatReplayList();
}
if (state.chatReplay.sourceList !== container) {
state.chatReplay.sourceList = container;
}
state.chatReplay.sourceArea = list;
if (container.scrollHeight > container.clientHeight + 4) {
const maxTop = Math.max(0, container.scrollHeight - container.clientHeight);
const nearBottom = maxTop - container.scrollTop <= 6;
state.chatReplay.lockTop = nearBottom
? Math.max(0, maxTop - 1)
: Math.max(0, container.scrollTop);
container.scrollTop = state.chatReplay.lockTop;
}
cancelChatReplayScrollLock();
state.chatReplay.lockRaf = window.requestAnimationFrame(function lockChatReplayScroll() {
const replay = state.chatReplay;
if (replay.sourceList !== container || !(container instanceof HTMLElement)) {
return;
}
if (Math.abs(container.scrollTop - replay.lockTop) > 1) {
container.scrollTop = replay.lockTop;
}
replay.lockRaf = window.requestAnimationFrame(lockChatReplayScroll);
});
cancelChatReplayResume();
return true;
}
function positionChatReplayButton(content) {
const actionHost = ensureChatReplayActionHost();
const button = ensureChatReplayButton();
const copyButton = ensureChatReplayCopyButton();
if (
!(actionHost instanceof HTMLElement) ||
!(button instanceof HTMLButtonElement) ||
!(copyButton instanceof HTMLButtonElement) ||
!(content instanceof HTMLElement)
) {
return;
}
actionHost.hidden = false;
const anchorRect = content.getBoundingClientRect();
const hostWidth = actionHost.offsetWidth || 108;
const hostHeight = actionHost.offsetHeight || 28;
const pointerX = state.chatReplay.pointerX;
let left = pointerX >= 0 ? pointerX - hostWidth / 2 : anchorRect.left + 6;
const contentMinLeft = Math.round(anchorRect.left);
const contentMaxLeft = Math.round(anchorRect.right - hostWidth);
if (contentMaxLeft >= contentMinLeft) {
left = Math.max(contentMinLeft, Math.min(left, contentMaxLeft));
} else {
left = contentMinLeft;
}
let top = anchorRect.top - hostHeight + 2;
left = Math.max(6, Math.min(left, window.innerWidth - hostWidth - 6));
top = Math.max(6, Math.min(top, window.innerHeight - hostHeight - 6));
actionHost.style.left = `${Math.round(left)}px`;
actionHost.style.top = `${Math.round(top)}px`;
}
function isPointInsideElementRect(element, clientX, clientY, padding = 0) {
if (!(element instanceof HTMLElement) || element.hidden) {
return false;
}
const rect = element.getBoundingClientRect();
return (
clientX >= rect.left - padding &&
clientX <= rect.right + padding &&
clientY >= rect.top - padding &&
clientY <= rect.bottom + padding
);
}
function setActiveChatReplayContent(content, nextText = '') {
if (!(content instanceof HTMLElement)) {
hideChatReplayButton();
return false;
}
const item = getChatReplayMessageItem(content);
const text = normalizeText(nextText || getChatReplayText(item));
if (!text) {
hideChatReplayButton();
return false;
}
const replay = state.chatReplay;
const nextKey = getChatReplayContentKey(content, text);
const shouldReposition =
replay.activeContentElement !== content ||
replay.activeContentKey !== nextKey ||
!(replay.button instanceof HTMLButtonElement) ||
replay.button.hidden;
if (replay.activeContentElement !== content) {
clearChatReplayActiveContent();
content.setAttribute('data-dy-clean-replay-active', 'true');
replay.activeContentElement = content;
}
replay.activeContentKey = nextKey;
replay.activeText = text;
if (shouldReposition) {
positionChatReplayButton(content);
}
return true;
}
function ensureChatReplayActionHost() {
const replay = state.chatReplay;
if (replay.actionHost instanceof HTMLElement && replay.actionHost.isConnected) {
return replay.actionHost;
}
const host = document.createElement('div');
host.setAttribute(UI_ATTR, 'chat-replay-actions');
host.hidden = true;
document.body.appendChild(host);
host.addEventListener('pointerdown', (event) => {
event.stopPropagation();
});
host.addEventListener('pointerenter', () => {
cancelChatReplayResume();
});
host.addEventListener('pointerleave', (event) => {
if (
event.relatedTarget instanceof Node &&
state.chatReplay.activeContentElement instanceof HTMLElement &&
state.chatReplay.activeContentElement.contains(event.relatedTarget)
) {
cancelChatReplayResume();
return;
}
resumeChatReplayList();
});
replay.actionHost = host;
return host;
}
function ensureChatReplayButton() {
const replay = state.chatReplay;
if (replay.button instanceof HTMLButtonElement && replay.button.isConnected) {
return replay.button;
}
const button = document.createElement('button');
button.type = 'button';
button.setAttribute(UI_ATTR, 'chat-replay-plus');
button.textContent = '+1';
ensureChatReplayActionHost().appendChild(button);
button.addEventListener('pointerdown', (event) => {
event.stopPropagation();
});
button.addEventListener('click', (event) => {
event.preventDefault();
event.stopPropagation();
if (fillChatInputWithReplayText(state.chatReplay.activeText)) {
resumeChatReplayList();
}
});
replay.button = button;
return button;
}
function ensureChatReplayCopyButton() {
const replay = state.chatReplay;
if (replay.copyButton instanceof HTMLButtonElement && replay.copyButton.isConnected) {
return replay.copyButton;
}
const button = document.createElement('button');
button.type = 'button';
button.setAttribute(UI_ATTR, 'chat-replay-copy');
button.textContent = '复制';
ensureChatReplayActionHost().appendChild(button);
button.addEventListener('pointerdown', (event) => {
event.stopPropagation();
});
button.addEventListener('click', (event) => {
event.preventDefault();
event.stopPropagation();
copyTextToClipboard(state.chatReplay.activeText).then((copied) => {
if (copied) {
resumeChatReplayList();
}
});
});
replay.copyButton = button;
return button;
}
function fallbackCopyTextToClipboard(text) {
const normalized = normalizeText(text);
if (!normalized) {
return false;
}
const textarea = document.createElement('textarea');
textarea.value = normalized;
textarea.setAttribute('readonly', 'readonly');
textarea.style.position = 'fixed';
textarea.style.top = '-9999px';
textarea.style.left = '-9999px';
textarea.style.opacity = '0';
document.body.appendChild(textarea);
textarea.focus();
textarea.select();
let copied = false;
try {
copied = document.execCommand('copy');
} catch (_error) {
copied = false;
}
textarea.remove();
return copied;
}
async function copyTextToClipboard(text) {
const normalized = normalizeText(text);
if (!normalized) {
return false;
}
if (navigator.clipboard && window.isSecureContext) {
try {
await navigator.clipboard.writeText(normalized);
return true;
} catch (_error) {
// fall through
}
}
return fallbackCopyTextToClipboard(normalized);
}
function resumeChatReplayList() {
const replay = state.chatReplay;
cancelChatReplayResume();
cancelChatReplayScrollLock();
const container = replay.sourceList;
const unreadTip =
container instanceof HTMLElement && container.parentElement
? container.parentElement.querySelector(
CHAT_UNREAD_SELECTOR,
)
: null;
closeChatReplayOverlay();
if (container instanceof HTMLElement) {
try {
if (unreadTip instanceof HTMLElement && isVisible(unreadTip)) {
if (!invokeReactClick(unreadTip)) {
clickElement(unreadTip);
}
} else {
container.scrollTop = container.scrollHeight;
}
} catch (_error) {
// keep overlay closed even if Douyin changed resume behavior
}
}
}
function openChatReplayOverlay(content) {
if (!isChatReplayEnabled() || !(content instanceof HTMLElement) || content.closest(`[${UI_ATTR}]`)) {
return false;
}
const item = getChatReplayMessageItem(content);
const text = normalizeText(getChatReplayText(item));
if (!text) {
if (state.chatReplay.sourceList) {
hideChatReplayButton();
}
return false;
}
if (!ensureChatReplayPaused(content)) {
resumeChatReplayList();
return false;
}
return setActiveChatReplayContent(content, text);
}
function getChatEditableElement() {
const chatInput = document.querySelector('#chatInput');
if (!(chatInput instanceof HTMLElement)) {
return null;
}
const editable = chatInput.querySelector('[data-e2e="live-chatting"] [contenteditable="true"]');
return editable instanceof HTMLElement ? editable : null;
}
function selectElementContents(element) {
if (!(element instanceof HTMLElement)) {
return false;
}
const selection = window.getSelection();
if (!selection) {
return false;
}
const range = document.createRange();
range.selectNodeContents(element);
selection.removeAllRanges();
selection.addRange(range);
return true;
}
function escapeHtml(value) {
return String(value || '')
.replace(/&/g, '&')
.replace(//g, '>')
.replace(/\n/g, '
');
}
function dispatchChatEditorEvent(target, type, data = '') {
if (!(target instanceof HTMLElement)) {
return;
}
try {
target.dispatchEvent(
new InputEvent(type, {
bubbles: true,
cancelable: type === 'beforeinput',
composed: true,
data,
inputType: 'insertText',
}),
);
} catch (_error) {
target.dispatchEvent(
new Event(type, {
bubbles: true,
cancelable: type === 'beforeinput',
}),
);
}
}
function fillChatInputWithReplayText(text) {
const normalized = normalizeText(text);
const inputBox = document.querySelector('#chatInput [data-e2e="live-chatting"]');
const editable = getChatEditableElement();
if (!(editable instanceof HTMLElement) || !normalized) {
return false;
}
if (inputBox instanceof HTMLElement) {
if (!invokeReactClick(inputBox)) {
clickElement(inputBox);
}
}
editable.focus();
selectElementContents(editable);
dispatchChatEditorEvent(editable, 'beforeinput', normalized);
let inserted = false;
try {
inserted = document.execCommand('insertText', false, normalized);
} catch (_error) {
inserted = false;
}
if (!inserted) {
try {
inserted = document.execCommand('insertHTML', false, escapeHtml(normalized));
} catch (_error) {
inserted = false;
}
}
if (!inserted) {
editable.textContent = normalized;
}
dispatchChatEditorEvent(editable, 'input', normalized);
if (inputBox instanceof HTMLElement) {
dispatchChatEditorEvent(inputBox, 'input', normalized);
}
editable.dispatchEvent(new Event('change', { bubbles: true }));
editable.focus();
const selection = window.getSelection();
if (selection && selection.rangeCount >= 1) {
selection.collapseToEnd();
}
return true;
}
function scheduleChatReplayResume(delay = 0) {
cancelChatReplayResume();
state.chatReplay.resumeTimer = window.setTimeout(() => {
resumeChatReplayList();
}, delay);
}
function isFoldSiblingElement(element) {
if (!(element instanceof HTMLElement)) {
return false;
}
let current = element;
while (current && current !== document.body) {
const parent = current.parentElement;
if (!parent) {
return false;
}
const fold = Array.from(parent.children).find(
(node) => node instanceof HTMLElement && node.matches(CHAT_FOLD_SELECTOR),
);
if (fold instanceof HTMLElement) {
let sibling = fold.previousElementSibling;
while (sibling) {
if (sibling === current || sibling.contains(current)) {
return true;
}
sibling = sibling.previousElementSibling;
}
}
current = parent;
}
return false;
}
function hasProtectedDescendant(element) {
try {
return Boolean(element.querySelector(`[${SAFE_ATTR}]`));
} catch (_error) {
return false;
}
}
function shouldSkip(element) {
if (!(element instanceof HTMLElement)) {
return true;
}
if (element.hasAttribute(UI_ATTR) || isUserScriptUi(element)) {
return true;
}
const tag = element.tagName;
if (
tag === 'HTML' ||
tag === 'BODY' ||
tag === 'SCRIPT' ||
tag === 'STYLE' ||
tag === 'LINK' ||
tag === 'META' ||
tag === 'VIDEO' ||
tag === 'CANVAS'
) {
return true;
}
if (!isVisible(element)) {
return true;
}
return false;
}
function getOverlapRatio(rect, mediaRect) {
if (!mediaRect) {
return 0;
}
const width = Math.max(0, Math.min(rect.right, mediaRect.right) - Math.max(rect.left, mediaRect.left));
const height = Math.max(0, Math.min(rect.bottom, mediaRect.bottom) - Math.max(rect.top, mediaRect.top));
const overlapArea = width * height;
const ownArea = rect.width * rect.height;
if (overlapArea <= 0 || ownArea <= 0) {
return 0;
}
return overlapArea / ownArea;
}
function isVideoBottomOverlay(element) {
const mediaRect = state.mainMediaRect;
if (!mediaRect) {
return false;
}
const rect = element.getBoundingClientRect();
if (
rect.width < 100 ||
rect.height < 28 ||
rect.height > mediaRect.height * 0.34 ||
rect.top < mediaRect.top + mediaRect.height * 0.48 ||
rect.bottom > mediaRect.bottom + 24
) {
return false;
}
const overlapRatio = getOverlapRatio(rect, mediaRect);
if (overlapRatio < 0.58) {
return false;
}
const text = getTextBundle(element).slice(0, 240);
const buttonCount = element.querySelectorAll('button, [role="button"]').length;
const iconCount = element.querySelectorAll('img, svg').length;
const inputCount = element.querySelectorAll('input, textarea, [contenteditable="true"]').length;
const hintMatches = GIFT_BAR_PATTERNS.filter((pattern) => pattern.test(text)).length;
const bottomGap = Math.abs(mediaRect.bottom - rect.bottom);
if (hintMatches >= 1) {
return true;
}
if (inputCount >= 1 && (buttonCount + iconCount) >= 2 && rect.height <= 120) {
return true;
}
if (
bottomGap <= 20 &&
rect.height <= 110 &&
rect.width >= mediaRect.width * 0.35 &&
rect.width <= mediaRect.width * 0.98 &&
(buttonCount + iconCount) >= 4
) {
return true;
}
return false;
}
function shouldHide(element) {
if (isExplicitSafeElement(element)) {
return false;
}
if (!isModuleEnabled('giftBar') && matchesSelectorGroup(element, HIDE_SELECTOR_GROUPS.giftBar)) {
return false;
}
if (
!isModuleEnabled('topWidgets') &&
(matchesSelectorGroup(element, HIDE_SELECTOR_GROUPS.topWidgets) || isFoldSiblingElement(element))
) {
return false;
}
if (isExplicitHideElement(element)) {
return true;
}
if (containsMedia(element)) {
return false;
}
if (hasProtectedDescendant(element)) {
return false;
}
const isBottomGiftBarOverlay = isModuleEnabled('giftBar') && isVideoBottomOverlay(element);
if (element.matches('input, textarea, [contenteditable="true"]') && !isBottomGiftBarOverlay) {
return false;
}
const rect = element.getBoundingClientRect();
if (rect.width <= 0 || rect.height <= 0) {
return false;
}
if (rect.width >= window.innerWidth * 0.95 && rect.height >= window.innerHeight * 0.95) {
return false;
}
if (isBottomGiftBarOverlay) {
return true;
}
return false;
}
function hideElement(element, reason) {
if (!(element instanceof HTMLElement) || element.hasAttribute(HIDE_ATTR)) {
return;
}
element.setAttribute(HIDE_ATTR, reason);
}
function unhideElement(element) {
if (!(element instanceof HTMLElement) || !element.hasAttribute(HIDE_ATTR)) {
return;
}
element.removeAttribute(HIDE_ATTR);
}
function applyExplicitHides() {
Object.entries(HIDE_SELECTOR_GROUPS).forEach(([moduleId, selectorGroup]) => {
if (!isModuleEnabled(moduleId) || !selectorGroup) {
return;
}
document.querySelectorAll(selectorGroup).forEach((node) => {
hideElement(node, `explicit-${moduleId}`);
});
});
}
function applyIndicatorHides() {
if (!isModuleEnabled('topWidgets')) {
return;
}
document
.querySelectorAll('iframe[src*="live_pc_indicator_container_douyin/pages/index.html"]')
.forEach((node) => {
const wrappers = [
node.closest('[data-e2e="exhibition-banner"]'),
node.parentElement,
node.parentElement ? node.parentElement.parentElement : null,
];
wrappers.forEach((wrapper) => {
if (
!(wrapper instanceof HTMLElement) ||
isExplicitSafeElement(wrapper) ||
wrapper.querySelector(CHAT_FOLD_SELECTOR)
) {
return;
}
hideElement(wrapper, 'indicator-match');
});
});
}
function applyChatFoldSiblingHides() {
if (!isModuleEnabled('topWidgets')) {
return;
}
document.querySelectorAll(CHAT_FOLD_SELECTOR).forEach((fold) => {
let sibling = fold.previousElementSibling;
while (sibling) {
const rect = sibling.getBoundingClientRect();
const shouldHideSibling =
rect.height <= 100 &&
!isExplicitSafeElement(sibling) &&
!sibling.querySelector(CHAT_FOLD_SELECTOR);
if (shouldHideSibling) {
hideElement(sibling, 'chat-fold-sibling');
}
sibling = sibling.previousElementSibling;
}
});
}
function applyTopWidgetTooltipHides() {
if (!isModuleEnabled('topWidgets')) {
return;
}
document.querySelectorAll('.dylive-tooltip').forEach((node) => {
if (!(node instanceof HTMLElement)) {
return;
}
const text = getTextBundle(node);
if (TOP_WIDGET_TOOLTIP_PATTERN.test(text)) {
hideElement(node, 'top-widget-tooltip');
}
});
}
function applyChatNoticeHides() {
if (!isModuleEnabled('chatNotices')) {
return;
}
document.querySelectorAll(CHAT_BOTTOM_NOTICE_SELECTOR).forEach((node) => {
if (isChatBottomNoticeContainer(node)) {
hideElement(node, 'chat-bottom-notice');
}
});
const chatroom = document.getElementById('chatroom');
if (!(chatroom instanceof HTMLElement)) {
return;
}
chatroom.querySelectorAll('div').forEach((node) => {
if (isChatJoinNoticeElement(node)) {
hideElement(findCompactNoticeContainer(node, chatroom), 'chat-join-notice');
}
});
}
function isChatNicknameElement(element) {
return Boolean(
element instanceof HTMLElement &&
element.matches(CHAT_NICKNAME_SELECTOR),
);
}
function markChatNickname(element) {
if (element instanceof HTMLElement && !element.hasAttribute(CHAT_NICKNAME_MARK_ATTR)) {
element.setAttribute(CHAT_NICKNAME_MARK_ATTR, 'true');
}
}
function markChatBadgeRow(element) {
if (element instanceof HTMLElement && !element.hasAttribute(CHAT_BADGE_ROW_MARK_ATTR)) {
element.setAttribute(CHAT_BADGE_ROW_MARK_ATTR, 'true');
}
}
function markChatContent(element) {
if (element instanceof HTMLElement && !element.hasAttribute(CHAT_CONTENT_MARK_ATTR)) {
element.setAttribute(CHAT_CONTENT_MARK_ATTR, 'true');
}
}
function isChatNicknameCandidate(element) {
if (!(element instanceof HTMLElement)) {
return false;
}
if (isChatNicknameElement(element)) {
return true;
}
if (
!(element.previousElementSibling instanceof HTMLElement) ||
!(element.nextElementSibling instanceof HTMLElement)
) {
return false;
}
if (element.querySelector('img, svg, video, canvas')) {
return false;
}
const text = normalizeText(element.textContent);
if (!text || text.length > 80) {
return false;
}
return /[::]\s*$/.test(text);
}
function getChatNicknameFromRow(row) {
if (!(row instanceof HTMLElement)) {
return null;
}
const nickname = Array.from(row.children).find(
(child) => child instanceof HTMLElement && isChatNicknameCandidate(child),
);
return nickname instanceof HTMLElement ? nickname : null;
}
function getChatContentAfterNickname(nickname) {
if (!(nickname instanceof HTMLElement)) {
return null;
}
const content = nickname.nextElementSibling;
if (!(content instanceof HTMLElement)) {
return null;
}
markChatContent(content);
return content;
}
function isLikelyChatBadgeRow(element) {
if (!(element instanceof HTMLElement)) {
return false;
}
const children = Array.from(element.children).filter((child) => child instanceof HTMLElement);
if (children.length < 2 || children.length > 8) {
return false;
}
return children.some((child) => isChatNicknameCandidate(child));
}
function hideLeadingChatBadgeSiblings(nickname) {
if (!(nickname instanceof HTMLElement)) {
return;
}
markChatNickname(nickname);
getChatContentAfterNickname(nickname);
let sibling = nickname.previousElementSibling;
while (sibling instanceof HTMLElement) {
hideElement(sibling, 'chat-badge');
sibling = sibling.previousElementSibling;
}
}
function hideInlineChatBadgeChildren(nickname) {
if (!(nickname instanceof HTMLElement)) {
return;
}
Array.from(nickname.children).forEach((child) => {
if (!(child instanceof HTMLElement)) {
return;
}
if (
child.matches(CHAT_NICKNAME_BADGE_SELECTOR) ||
child.matches('img, svg') ||
child.querySelector('img, svg')
) {
hideElement(child, 'chat-badge');
}
});
}
function hideChatBadgeRow(row) {
if (!(row instanceof HTMLElement)) {
return;
}
markChatBadgeRow(row);
const nickname = getChatNicknameFromRow(row);
if (nickname instanceof HTMLElement) {
getChatContentAfterNickname(nickname);
}
Array.from(row.children).forEach((child) => {
if (!(child instanceof HTMLElement) || !isChatNicknameCandidate(child)) {
return;
}
hideLeadingChatBadgeSiblings(child);
hideInlineChatBadgeChildren(child);
});
}
function collectMatchingElements(root, selector) {
if (!(root instanceof Element)) {
return [];
}
const matched = [];
if (root.matches(selector)) {
matched.push(root);
}
matched.push(...root.querySelectorAll(selector));
return matched;
}
function collectChatBadgeRows(root) {
if (!(root instanceof Element)) {
return [];
}
const rows = new Set();
const pushRow = (node) => {
if (isLikelyChatBadgeRow(node)) {
rows.add(node);
}
};
if (isLikelyChatBadgeRow(root)) {
rows.add(root);
}
const rootItem =
root instanceof HTMLElement && root.matches(CHAT_MESSAGE_ITEM_SELECTOR)
? root
: root.closest(CHAT_MESSAGE_ITEM_SELECTOR);
if (rootItem instanceof HTMLElement) {
rootItem.querySelectorAll('div').forEach(pushRow);
}
collectMatchingElements(root, CHAT_MESSAGE_ITEM_SELECTOR).forEach((item) => {
item.querySelectorAll('div').forEach(pushRow);
});
collectMatchingElements(root, CHAT_DISPLAY_TEXT_SELECTOR).forEach(pushRow);
return Array.from(rows);
}
function getPrimaryChatContentElement(item) {
if (!(item instanceof HTMLElement)) {
return null;
}
const row = collectChatBadgeRows(item)[0];
const nickname = getChatNicknameFromRow(row);
if (nickname instanceof HTMLElement) {
const content = getChatContentAfterNickname(nickname);
if (content instanceof HTMLElement) {
return content;
}
}
const fallback = item.querySelector(CHAT_TEXT_CONTENT_SELECTOR);
return fallback instanceof HTMLElement ? fallback : null;
}
function isPotentialChatBadgeNode(node) {
if (!(node instanceof HTMLElement)) {
return false;
}
return Boolean(
node.matches(CHAT_MESSAGE_ITEM_SELECTOR) ||
node.closest(CHAT_MESSAGE_ITEM_SELECTOR) ||
node.querySelector(CHAT_MESSAGE_ITEM_SELECTOR) ||
isLikelyChatBadgeRow(node),
);
}
function applyChatBadgeHidesInRoot(root) {
if (!isModuleEnabled('chatBadges') || !(root instanceof Element)) {
return;
}
collectChatBadgeRows(root).forEach((node) => {
hideChatBadgeRow(node);
});
collectMatchingElements(root, `${CHAT_NICKNAME_SELECTOR}, [${CHAT_NICKNAME_MARK_ATTR}]`).forEach((node) => {
hideLeadingChatBadgeSiblings(node);
hideInlineChatBadgeChildren(node);
});
}
function applyChatBadgeHides() {
applyChatBadgeHidesInRoot(document.body);
}
function getPrimaryPlayerSurface() {
const candidates = [
document.querySelector('#PlayerLayout .douyin-player'),
document.querySelector('#PlayerLayout [class*="douyin-player"]'),
document.querySelector('#PlayerLayout'),
document.querySelector('.__livingPlayer__'),
document.querySelector('video') ? document.querySelector('video').parentElement : null,
];
for (const candidate of candidates) {
if (candidate instanceof HTMLElement && isVisible(candidate)) {
return candidate;
}
}
return null;
}
function wakePlayerControls(force = false) {
const surface = getPrimaryPlayerSurface();
if (!(surface instanceof HTMLElement)) {
return false;
}
const now = Date.now();
if (!force && now - state.lastPlayerWakeAt < 320) {
return true;
}
const rect = surface.getBoundingClientRect();
if (rect.width <= 0 || rect.height <= 0) {
return false;
}
state.lastPlayerWakeAt = now;
const clientX = Math.round(rect.left + Math.min(Math.max(rect.width * 0.76, 48), rect.width - 20));
const clientY = Math.round(rect.top + Math.max(rect.height - 54, rect.height * 0.82));
['mouseover', 'mousemove', 'mouseenter'].forEach((type) => {
surface.dispatchEvent(
new MouseEvent(type, {
bubbles: type !== 'mouseenter',
cancelable: true,
view: window,
clientX,
clientY,
}),
);
});
return true;
}
function clickElement(element) {
if (!(element instanceof HTMLElement)) {
return false;
}
const rect = element.getBoundingClientRect();
const clientX = Math.round(rect.left + rect.width / 2);
const clientY = Math.round(rect.top + rect.height / 2);
if (typeof PointerEvent === 'function') {
element.dispatchEvent(
new PointerEvent('pointerdown', {
bubbles: true,
cancelable: true,
view: window,
pointerId: 1,
pointerType: 'mouse',
isPrimary: true,
clientX,
clientY,
}),
);
}
element.dispatchEvent(
new MouseEvent('mousedown', {
bubbles: true,
cancelable: true,
view: window,
clientX,
clientY,
}),
);
if (typeof PointerEvent === 'function') {
element.dispatchEvent(
new PointerEvent('pointerup', {
bubbles: true,
cancelable: true,
view: window,
pointerId: 1,
pointerType: 'mouse',
isPrimary: true,
clientX,
clientY,
}),
);
}
element.dispatchEvent(
new MouseEvent('mouseup', {
bubbles: true,
cancelable: true,
view: window,
clientX,
clientY,
}),
);
element.click();
return true;
}
function hoverElement(element) {
if (!(element instanceof HTMLElement)) {
return false;
}
const rect = element.getBoundingClientRect();
if (rect.width <= 0 || rect.height <= 0) {
return false;
}
const clientX = Math.round(rect.left + rect.width / 2);
const clientY = Math.round(rect.top + rect.height / 2);
if (typeof PointerEvent === 'function') {
['pointerover', 'pointerenter', 'pointermove'].forEach((type) => {
element.dispatchEvent(
new PointerEvent(type, {
bubbles: type !== 'pointerenter',
cancelable: true,
view: window,
pointerId: 1,
pointerType: 'mouse',
isPrimary: true,
clientX,
clientY,
}),
);
});
}
['mouseover', 'mouseenter', 'mousemove'].forEach((type) => {
element.dispatchEvent(
new MouseEvent(type, {
bubbles: type !== 'mouseenter',
cancelable: true,
view: window,
clientX,
clientY,
}),
);
});
return true;
}
function getReactProps(element) {
if (!(element instanceof HTMLElement)) {
return null;
}
const reactPropsKey = Object.keys(element).find((key) => key.startsWith('__reactProps$'));
if (reactPropsKey && element[reactPropsKey] && typeof element[reactPropsKey] === 'object') {
return element[reactPropsKey];
}
const reactFiberKey = Object.keys(element).find((key) => key.startsWith('__reactFiber$'));
const fiber = reactFiberKey ? element[reactFiberKey] : null;
if (fiber && fiber.memoizedProps && typeof fiber.memoizedProps === 'object') {
return fiber.memoizedProps;
}
return null;
}
function createReactLikeMouseEvent(type, currentTarget, target) {
const rect = currentTarget.getBoundingClientRect();
const clientX = Math.round(rect.left + rect.width / 2);
const clientY = Math.round(rect.top + rect.height / 2);
let defaultPrevented = false;
let propagationStopped = false;
return {
type,
target,
currentTarget,
nativeEvent: {
type,
target,
currentTarget,
clientX,
clientY,
button: 0,
buttons: type === 'mouseup' ? 0 : 1,
pointerId: 1,
pointerType: 'mouse',
isPrimary: true,
preventDefault() {},
stopPropagation() {},
},
clientX,
clientY,
button: 0,
buttons: type === 'mouseup' ? 0 : 1,
pointerId: 1,
pointerType: 'mouse',
isPrimary: true,
preventDefault() {
defaultPrevented = true;
},
isDefaultPrevented() {
return defaultPrevented;
},
stopPropagation() {
propagationStopped = true;
},
isPropagationStopped() {
return propagationStopped;
},
persist() {},
};
}
function invokeReactHandler(element, handlerName, eventType) {
let current = element;
let depth = 0;
while (current && current !== document.body && depth < 5) {
const props = getReactProps(current);
if (props && typeof props[handlerName] === 'function') {
props[handlerName](createReactLikeMouseEvent(eventType, current, element));
return true;
}
current = current.parentElement;
depth += 1;
}
return false;
}
function invokeReactHover(element) {
if (!(element instanceof HTMLElement)) {
return false;
}
let handled = false;
handled = invokeReactHandler(element, 'onPointerEnter', 'pointerenter') || handled;
handled = invokeReactHandler(element, 'onMouseEnter', 'mouseenter') || handled;
handled = invokeReactHandler(element, 'onMouseOver', 'mouseover') || handled;
handled = invokeReactHandler(element, 'onMouseMove', 'mousemove') || handled;
return handled;
}
function invokeReactClick(element) {
if (!(element instanceof HTMLElement)) {
return false;
}
let handled = false;
handled = invokeReactHandler(element, 'onPointerDown', 'pointerdown') || handled;
handled = invokeReactHandler(element, 'onMouseDown', 'mousedown') || handled;
handled = invokeReactHandler(element, 'onPointerUp', 'pointerup') || handled;
handled = invokeReactHandler(element, 'onMouseUp', 'mouseup') || handled;
handled = invokeReactHandler(element, 'onClick', 'click') || handled;
return handled;
}
function hasOwnReactHandler(element, handlerName) {
if (!(element instanceof HTMLElement)) {
return false;
}
const props = getReactProps(element);
return Boolean(props && typeof props[handlerName] === 'function');
}
function getQualitySelector() {
const selector = document.querySelector('[data-e2e="quality-selector"]');
return selector instanceof HTMLElement ? selector : null;
}
function getQualityTrigger() {
const trigger = document.querySelector('[data-e2e="quality"]');
return trigger instanceof HTMLElement ? trigger : null;
}
function getQualityHoverTargets() {
const trigger = getQualityTrigger();
if (!(trigger instanceof HTMLElement)) {
return [];
}
return [
trigger.parentElement,
trigger.parentElement ? trigger.parentElement.parentElement : null,
trigger,
].filter((node, index, array) => node instanceof HTMLElement && array.indexOf(node) === index);
}
function getQualityOptions() {
const selector = getQualitySelector();
if (!(selector instanceof HTMLElement)) {
return [];
}
return Array.from(selector.children)
.map((node) => (node.firstElementChild instanceof HTMLElement ? node.firstElementChild : node))
.filter((node) => node instanceof HTMLElement && normalizeText(getTextBundle(node)));
}
function getQualityOptionFromTarget(target) {
if (!(target instanceof Element)) {
return null;
}
const selector = target.closest('[data-e2e="quality-selector"]');
if (!(selector instanceof HTMLElement)) {
return null;
}
let current = target instanceof HTMLElement ? target : target.parentElement;
while (current && current !== selector) {
if (current.parentElement === selector) {
return current.firstElementChild instanceof HTMLElement ? current.firstElementChild : current;
}
current = current.parentElement;
}
return null;
}
function getQualityOptionActionTarget(option) {
if (!(option instanceof HTMLElement)) {
return null;
}
const candidates = [option.parentElement, option].filter((node) => node instanceof HTMLElement);
return (
candidates.find(
(node) => hasOwnReactHandler(node, 'onClick') || hasOwnReactHandler(node, 'onMouseEnter'),
) || option
);
}
function maybeDisableAutoBestQualityByUserTarget(target) {
if (!state.enabled || !isModuleEnabled('autoBestQuality')) {
return false;
}
const clickedOption = getQualityOptionFromTarget(target);
if (!(clickedOption instanceof HTMLElement)) {
return false;
}
const highestOption = getQualityOptions()[0];
if (!(highestOption instanceof HTMLElement)) {
return disableAutomationModuleByUser('autoBestQuality');
}
const clickedLabel = normalizeText(getTextBundle(clickedOption));
const highestLabel = normalizeText(getTextBundle(highestOption));
if (!clickedLabel || !highestLabel || clickedLabel === highestLabel) {
return false;
}
return disableAutomationModuleByUser('autoBestQuality');
}
function findReactActionTarget(root, handlerNames = ['onClick']) {
if (!(root instanceof HTMLElement)) {
return null;
}
const candidates = [root, ...root.querySelectorAll('*')].filter((node) => node instanceof HTMLElement);
return (
candidates.find((node) => handlerNames.some((handlerName) => hasOwnReactHandler(node, handlerName))) ||
null
);
}
function isQualityOptionActive(option) {
if (!(option instanceof HTMLElement)) {
return false;
}
const styleText = String(option.getAttribute('style') || '').replace(/\s+/g, '');
if (/background:rgba\(242,242,244,0\.08\)/.test(styleText)) {
return true;
}
const computed = window.getComputedStyle(option);
return (
computed.backgroundColor !== 'rgba(0, 0, 0, 0)' &&
computed.backgroundColor !== 'transparent' &&
computed.color === 'rgb(255, 255, 255)'
);
}
function isHighestQualitySelected(trigger, option) {
if (!(trigger instanceof HTMLElement) || !(option instanceof HTMLElement)) {
return false;
}
if (isQualityOptionActive(option)) {
return true;
}
const triggerLabel = normalizeText(getTextBundle(trigger));
const optionLabel = normalizeText(getTextBundle(option));
return Boolean(triggerLabel && optionLabel && triggerLabel === optionLabel);
}
function applyAutoBestQuality() {
if (!isModuleEnabled('autoBestQuality')) {
return;
}
const now = Date.now();
const trigger = getQualityTrigger();
const selector = getQualitySelector();
const highestOption = getQualityOptions()[0];
if (!(trigger instanceof HTMLElement) || !(selector instanceof HTMLElement) || !(highestOption instanceof HTMLElement)) {
state.autoBestQualityDone = false;
if (now - state.lastBestQualityAttemptAt >= 900) {
state.lastBestQualityAttemptAt = now;
wakePlayerControls();
scheduleScan(document.body, 260);
}
return;
}
if (state.autoBestQualityDone && !isHighestQualitySelected(trigger, highestOption)) {
disableAutomationModuleByUser('autoBestQuality');
return;
}
if (isHighestQualitySelected(trigger, highestOption)) {
state.autoBestQualityDone = true;
return;
}
state.autoBestQualityDone = false;
if (now - state.lastBestQualityAttemptAt < 650) {
return;
}
state.lastBestQualityAttemptAt = now;
wakePlayerControls(true);
const highestOptionActionTarget = getQualityOptionActionTarget(highestOption);
if (highestOptionActionTarget instanceof HTMLElement) {
invokeReactHover(highestOptionActionTarget);
if (invokeReactClick(highestOptionActionTarget)) {
scheduleScan(document.body, 260);
return;
}
}
if (!isVisible(selector)) {
const hoverTargets = getQualityHoverTargets();
hoverTargets.forEach((target) => {
invokeReactHover(target);
hoverElement(target);
});
if (!isVisible(selector)) {
if (!invokeReactClick(trigger)) {
clickElement(trigger);
}
}
scheduleScan(document.body, 180);
return;
}
invokeReactHover(selector);
invokeReactHover(highestOptionActionTarget || highestOption);
hoverElement(selector);
hoverElement(highestOptionActionTarget || highestOption);
if (
invokeReactClick(highestOptionActionTarget || highestOption) ||
clickElement(highestOptionActionTarget || highestOption)
) {
scheduleScan(document.body, 340);
}
}
function resolveSlotContent(slot) {
if (!(slot instanceof Element)) {
return null;
}
if (slot.firstElementChild instanceof HTMLElement) {
return slot.firstElementChild;
}
if (typeof slot.assignedElements === 'function') {
const assigned = slot.assignedElements({ flatten: true }).find((node) => node instanceof HTMLElement);
return assigned instanceof HTMLElement ? assigned : null;
}
return null;
}
function collectPlayerControlRoots(controlsRoot) {
if (!(controlsRoot instanceof HTMLElement)) {
return [];
}
const roots = [];
const pushRoot = (node) => {
if (node instanceof HTMLElement && !roots.includes(node)) {
roots.push(node);
}
};
Array.from(controlsRoot.children).forEach((node) => {
if (node instanceof HTMLElement) {
pushRoot(node);
}
});
controlsRoot.querySelectorAll('slot').forEach((slot) => {
pushRoot(resolveSlotContent(slot));
});
controlsRoot.querySelectorAll('button, [role="button"]').forEach((node) => {
pushRoot(node);
pushRoot(node.parentElement);
});
return roots;
}
function isXgWebFullscreenControlElement(control) {
return Boolean(
control instanceof HTMLElement &&
(control.matches('[data-e2e="xgplayer-page-full-screen"], .xgplayer-page-full-screen') ||
control.closest('[data-e2e="xgplayer-page-full-screen"], .xgplayer-page-full-screen')),
);
}
function getWebFullscreenControlScore(control) {
if (!(control instanceof HTMLElement)) {
return -1;
}
const text = normalizeText([getTextBundle(control), control.textContent].filter(Boolean).join(' '));
const isXgWebFullscreenControl = isXgWebFullscreenControlElement(control);
if (!text && !isXgWebFullscreenControl) {
return -1;
}
const hasShortcutY = /(?:^|\s)Y(?:\s|$)/i.test(text);
if (!/网页全屏/.test(text) && !isXgWebFullscreenControl) {
return hasShortcutY && !/设置|弹幕|礼物|画质|小窗|旋转|全屏/.test(text) ? 12 : -1;
}
let score = 0;
if (/退出网页全屏/.test(text)) {
score += 40;
} else if (/进入网页全屏/.test(text)) {
score += 36;
} else {
score += 32;
}
if (hasShortcutY) {
score += 3;
}
if (isXgWebFullscreenControl) {
score += 8;
}
score += isVisible(control) ? 12 : -12;
const slot = control.closest('slot');
if (slot instanceof Element && slot.getAttribute('data-index') === '2') {
score += 1;
}
return score;
}
function getPlayerControlContainers() {
return [
document.getElementById('PlayerControlLayout'),
document.querySelector('.douyin-player-controls-right'),
document.querySelector('.xg-right-grid'),
document.querySelector('xg-right-grid'),
].filter((node, index, array) => node instanceof HTMLElement && array.indexOf(node) === index);
}
function findWebFullscreenControl() {
const controlContainers = getPlayerControlContainers();
const candidates = [];
const pushCandidate = (candidate) => {
if (candidate instanceof HTMLElement && !candidates.includes(candidate)) {
candidates.push(candidate);
}
};
pushCandidate(document.querySelector('[data-e2e="xgplayer-page-full-screen"]'));
pushCandidate(document.querySelector('.xgplayer-page-full-screen'));
controlContainers.forEach((container) => {
collectPlayerControlRoots(container).forEach((control) => {
pushCandidate(control);
});
});
const matchedByText = candidates
.map((control) => ({
control,
score: getWebFullscreenControlScore(control),
}))
.filter(({ score }) => score >= 0)
.sort((left, right) => right.score - left.score)[0]?.control;
if (matchedByText instanceof HTMLElement) {
return matchedByText;
}
if (controlContainers.length <= 0) {
return null;
}
const slotCandidates = [];
const pushSlot = (slot) => {
if (slot instanceof HTMLElement && !slotCandidates.includes(slot)) {
slotCandidates.push(slot);
}
};
controlContainers.forEach((container) => {
pushSlot(container.querySelector('slot[data-index="2"]'));
});
controlContainers.forEach((container) => {
container.querySelectorAll('slot').forEach(pushSlot);
});
for (const slot of slotCandidates) {
const content = resolveSlotContent(slot);
if (getWebFullscreenControlScore(content) >= 0) {
return content;
}
}
return null;
}
function getWebFullscreenActionTarget(control) {
if (!(control instanceof HTMLElement)) {
return null;
}
return (
findReactActionTarget(control, ['onClick', 'onPointerDown', 'onMouseDown', 'onPointerUp', 'onMouseUp']) ||
control.querySelector('button, [role="button"]') ||
control
);
}
function isWebFullscreenControlTarget(target) {
if (!(target instanceof Element)) {
return false;
}
const control = findWebFullscreenControl();
return Boolean(control instanceof HTMLElement && (control === target || control.contains(target)));
}
function maybeDisableAutoWebFullscreenByUserTarget(target) {
if (!state.enabled || !isModuleEnabled('autoWebFullscreen')) {
return false;
}
if (!isWebFullscreenControlTarget(target)) {
return false;
}
return disableAutomationModuleByUser('autoWebFullscreen');
}
function shouldIgnoreWebFullscreenShortcutEvent(event) {
if (!(event.target instanceof HTMLElement)) {
return false;
}
return event.target.matches('input, textarea, select') || event.target.isContentEditable;
}
function isWebFullscreenActive(control) {
if (document.fullscreenElement) {
return true;
}
const controlState = normalizeText(control?.getAttribute('data-state'));
if (controlState && !/^(?:normal|default|inactive|closed|off|false|0)$/i.test(controlState)) {
return true;
}
const text = getTextBundle(control);
if (/退出网页全屏/.test(text)) {
return true;
}
if (/^(?:进入)?网页全屏$/.test(text) || /(?:^|\s)(?:进入)?网页全屏(?:\s|$)/.test(text)) {
return false;
}
const playerLayout = document.getElementById('PlayerLayout') || getPrimaryPlayerSurface();
if (playerLayout instanceof HTMLElement) {
const rect = playerLayout.getBoundingClientRect();
if (rect.width >= window.innerWidth * 0.78 && rect.height >= window.innerHeight * 0.72) {
return true;
}
}
return false;
}
function applyAutoWebFullscreen() {
if (!isModuleEnabled('autoWebFullscreen')) {
return;
}
const now = Date.now();
const control = findWebFullscreenControl();
if (!(control instanceof HTMLElement)) {
state.autoWebFullscreenDone = false;
if (now - state.lastWebFullscreenAttemptAt >= 900) {
state.lastWebFullscreenAttemptAt = now;
wakePlayerControls();
scheduleScan(document.body, 260);
}
return;
}
const webFullscreenActive = isWebFullscreenActive(control);
if (state.autoWebFullscreenDone && !webFullscreenActive) {
disableAutomationModuleByUser('autoWebFullscreen');
return;
}
if (webFullscreenActive) {
state.autoWebFullscreenDone = true;
return;
}
state.autoWebFullscreenDone = false;
if (now - state.lastWebFullscreenAttemptAt < 700) {
return;
}
state.lastWebFullscreenAttemptAt = now;
wakePlayerControls(true);
const actionTarget = getWebFullscreenActionTarget(control);
if (!isVisible(control)) {
scheduleScan(document.body, 260);
return;
}
if (
(actionTarget instanceof HTMLElement && invokeReactClick(actionTarget)) ||
clickElement(actionTarget || control)
) {
scheduleScan(document.body, 360);
}
}
function shouldRevealProtectedElement(element) {
if (!(element instanceof HTMLElement) || isExplicitHideElement(element)) {
return false;
}
if (isModuleEnabled('chatNotices') && CHAT_NOTICE_REASONS.has(element.getAttribute(HIDE_ATTR))) {
return false;
}
if (isModuleEnabled('chatBadges') && CHAT_BADGE_REASONS.has(element.getAttribute(HIDE_ATTR))) {
return false;
}
if (element.hasAttribute(SAFE_ATTR) || isExplicitSafeElement(element) || hasProtectedDescendant(element)) {
return true;
}
return false;
}
function revealProtectedElements() {
document.querySelectorAll(`[${HIDE_ATTR}]`).forEach((node) => {
if (shouldRevealProtectedElement(node)) {
unhideElement(node);
}
});
}
function revealAll() {
closeChatReplayOverlay();
document.querySelectorAll(`[${HIDE_ATTR}]`).forEach((node) => {
node.removeAttribute(HIDE_ATTR);
});
state.hiddenCount = 0;
updateUi();
}
function revealModuleHides(moduleId) {
const reasons = MODULE_HIDE_REASONS[moduleId];
if (!reasons || reasons.size <= 0) {
return;
}
document.querySelectorAll(`[${HIDE_ATTR}]`).forEach((node) => {
if (!(node instanceof HTMLElement)) {
return;
}
const reason = String(node.getAttribute(HIDE_ATTR) || '');
if (reasons.has(reason)) {
unhideElement(node);
}
});
state.hiddenCount = document.querySelectorAll(`[${HIDE_ATTR}]`).length;
updateUi();
}
function scan(root = document.body) {
if (!state.enabled || !(root instanceof HTMLElement)) {
return;
}
syncRoomContext();
if (!state.roomKey) {
return;
}
detectSafeZones();
revealProtectedElements();
applyExplicitHides();
applyIndicatorHides();
applyChatFoldSiblingHides();
applyTopWidgetTooltipHides();
applyChatNoticeHides();
applyChatBadgeHides();
applyAutoBestQuality();
applyAutoWebFullscreen();
const nodes = [root, ...root.querySelectorAll('*')];
for (const node of nodes) {
if (shouldSkip(node)) {
continue;
}
if (shouldHide(node)) {
hideElement(node, 'matched');
}
}
state.hiddenCount = document.querySelectorAll(`[${HIDE_ATTR}]`).length;
updateUi();
}
function scheduleScan(root = document.body, delay = 160) {
if (!state.enabled) {
return;
}
syncRoomContext();
if (!state.roomKey) {
return;
}
window.clearTimeout(state.scanTimer);
state.scanTimer = window.setTimeout(() => {
scan(root instanceof HTMLElement ? root : document.body);
}, delay);
}
function removeUiDock() {
document.querySelectorAll(`[${UI_ATTR}="dock"]`).forEach((node) => {
node.remove();
});
}
function getChatInputDockAnchor(chatInput) {
if (!(chatInput instanceof HTMLElement)) {
return null;
}
const actionButtons = Array.from(chatInput.querySelectorAll('button, [role="button"]')).filter(
(node) =>
node instanceof HTMLElement &&
!node.closest(`[${UI_ATTR}]`) &&
isVisible(node),
);
const emojiButton = actionButtons.find((button) => /表情|emoji/i.test(getTextBundle(button)));
if (emojiButton instanceof HTMLElement) {
return emojiButton;
}
return actionButtons[actionButtons.length - 1] instanceof HTMLElement
? actionButtons[actionButtons.length - 1]
: null;
}
function ensureUiMounted() {
if (!state.roomKey) {
removeUiDock();
return {};
}
const chatInput = document.querySelector('#chatInput');
if (!(chatInput instanceof HTMLElement)) {
return {};
}
let dock = chatInput.querySelector(`[${UI_ATTR}="dock"]`);
if (!(dock instanceof HTMLElement)) {
const moduleItems = MODULE_CONFIGS.map(
(module) => `
`,
).join('');
dock = document.createElement('div');
dock.setAttribute(UI_ATTR, 'dock');
dock.innerHTML = `